Jump to content

User:Barkeep49/rfxCloser.js

From Wikipedia, the free encyclopedia
The printable version is no longer supported and may have rendering errors. Please update your browser bookmarks and please use the default browser print function instead.
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.0.0
 */
(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',
            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"]'
        },
        hardcodedCrats: [
            '28bytes', 'Acalamari', 'AmandaNP', 'Avraham', 'Barkeep49',
            'Bibliomaniac15', 'Cecropia', 'Dweller', 'Lee Vilenski', 'Maxim',
            'Primefac', 'UninvitedCompany', 'Useight', 'WereSpielChequers',
            'Xaosflux', 'Xeno'
        ],
        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

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

    // --- 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})` };
        }
    }

    // --- UI Update Functions ---

    /** Updates the percentage display based on input fields. */
    const updatePercentageDisplay = () => {
        const supportInputEl = document.querySelector(config.selectors.supportInput);
        const opposeInputEl = document.querySelector(config.selectors.opposeInput);
        const percentageDivEl = document.querySelector(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 = document.querySelector(config.selectors.supportInput);
            const opposeInputEl = document.querySelector(config.selectors.opposeInput);
            const neutralInputEl = document.querySelector(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 = document.querySelector(config.selectors.supportInput);
        const opposeInputEl = document.querySelector(config.selectors.opposeInput);
        const neutralInputEl = document.querySelector(config.selectors.neutralInput);
        return {
            support: supportInputEl?.value || '0',
            oppose: opposeInputEl?.value || '0',
            neutral: neutralInputEl?.value || '0'
        };
    }

    /** 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="/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">(?/?/?)</span>; ended by ~~</nowiki>~~`;
            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
        container.appendChild(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' }
        }));
        const cratListTextarea = createElement('textarea', {
            id: config.selectors.cratListTextarea.substring(1), // Remove # for ID
            className: 'rfx-onhold-notify-textarea',
            rows: 6,
            value: config.hardcodedCrats.join('\n') // Use config
        });
        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}. ~~<nowiki/>~~`
        });
        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
        const updateInitialStatus = () => {
            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';
        };
        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}.  ~~</nowiki>~~`
        });
        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: '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="/wiki/Special:Userrights/${encodeURIComponent(actualCandidateUsername)}" target="_blank">Special:Userrights/${actualCandidateUsername}</a>` }),
            createElement('li', { innerHTML: `Reference: <a href="/wiki/Special:ListGroupRights" target="_blank">Special:ListGroupRights</a>` })
        ]);
        container.appendChild(list);
        return container;
    }

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

        // --- 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' } }));
        }

        // --- 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;
        const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
        const now = new Date(); let day = now.getDate(); let month = months[now.getMonth()]; let year = now.getFullYear(); let formattedDate = `${day} ${month} ${year}`;
        // Attempt to parse date from fetched data
        if (rfaData && rfaData.endTime && rfaData.endTime !== 'N/A') {
            try { const dateParts = rfaData.endTime.match(/(\d{1,2}) (\w+) (\d{4})/); if (dateParts) { formattedDate = `${dateParts[1]} ${dateParts[2]} ${dateParts[3]}`; day = parseInt(dateParts[1], 10); month = dateParts[2]; year = parseInt(dateParts[3], 10); } else { console.warn("Could not parse DD Month YYYY from endTime:", rfaData.endTime); } } catch (e) { console.warn("Error parsing endTime:", rfaData.endTime, e); }
        }

        // 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}|successful|${votes.support}|${votes.oppose}|${votes.neutral}|${config.userName}}}`;
        } 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);
            generatedListEntry = `${isSubsequentNomination ? '*:' : '*'} [[${config.pageName}|${actualCandidateUsername}]] ${formattedDate} - ${reasonText} ([[User:${config.userName}|${config.userName}]]) (${votes.support}/${votes.oppose}/${votes.neutral})`;
            generatedRfarowTemplate = `|{{rfarow|${actualCandidateUsername}||${formattedDate}|${rfarowResult}|${votes.support}|${votes.oppose}|${votes.neutral}|${config.userName}}}`;
        }

        // --- Handle Yearly Page Update ---
        if (yearlyListPageName && generatedRfarowTemplate) {
            const yearlyDiv = createElement('div', { style: { marginTop: '10px' } });
            yearlyDiv.appendChild(createElement('p', { innerHTML: `2a. Update <a href="/wiki/${yearlyListPageName}" target="_blank">${yearlyListPageName}</a>:` }));
            yearlyDiv.appendChild(createElement('div', { className: 'rfx-closer-known-issue', innerHTML: `<strong>Known Issue:</strong> The script may fail to automatically uncomment the correct month header if it's currently commented out. Please double-check the yearly list page and manually uncomment the header (e.g., remove <code>&lt;!--</code> and <code>--&gt;</code>) if needed before saving.` }));
            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}`); }

                    const escapedMonth = month.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
                    // Updated regex to match both commented and uncommented month headers
                    const monthHeaderRegex = new RegExp(`^(?:<!--)?\\|-\\s*\\n\\|\\s*colspan="[\\d]+"\\s*style="background:#ffdead;"\\s*\\|\\s*'''\\{\\{Anchord\\|${escapedMonth}\\s+${year}\\}\\}'''.*?$(?:\\n-->)?`, 'mi');
                    let insertionIndex = -1;
                    let headerFound = false;
                    let originalWikitextForInsertionCheck = modifiedYearlyWikitext;
                    let headerMatch = modifiedYearlyWikitext.match(monthHeaderRegex);

                    if (headerMatch) {
                        headerFound = true;
                        const fullMatchText = headerMatch[0];
                        const headerStartIndex = headerMatch.index;
                        const isCommented = fullMatchText.startsWith('<!--');

                        if (isCommented) {
                            uncommentAttempted = true;
                            console.warn(`RfX Closer: Attempting to uncomment month header for ${month} ${year}.`);
                            // Find the end of the commented section
                            const commentEndIndex = modifiedYearlyWikitext.indexOf('-->', headerStartIndex);
                            if (commentEndIndex !== -1) {
                                // Extract the content between <!-- and -->
                                const commentedContent = modifiedYearlyWikitext.substring(headerStartIndex + 4, commentEndIndex);
                                // Replace the entire commented section with the uncommented content
                                modifiedYearlyWikitext = modifiedYearlyWikitext.substring(0, headerStartIndex) + 
                                                       commentedContent + 
                                                       modifiedYearlyWikitext.substring(commentEndIndex + 3);
                                modificationPerformed = true;
                                // Find the next |- after the header
                                const nextRowIndex = modifiedYearlyWikitext.indexOf('|-', headerStartIndex);
                                insertionIndex = nextRowIndex !== -1 ? nextRowIndex : headerStartIndex + commentedContent.length;
                            } else {
                                headerFound = false;
                                console.error(`RfX Closer: Failed to find end of comment for ${month} ${year} header.`);
                            }
                        } else {
                            // For uncommented headers, find the next |- or end of the section
                            const nextRowIndex = modifiedYearlyWikitext.indexOf('|-', headerStartIndex + fullMatchText.length);
                            insertionIndex = nextRowIndex !== -1 ? nextRowIndex : headerStartIndex + fullMatchText.length;
                        }
                    } 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 && entryAdded) { noteText += `Count updated and entry added.`; }
                    else if (countUpdated && !entryAdded) { noteText += `Count updated (could not add entry).`; }
                    else if (!countUpdated && entryAdded) { noteText += `Entry added (could not update count).`; }
                    else if (!countUpdated && !entryAdded) { noteText = ` Could not automatically update ${yearlyListPageName}.`; }
                    if (uncommentAttempted && !entryAdded) { noteText += ` (Failed to uncomment month header or insert entry).`; }
                    else if (uncommentAttempted) { noteText += ` (Attempted to uncomment month header - 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="/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="/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 defaultMessage = `{{subst:${templateName}}} [[User:${config.userName}|${config.userName}]] ([[User talk:${config.userName}|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:${config.userName}|${config.userName}]] ([[User talk:${config.userName}|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 defaultMessage = `Hi ${actualCandidateUsername}. I'm ${config.userName} 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.  ~~</nowiki>~~`
            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 = document.querySelector(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 = document.querySelector(config.selectors.outcomeSelector);
        if (selector) selector.value = selectedOutcome;
    }

    // --- Event Handlers ---

    /** Handles clicks on the "Notify Bureaucrats" button (Step 6). */
    async function handleNotifyCratsClick() {
        const postButton = document.querySelector(config.selectors.cratNotifyButton);
        const statusArea = document.querySelector(config.selectors.cratNotifyStatus);
        const cratListTextarea = document.querySelector(config.selectors.cratListTextarea);
        const messageTextarea = document.querySelector(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 = document.querySelector(config.selectors.candidateOnholdNotifyButton);
        const statusArea = document.querySelector(config.selectors.candidateOnholdNotifyStatus);
        const messageTextarea = document.querySelector(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 = document.querySelector(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 = document.querySelector(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 = document.querySelector(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 = document.querySelector(config.selectors.container);
        const contentAndInputContainerEl = document.querySelector(config.selectors.contentAndInputContainer);
        const collapseButtonEl = document.querySelector(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(document.querySelector(config.selectors.outcomeSelector)?.value || '', getCurrentVoteCounts());
        }
    }

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

    /** Handles the main launch button click. */
    function handleLaunchClick(e) {
        e.preventDefault();
        const containerEl = document.querySelector(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 = document.querySelector(config.selectors.supportInput);
                const opposeInputEl = document.querySelector(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 = document.querySelector(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) => createElement('div', {}, [
            createElement('label', { textContent: label, htmlFor: id }),
            createElement('input', { type: 'number', id: id, min: '0' })
        ]);

        // 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 inputFields = createElement('div', { className: config.selectors.inputFields.substring(1) }, [supportInputContainer, opposeInputContainer, neutralInputContainer]);
        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;
            }

            /* 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 = document.querySelector(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).");

})();