Jump to content

User:Barkeep49/rfxCloser.js

From Wikipedia, the free encyclopedia
This is an old revision of this page, as edited by Barkeep49 (talk | contribs) at 22:25, 15 April 2025 (test). The present address (URL) is a permanent link to this revision, which may differ significantly from the current revision.
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 - A tool for closing Requests for Adminship/Bureaucratship on Wikipedia
 *
 * CHANGE LOG:
 * - Added functionality to generate wikitext for inserting unsuccessful candidates into the correct alphabetical list page (e.g., /A, /B, etc.).
 * - Made support percentage display update dynamically when support/oppose counts are manually changed.
 * - Added step 7: Notify Candidate on their talk page with templates/custom messages.
 * - Added API button to automatically post the notification message in Step 7.
 * - Replaced most wikitext display boxes with "Copy Code" and "Edit Page" links.
 * - Updated finaltally reason for withdrawn to 'WD'.
 * - Clarified text in Step 6. Restored action links for yearly chrono list. Restored intro text for list updates section.
 * - Updated help text for custom message copy button in Step 7.
 * - Added warning about known issue with uncommenting month headers in Step 6.
 * - Fixed bug where incorrect outcome code (with wikilinks) was used in rfarow template for NOTNOW/SNOW.
 * - Updated rfarow template generation to use full outcome words (e.g., "unsuccessful") instead of codes (e.g., "f").
 * - Restored "On hold" outcome option.
 * - Updated Step 4 logic for "On hold" to include finaltally and chat link.
 * - Added new Step 5 to guide creation of Bureaucrat Chat page for "On hold" outcome.
 * - Added new Step 6 (for 'onhold' only): Notify Bureaucrats via API about the crat chat.
 * - Added new Step 7 (for 'onhold' only): Notify Candidate via API about the hold.
 * - Renumbered subsequent steps (Promotion, Update Lists, Notify Candidate are now 8, 9, 10).
 * - Adjusted visibility logic for steps based on 'onhold' outcome.
 * - Fixed typo in fetchBureaucrats API call (gmgrou -> gmgroup).
 * - Improved error checking and logging in fetchBureaucrats.
 */
(function() {
    'use strict';

    // --- Configuration & Initialization ---

    // Check if the current page is a main RfA or RfB subpage
    const pageName = mw.config.get('wgPageName');
    const rfxType = pageName.includes('Requests_for_adminship') ? 'adminship' : 'bureaucratship';
    // Use underscores for internal API calls and links where MediaWiki handles normalization
    const baseRfxPage = `Wikipedia:Requests_for_${rfxType}`;
    // Use spaces for display text
    const displayBaseRfxPage = baseRfxPage.replace(/_/g, ' ');

    if (!new RegExp(`^${baseRfxPage.replace(/ /g, '_')}/[^/]+$`).test(pageName)) { // Ensure regex uses underscores
        // Don't run on talk pages, sub-subpages (/Support, etc.), or the main RfA list page
        console.log('RfX Closer: Not on a main RfX subpage. Exiting.');
        return;
    }
    const candidateSubpage = pageName.split('/').pop(); // Get the candidate part of the pagename
    const tagLine = ' (using [[User:Barkeep49/rfxCloser.js|rfxCloser]])'; // Adjusted tagline
    const closerUsername = mw.config.get('wgUserName') || 'YourUsername'; // Get current user or fallback

    // Store fetched data to avoid multiple API calls
    let rfaData = null;
    let rfaWikitext = null; // Store fetched wikitext for *this* RfX page
    let basePageWikitextCache = {}; // Cache for other pages like the base RfX list
    let bureaucratListCache = null; // Cache for bureaucrat list
    let actualCandidateUsername = candidateSubpage; // Default to subpage name
    let fetchErrorOccurred = false; // Flag to track if the initial fetch failed
    let wikitextErrorOccurred = false; // Flag for wikitext fetch error

    // --- UI Element Creation ---

    const container = document.createElement('div');
    container.id = 'rfx-closer-container';
    // Styles moved to CSS block below for better readability

    const header = document.createElement('div');
    header.className = 'rfx-closer-header';

    const title = document.createElement('h2');
    title.textContent = 'RfX Closer';
    title.className = 'rfx-closer-title';

    const headerButtonContainer = document.createElement('div');

    const collapseButton = document.createElement('button');
    collapseButton.innerHTML = '−'; // Use innerHTML for entities
    collapseButton.title = 'Collapse/Expand';
    collapseButton.className = 'rfx-closer-button rfx-closer-collapse';

    const closeButton = document.createElement('button');
    closeButton.innerHTML = '×'; // Use innerHTML for entities
    closeButton.title = 'Close';
    closeButton.className = 'rfx-closer-button rfx-closer-close';

    header.appendChild(title);
    headerButtonContainer.appendChild(collapseButton);
    headerButtonContainer.appendChild(closeButton);
    header.appendChild(headerButtonContainer);

    const contentAndInputContainer = document.createElement('div');
    contentAndInputContainer.className = 'rfx-closer-main-content';

    const inputSection = document.createElement('div');
    inputSection.className = 'rfx-closer-input-section';

    const inputFields = document.createElement('div');
    inputFields.className = 'rfx-closer-input-fields';

    const createInputField = (label, id) => {
        const inputContainer = document.createElement('div');
        const labelElement = document.createElement('label');
        labelElement.textContent = label;
        labelElement.htmlFor = id;
        const input = document.createElement('input');
        input.type = 'number';
        input.id = id;
        input.min = '0';
        inputContainer.appendChild(labelElement);
        inputContainer.appendChild(input);
        return inputContainer;
    };

    inputFields.appendChild(createInputField('Support', 'support-count'));
    inputFields.appendChild(createInputField('Oppose', 'oppose-count'));
    inputFields.appendChild(createInputField('Neutral', 'neutral-count'));
    inputSection.appendChild(inputFields);

    // --- Create Percentage Display Area ---
    // Create it here so it exists before update functions are called
    const percentageDiv = document.createElement('div');
    percentageDiv.className = 'rfx-closer-percentage';
    percentageDiv.textContent = 'Support percentage: N/A'; // Initial text
    inputSection.appendChild(percentageDiv); // Add it to the input section


    const contentContainer = document.createElement('div');
    contentContainer.className = 'rfx-closer-content-container';

    const stepsContainer = document.createElement('div');
    stepsContainer.id = 'rfx-closer-steps';

    // --- CSS Styling ---
    const styles = `
        #rfx-closer-container {
            position: fixed;
            right: 10px;
            top: 50%;
            transform: translateY(-50%);
            width: 380px; /* Increased width slightly */
            max-height: 90vh;
            background: #f8f9fa;
            border: 1px solid #a2a9b1;
            border-radius: 5px; /* Slightly more rounded */
            z-index: 1001; /* Ensure it's above most elements */
            box-shadow: 0 4px 8px rgba(0,0,0,0.15); /* Stronger shadow */
            display: none; /* Initially hidden */
            font-family: sans-serif;
            font-size: 14px; /* Base font size */
            color: #202122;
            display: flex; /* Use flex for layout */
            flex-direction: column;
        }
        .rfx-closer-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 10px 15px;
            background: #f8f9fa;
            border-bottom: 1px solid #a2a9b1;
            cursor: grab;
            flex-shrink: 0; /* Prevent header from shrinking */
        }
        .rfx-closer-title {
            margin: 0;
            font-size: 1.15em; /* Adjusted size */
            font-weight: bold;
        }
        .rfx-closer-button {
            background: none;
            border: 1px solid transparent;
            font-size: 1.5em;
            cursor: pointer;
            color: #54595d;
            padding: 0 5px;
            line-height: 1;
            border-radius: 3px;
        }
        .rfx-closer-button:hover {
            background-color: #eaecf0; /* Subtle hover */
        }
        .rfx-closer-button:focus {
            outline: none;
            border-color: #36c; /* Focus indicator */
            box-shadow: 0 0 0 1px #36c;
        }
        .rfx-closer-action-button { /* Style for API button */
            padding: 5px 10px;
            background-color: #36c;
            color: white;
            border: 1px solid #36c;
            border-radius: 3px;
            cursor: pointer;
            margin-top: 10px; /* Add space above button */
            margin-right: 10px;
            font-size: 0.95em;
        }
        .rfx-closer-action-button:hover {
            background-color: #447ff5;
        }
         .rfx-closer-action-button:disabled {
            background-color: #a2a9b1;
            border-color: #a2a9b1;
            cursor: not-allowed;
        }
        .rfx-closer-api-status { /* Generic API status */
            font-size: 0.9em;
            font-style: italic;
            margin-top: 5px;
        }
        .rfx-closer-beta-warning { /* Style for beta warning */
            font-size: 0.9em;
            font-weight: bold;
            color: #8a6d3b; /* Bootstrap 'warning' text color */
            background-color: #fcf8e3; /* Bootstrap 'warning' background color */
            border: 1px solid #faebcc; /* Bootstrap 'warning' border color */
            padding: 8px;
            border-radius: 3px;
            margin-top: 5px;
            margin-bottom: 10px;
        }
        .rfx-closer-beta-warning a {
            color: #66512c; /* Darker link color for warning */
            text-decoration: underline;
        }
        /* Styles for OOUI elements within the step */
        .rfx-closer-step-description .oo-ui-fieldsetLayout {
            margin: 10px 0;
            border: 1px solid #c8ccd1;
            padding: 10px;
            border-radius: 2px;
        }
         .rfx-closer-step-description .oo-ui-fieldsetLayout-header {
            font-weight: bold;
            margin-bottom: 5px;
            padding-bottom: 5px;
            border-bottom: 1px solid #c8ccd1;
        }
         .rfx-closer-step-description .oo-ui-fieldLayout-align-inline > .oo-ui-fieldLayout-body > .oo-ui-fieldLayout-field {
            display: inline-block; /* Make checkbox and label align better */
            margin-right: 15px; /* Space between checkboxes */
            margin-bottom: 5px; /* Space below checkboxes if they wrap */
        }
        .rfx-closer-step-description .oo-ui-checkboxInputWidget {
            vertical-align: middle; /* Align checkboxes better with text */
        }

        .rfx-closer-collapse { margin-right: 5px; }
        .rfx-closer-main-content {
            overflow: hidden; /* Contains scrolling content */
            display: flex;
            flex-direction: column;
            flex-grow: 1; /* Allow this section to grow */
        }
        .rfx-closer-input-section {
            padding: 15px;
            border-bottom: 1px solid #a2a9b1;
            background: #fff;
            flex-shrink: 0; /* Prevent input section from shrinking */
        }
        .rfx-closer-input-fields {
            display: grid;
            grid-template-columns: repeat(3, 1fr); /* Always 3 columns */
            gap: 10px;
        }
        .rfx-closer-input-fields div {
            display: flex;
            flex-direction: column;
            gap: 4px;
        }
        .rfx-closer-input-fields label {
            font-size: 0.85em;
            color: #54595d;
        }
        .rfx-closer-input-fields input {
            padding: 6px 8px; /* Adjusted padding */
            border: 1px solid #a2a9b1;
            border-radius: 3px;
            width: 100%;
            box-sizing: border-box;
            text-align: right;
            font-size: 0.95em; /* Slightly larger input font */
        }
        .rfx-closer-content-container {
            padding: 15px;
            overflow-y: auto; /* Enable vertical scroll */
            flex-grow: 1; /* Allow content to take remaining space */
            overscroll-behavior: contain;
        }
        .rfx-closer-step {
            margin-bottom: 15px;
            padding: 12px; /* Increased padding */
            border: 1px solid #a2a9b1;
            border-radius: 4px;
            background: #fff;
            transition: background-color 0.3s ease; /* Smooth transition */
        }
        .rfx-closer-step[data-completed="true"] {
            background-color: #e6f3e6; /* Greenish background for completed */
            border-color: #c3e6cb;
        }
        .rfx-closer-step h3 {
            margin: 0 0 8px 0; /* Adjusted margin */
            font-size: 1.05em; /* Slightly larger step title */
            font-weight: bold;
            display: flex; /* Use flex for checkbox alignment */
            align-items: center;
        }
         .rfx-closer-step h3 input[type="checkbox"] {
            margin-right: 8px; /* Space between checkbox and title */
            margin-top: 0; /* Reset margin */
            vertical-align: middle; /* Align checkbox */
            width: 16px; /* Explicit size */
            height: 16px;
        }
        .rfx-closer-step-description {
            margin: 0;
            font-size: 0.95em; /* Slightly larger description text */
            color: #333; /* Darker text */
            line-height: 1.5; /* Improved line spacing */
        }
        .rfx-closer-step-description p { margin: 0 0 10px 0; } /* Spacing for paragraphs */
        .rfx-closer-step-description p:last-child { margin-bottom: 0; }
        .rfx-closer-step-description code {
            background-color: #f0f0f0;
            padding: 0.1em 0.4em;
            border-radius: 3px;
            font-size: 0.9em;
        }
        .rfx-closer-step-description a { color: #36c; text-decoration: none; }
        .rfx-closer-step-description a:hover { text-decoration: underline; }
        .rfx-closer-step-description ul, .rfx-closer-step-description ol { padding-left: 20px; margin: 10px 0; }
        .rfx-closer-step-description li { margin-bottom: 5px; }
        .rfx-closer-info-box {
            margin-top: 10px;
            padding: 8px 10px;
            border: 1px solid #ccc;
            border-radius: 3px;
            font-size: 0.9em;
            background-color: #f8f9fa;
        }
        /* Style for info box when data fetch fails */
        .rfx-closer-info-box.error {
            background-color: #f8d7da;
            border-color: #f5c6cb;
            color: #721c24;
        }
        /* Style for known issue warning */
        .rfx-closer-known-issue {
            margin-top: 8px;
            padding: 8px 10px;
            border: 1px solid #faebcc; /* Warning border */
            border-radius: 3px;
            font-size: 0.9em;
            background-color: #fcf8e3; /* Warning background */
            color: #8a6d3b; /* Warning text */
        }
        #rfx-outcome-selector {
            width: 100%;
            margin: 10px 0 5px 0; /* Adjusted margin */
            padding: 8px;
            border: 1px solid #a2a9b1;
            border-radius: 4px;
            background-color: #fff;
            font-size: 0.95em;
            cursor: pointer;
        }
        /* Copy Box Styles (kept for specific fallback cases) */
        .rfx-copy-box-container {
            position: relative;
            margin-top: 8px;
            margin-bottom: 12px;
        }
        .rfx-copy-box-display {
            background-color: #f0f0f0; /* Lighter grey */
            border: 1px solid #ccc; /* Lighter border */
            padding: 10px;
            padding-right: 60px; /* Space for button */
            font-family: monospace;
            font-size: 0.9em;
            margin: 0;
            border-radius: 3px;
            white-space: pre-wrap;
            word-break: break-all;
            overflow-x: auto;
            color: #333;
            max-height: 200px; /* Limit height for full wikitext */
            overflow-y: auto;   /* Add scroll for full wikitext */
        }
        .rfx-copy-box-button {
            position: absolute;
            top: 5px;
            right: 5px;
            background-color: #eaf3ff;
            border: 1px solid #a2a9b1;
            color: #333;
            padding: 3px 8px;
            border-radius: 3px;
            cursor: pointer;
            font-size: 0.8em;
            transition: background-color 0.2s ease;
            z-index: 1; /* Ensure button is above text */
        }
        .rfx-copy-box-button:hover { background-color: #cfe2ff; }
        .rfx-copy-box-button:disabled {
            cursor: default;
            opacity: 0.7;
        }
        .rfx-copy-box-button.copied { background-color: #d1e7dd !important; }
        .rfx-copy-box-button.error { background-color: #f8d7da !important; }
        .rfx-copy-box-helptext {
            font-size: 0.85em;
            color: #54595d;
            margin-top: 4px;
        }
        /* Launch Button */
        #rfx-closer-launch-li { /* Style the list item */
            border-left: 1px solid #a2a9b1;
            margin-left: 0.5em;
            padding-left: 0.5em;
        }
        #rfx-closer-launch {
            display: inline-block;
            padding: 0.3em 0.8em; /* Adjusted padding */
            background: #36c;
            color: white !important; /* Ensure white text */
            text-decoration: none !important;
            border-radius: 3px;
            font-size: 0.9em;
            cursor: pointer;
            transition: background-color 0.2s ease;
        }
        #rfx-closer-launch:hover {
            background: #447ff5; /* Lighter blue on hover */
            color: white !important;
        }
        .rfx-closer-percentage {
            margin-top: 10px;
            padding: 8px;
            background-color: #f8f9fa;
            border: 1px solid #a2a9b1;
            border-radius: 3px;
            font-size: 0.95em;
        }
        /* Add CSS for disabled checkbox styling */
        .rfx-closer-checkbox.oo-ui-widget-disabled {
            opacity: 1 !important;
        }
        .rfx-closer-checkbox.oo-ui-widget-disabled .oo-ui-checkboxInputWidget-checkIcon {
            opacity: 1 !important;
        }
        /* Styles for Notification Step (Step 10) */
        .rfx-notify-options label {
            display: block;
            margin-bottom: 5px;
        }
        .rfx-notify-options input[type="radio"] {
            margin-right: 5px;
        }
        .rfx-notify-textarea { /* Used in Step 10 */
            width: 100%;
            min-height: 100px;
            margin-top: 5px;
            margin-bottom: 5px;
            padding: 5px;
            border: 1px solid #a2a9b1;
            border-radius: 3px;
            font-family: monospace;
            box-sizing: border-box; /* Include padding and border in width */
        }
        /* Styles for Action Links */
        .rfx-action-links-container {
            margin-top: 10px;
            margin-bottom: 10px;
        }
        .rfx-action-link {
             display: inline-block;
             margin-right: 15px; /* Space between links */
             padding: 5px 0px; /* Minimal padding */
             text-decoration: none;
             font-size: 0.9em;
             cursor: pointer;
        }
        .rfx-action-link:hover {
             text-decoration: underline;
        }
        .rfx-action-link.copied {
             color: green;
             font-weight: bold;
        }
        .rfx-action-link.error {
             color: red;
             font-weight: bold;
        }
        .rfx-notify-editlink { /* Keep this for the manual fallback in notify step */
             display: inline-block;
             margin-top: 10px;
             margin-left: 10px; /* Space between buttons */
             padding: 5px 10px;
             background-color: #f8f9fa;
             border: 1px solid #a2a9b1;
             border-radius: 3px;
             text-decoration: none;
             font-size: 0.9em;
        }
        .rfx-notify-editlink:hover {
             background-color: #eaecf0;
        }
        .rfx-notify-status { /* Style for API status message in Step 10 */
             font-size: 0.9em;
             margin-top: 8px;
        }
        /* Styles for Crat Chat Step (Step 5) */
        .rfx-crat-chat-textarea {
             width: 100%;
             min-height: 80px;
             margin-top: 5px;
             margin-bottom: 5px;
             padding: 5px;
             border: 1px solid #a2a9b1;
             border-radius: 3px;
             font-family: sans-serif; /* Use standard font */
             box-sizing: border-box;
        }
        /* Styles for NEW Steps 6 & 7 */
        .rfx-onhold-notify-textarea {
             width: 100%;
             min-height: 80px; /* Slightly smaller */
             margin-top: 5px;
             margin-bottom: 5px;
             padding: 5px;
             border: 1px solid #a2a9b1;
             border-radius: 3px;
             font-family: sans-serif; /* Use standard font */
             box-sizing: border-box;
        }
        .rfx-crat-notify-status, .rfx-candidate-onhold-notify-status {
             font-size: 0.9em;
             margin-top: 8px;
             max-height: 60px; /* Limit height for multi-line status */
             overflow-y: auto; /* Allow scrolling */
        }
        .rfx-crat-notify-status ul {
             margin: 0;
             padding-left: 15px;
             font-size: 0.9em;
        }
        .rfx-crat-notify-status li {
             margin-bottom: 2px;
        }
        .rfx-crat-notify-status .success { color: green; }
        .rfx-crat-notify-status .error { color: red; }
        .rfx-crat-notify-status .warning { color: orange; }

    `;
    const styleSheet = document.createElement("style");
    styleSheet.textContent = styles;
    document.head.appendChild(styleSheet);


    // --- Assemble UI ---
    document.body.appendChild(container);
    container.appendChild(header);
    contentAndInputContainer.appendChild(inputSection); // Input section added here
    contentAndInputContainer.appendChild(contentContainer);
    container.appendChild(contentAndInputContainer);
    contentContainer.appendChild(stepsContainer);

    // --- Data Fetching ---

    // Fetches summary data from the main RfX list page
    const fetchRfaData = () => {
        // Return cached data if available and no error occurred previously
        if (rfaData && !fetchErrorOccurred) return Promise.resolve(rfaData);

        const api = new mw.Api();
        console.log(`RfX Closer: Fetching summary data for RfX: ${candidateSubpage} from ${baseRfxPage}`);
        fetchErrorOccurred = false; // Reset error flag on new fetch attempt

        return api.get({
            action: 'parse',
            page: baseRfxPage, // Fetch the main list page
            prop: 'text',
            formatversion: 2,
            format: 'json'
        }).then(data => {
            const htmlContent = data.parse.text;
            const tempDiv = document.createElement('div');
            tempDiv.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');
                    // Normalize href for comparison (replace spaces with underscores)
                    const linkHref = rfaLink ? rfaLink.getAttribute('href').replace(/ /g, '_') : '';
                    const targetHref = `/wiki/${baseRfxPage.replace(/ /g, '_')}/${candidateSubpage}`;

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

            if (foundData) {
                rfaData = foundData;
                actualCandidateUsername = rfaData.candidate; // Update username from fetched data
            } else {
                console.warn(`RfX Closer: Could not find summary data for ${candidateSubpage}. 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;
        }).catch(error => {
            console.error('RfX Closer: Error fetching summary data:', error);
            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
    async function fetchPageWikitext(targetPageName) {
        // Normalize page name (use underscores for API)
        const normalizedPageName = targetPageName.replace(/ /g, '_');

        // Return cached wikitext if available
        if (basePageWikitextCache[normalizedPageName]) {
            console.log(`RfX Closer: Using cached wikitext for ${normalizedPageName}.`);
            return basePageWikitextCache[normalizedPageName];
        }

        const api = new mw.Api();
        console.log(`RfX Closer: Fetching wikitext for page: ${normalizedPageName}`);
        try {
            const data = await api.get({
                action: 'query',
                prop: 'revisions',
                titles: normalizedPageName, // Use normalized name
                rvslots: 'main',
                rvprop: 'content',
                formatversion: 2,
                format: 'json'
            });
            const page = data.query.pages[0];
            if (page && !page.missing && page.revisions && page.revisions[0].slots.main.content) {
                const wikitext = page.revisions[0].slots.main.content;
                basePageWikitextCache[normalizedPageName] = wikitext; // Cache the result
                console.log(`RfX Closer: Successfully fetched wikitext for ${normalizedPageName}.`);
                return wikitext;
            } else {
                // If page is missing (e.g., yearly chrono page doesn't exist yet), return empty string
                if (page && page.missing) {
                    console.log(`RfX Closer: Page ${normalizedPageName} does not exist. Returning empty wikitext.`);
                    basePageWikitextCache[normalizedPageName] = ''; // Cache empty result
                    return '';
                }
                 console.error(`RfX Closer: Could not find wikitext for page ${normalizedPageName}. Response:`, data);
                return null; // Indicate other error
            }
        } catch (error) {
             console.error(`RfX Closer: Error fetching wikitext for ${normalizedPageName}:`, error);
             throw error; // Re-throw for the caller to handle
        }
    }

    // Specific function for the current RfX page wikitext (uses generic fetcher)
    const fetchRfXWikitext = () => {
        // Use the generic fetcher, relies on its internal caching
        return fetchPageWikitext(pageName).catch(err => {
            // Set specific flag for RfX page fetch error
            wikitextErrorOccurred = true;
            return null;
        });
    };

    // Fetches the list of bureaucrats
    async function fetchBureaucrats() {
        if (bureaucratListCache) {
            console.log("RfX Closer: Using cached bureaucrat list.");
            return bureaucratListCache;
        }
        const api = new mw.Api();
        console.log("RfX Closer: Fetching bureaucrat list...");
        try {
            const data = await api.get({
                action: 'query',
                list: 'groupmembers',
                gmgroup: 'bureaucrat',
                gmlimit: 'max',
                format: 'json',
                formatversion: 2
            });

            // More detailed check and logging
            if (data && data.query && Array.isArray(data.query.groupmembers)) { // Check if it's an array
                if (data.query.groupmembers.length > 0) {
                    bureaucratListCache = data.query.groupmembers.map(member => member.name);
                    console.log(`RfX Closer: Found ${bureaucratListCache.length} bureaucrats.`);
                    return bureaucratListCache;
                } else {
                    // Array exists but is empty
                    console.warn("RfX Closer: API returned an empty list for the 'bureaucrat' group. Response:", data);
                    bureaucratListCache = []; // Cache the empty result
                    return [];
                }
            } else {
                // Query or groupmembers property is missing or not an array
                console.error("RfX Closer: Could not parse bureaucrat list from API response (query or groupmembers missing/invalid). Full response:", JSON.stringify(data)); // Log full response string
                return [];
            }
        } catch (error) {
            console.error("RfX Closer: Error fetching bureaucrat list:", error);
            return []; // Return empty list on error
        }
    }


    // --- Function to update the percentage display ---
    const updatePercentageDisplay = () => {
        const supportInput = document.getElementById('support-count');
        const opposeInput = document.getElementById('oppose-count');
        const percentageDiv = document.querySelector('.rfx-closer-percentage'); // Find the display div

        if (!supportInput || !opposeInput || !percentageDiv) {
            console.warn('RfX Closer: Could not find input fields or percentage display for dynamic update.');
            return;
        }

        const support = parseInt(supportInput.value, 10) || 0; // Default to 0 if NaN
        const oppose = parseInt(opposeInput.value, 10) || 0; // Default to 0 if NaN
        const total = support + oppose;
        const percentage = total > 0 ? (support / total * 100).toFixed(2) : 0;

        percentageDiv.textContent = `Support percentage: ${percentage}% (${support}/${total})`;
    };

    const updateInputFields = () => {
        // Use the promise directly to avoid race conditions if called before initial fetch completes
        fetchRfaData().then(data => {
            if (data) {
                document.getElementById('support-count').value = data.support || '';
                document.getElementById('oppose-count').value = data.oppose || '';
                document.getElementById('neutral-count').value = data.neutral || '';

                // Calculate and update percentage using the new function
                updatePercentageDisplay(); // Call the dedicated function

            } else {
                document.getElementById('support-count').value = '';
                document.getElementById('oppose-count').value = '';
                document.getElementById('neutral-count').value = '';
                updatePercentageDisplay(); // Update percentage even if fetch fails (will show 0%)
            }
        });
    };

    // --- Helper Functions ---

    // Helper to escape characters for use in regex
    function escapeRegex(string) {
        // Escape basic regex characters. Handles most common cases in usernames/pagenames.
        // Also replace spaces with underscores OR spaces to match either in regex
        const spacedString = string.replace(/_/g, ' ');
        const underscoredString = string.replace(/ /g, '_');
        // If they are the same, just escape one
        if (spacedString === underscoredString) {
            return spacedString.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
        }
        // If different, create a pattern like (Underscore_Version|Space Version)
        const escapedSpaced = spacedString.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
        const escapedUnderscored = underscoredString.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
        return `(?:${escapedUnderscored}|${escapedSpaced})`;
    }


    // API function to check user groups
    async function getUserGroups(username) {
        const api = new mw.Api();
        console.log(`getUserGroups: Fetching groups for ${username}`);
        try {
            const data = await api.get({
                action: 'query', list: 'users', ususers: username, usprop: 'groups',
                format: 'json', formatversion: 2
            });
            if (data.query?.users?.[0]) {
                const user = data.query.users[0];
                if (user.missing || user.invalid) {
                    console.warn(`getUserGroups: User '${username}' not found or invalid.`);
                    return null; // Indicate user not found
                }
                const groups = user.groups || [];
                console.log(`getUserGroups: Found groups for ${username}:`, groups);
                return groups;
            } else {
                console.warn(`getUserGroups: Could not find user data for '${username}'.`, data);
                return null; // Indicate API error or unexpected response
            }
        } catch (error) {
            console.error(`getUserGroups: API error checking groups for '${username}'.`, error);
            throw error; // Rethrow so the caller knows it failed
        }
    }


    // API function to grant/remove rights
    function grantPermissionAPI(username, groupToAdd, reason, groupsToRemove = null, expiry = 'infinity') {
        const api = new mw.Api();
        const params = {
            action: 'userrights',
            format: 'json',
            user: username,
            reason: reason,
            expiry: expiry
        };
        // Add 'add' parameter only if groupToAdd is provided
        if (groupToAdd) {
            params.add = groupToAdd;
        }
        // Add the 'remove' parameter only if groupsToRemove is provided and not empty
        if (groupsToRemove) {
            params.remove = groupsToRemove; // Expects pipe-separated string
        }
        // Use postWithToken to handle edit tokens automatically
        return api.postWithToken('userrights', params); // Returns a jQuery promise
    }

    // Helper function to post a message to a talk page
    async function postToTalkPage(targetPage, sectionTitle, messageContent, summary) {
        const api = new mw.Api();
        try {
            await api.postWithToken('edit', {
                action: 'edit',
                title: targetPage,
                section: 'new',
                sectiontitle: sectionTitle,
                text: messageContent,
                summary: summary,
                format: 'json'
            });
            return { success: true, page: targetPage };
        } catch (error) {
            console.error(`RfX Closer: Error posting to ${targetPage}:`, error);
            const errorCode = error?.error?.code || 'unknown';
            const errorInfo = error?.error?.info || 'Unknown error';
            return { success: false, page: targetPage, error: `${errorInfo} (${errorCode})` };
        }
    }

    // Helper to add a delay
    function sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }


    // --- Helper Function to create Copy Code and Edit Page links ---
    /**
     * Creates a container with "Copy Code" and "Edit Page" links.
     * @param {string} targetPage - The full page name (e.g., "Wikipedia:Some_Page"). Can be null if only copy is needed.
     * @param {string|Function} wikitextOrGetter - The wikitext string or a function that returns it.
     * @param {string} linkTextPrefix - Text to prepend to link labels (e.g., "Full RfX Page").
     * @param {boolean} isCreateLink - If true, the edit link text says "Create Page". Defaults to false.
     * @returns {HTMLElement} A div element containing the links.
     */
    function createActionLinks(targetPage, wikitextOrGetter, linkTextPrefix = '', isCreateLink = false) {
        const linksContainer = document.createElement('div');
        linksContainer.className = 'rfx-action-links-container';

        // --- Copy Link ---
        const copyLink = document.createElement('a');
        copyLink.href = '#';
        copyLink.textContent = `Copy Code${linkTextPrefix ? ' for ' + linkTextPrefix : ''}`;
        copyLink.className = 'rfx-action-link';
        copyLink.title = 'Copy generated wikitext to clipboard';

        copyLink.addEventListener('click', (e) => {
            e.preventDefault();
            // Get the current wikitext, either directly or via the getter function
            const textToCopy = (typeof wikitextOrGetter === 'function') ? wikitextOrGetter() : wikitextOrGetter;

            if (textToCopy === null || textToCopy === undefined) {
                console.warn("RfX Closer: Attempted to copy null or 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'; // Make it stand out
                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);

        // --- Edit/Create Link ---
        // Only add edit link if targetPage is provided
        if (targetPage) {
            const editLink = document.createElement('a');
            // Use mw.util.getUrl to handle encoding and base path correctly
            editLink.href = mw.util.getUrl(targetPage, { action: 'edit' });
            editLink.target = '_blank'; // Open in new tab
            const linkVerb = isCreateLink ? 'Create' : 'Edit';
            editLink.textContent = `${linkVerb} ${linkTextPrefix || targetPage.replace(/_/g, ' ')}`;
            editLink.className = 'rfx-action-link';
            editLink.title = `${linkVerb} ${targetPage.replace(/_/g, ' ')} in edit mode`;
            linksContainer.appendChild(editLink);
        }

        return linksContainer;
    }

    // Kept for specific fallback cases if needed (e.g., simple template copy)
    function createCopyableBox(content, helpText = 'Click button to copy', isLarge = false, inputElement = null) {
         const boxContainer = document.createElement('div');
         boxContainer.className = 'rfx-copy-box-container';
         const displayElement = inputElement ? inputElement : document.createElement('pre');
         if (!inputElement) {
             displayElement.textContent = content;
             displayElement.className = 'rfx-copy-box-display';
             if (isLarge) { displayElement.style.maxHeight = '300px'; }
             boxContainer.appendChild(displayElement);
         }
         const copyButton = document.createElement('button');
         copyButton.textContent = 'Copy';
         copyButton.title = helpText;
         copyButton.className = 'rfx-copy-box-button';
         if (inputElement && inputElement.tagName === 'TEXTAREA') { copyButton.style.top = 'auto'; copyButton.style.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);
         const helpTextElement = document.createElement('div');
         helpTextElement.textContent = helpText;
         helpTextElement.className = 'rfx-copy-box-helptext';
         boxContainer.appendChild(helpTextElement);
         return boxContainer;
     }


    // Helper to get current vote counts from input fields
    function getCurrentVoteCounts() {
        return {
            support: document.getElementById('support-count').value || '0',
            oppose: document.getElementById('oppose-count').value || '0',
            neutral: document.getElementById('neutral-count').value || '0'
        };
    }

    // Map internal group names to human-readable display names
    const 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', // Corrected label
        'abusefilter': 'Edit filter manager', // Added entry
        'reviewer': 'Pending changes reviewer',
        'accountcreator': 'Account creator',
        'autoreviewer': 'Autopatrolled'
        // Add other common groups here if needed for display
    };

    // Helper function to update count line (e.g., '''X successful candidacies so far''')
    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;
                // Preserve original formatting (bolding) and the text part
                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 Definitions ---

    const steps = [
        { // 0: Check Timing
            title: 'Check Timing',
            description: () => { /* ... unchanged ... */
                const stepContainer = document.createElement('div');
                const text = document.createElement('p');
                text.innerHTML = `Verify that at least 7 days have passed since the listing on <a href="/wiki/${baseRfxPage}" target="_blank">${displayBaseRfxPage}</a>.`;
                stepContainer.appendChild(text);
                const timingInfoContainer = document.createElement('div');
                timingInfoContainer.className = 'rfx-closer-info-box';
                timingInfoContainer.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;
            },
            completed: false
        },
        { // 1: Verify History
            title: 'Verify History',
            description: () => { /* ... unchanged ... */
                const container = document.createElement('div');
                const text = document.createElement('p');
                text.textContent = 'Check the history of the transcluded page to ensure comments are genuine and haven\'t been tampered with.';
                container.appendChild(text);
                const historyUrl = `/w/index.php?title=${encodeURIComponent(pageName)}&action=history`;
                const historyLink = document.createElement('a');
                historyLink.href = historyUrl;
                historyLink.target = '_blank';
                historyLink.textContent = 'View page history';
                historyLink.style.cssText = `display: inline-block; margin-top: 5px; padding: 5px 10px; background-color: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 3px; text-decoration: none;`;
                container.appendChild(historyLink);
                return container;
            },
            completed: false
        },
        { // 2: Determine Consensus
            title: 'Determine Consensus',
            description: () => { /* ... unchanged ... */
                const stepContainer = document.createElement('div');
                const staticDesc = document.createElement('p');
                staticDesc.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';
                stepContainer.appendChild(staticDesc);
                const voteTallyContainer = document.createElement('div');
                voteTallyContainer.className = 'rfx-closer-info-box';
                voteTallyContainer.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;
            },
            completed: false
        },
        { // 3: Select Outcome
            title: 'Select Outcome',
            description: 'Based on the consensus determination, select the appropriate outcome:',
            completed: false,
            isSelectionStep: true // Flag this step
        },
        // Step 4: Prepare Full Wikitext for Closing / Hold
        {
            title: 'Prepare RfX Page Wikitext', // Renamed slightly
            description: (selectedOutcome, votes) => {
                const container = document.createElement('div');
                const loadingMsg = document.createElement('p');
                loadingMsg.textContent = 'Loading and processing wikitext...';
                container.appendChild(loadingMsg);

                let reason = ''; // For finaltally template
                let topTemplateName = ''; // For top template (rfap/rfaf/rfah)
                let 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; // Set flag and reason for hold
                    default: return 'This step is not applicable for the selected outcome.';
                }

                const topTemplateCode = `{{subst:${topTemplateName}}}`;

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

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

                    // --- Logic for finaltally, chat link, rfab ---
                    const finaltallyTemplate = `'''Final <span id="rfatally">(?/?/?)</span>; ended by [[User:Barkeep49|Barkeep49]] ([[User_talk:Barkeep49|talk]]) at 22:25, 15 April 2025 (UTC)''' <!-- Template:finaltally -->}`;
                    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]].`;

                    // Attempt to replace content between header and next section
                    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;
                    let headerEndIndex = -1;

                    if (headerMatch) {
                        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);
                            // Construct replacement content based on outcome
                            const replacementContent = isHoldOutcome
                                ? `\n${chatLink}\n${finaltallyTemplate}\n` // For hold: chat link + finaltally
                                : `\n${finaltallyTemplate}\n`; // For closing: just finaltally
                            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) {
                         // Add footer only if it's not 'onhold'
                        if (!isHoldOutcome) {
                            modifiedWikitext = modifiedWikitext.replace(/\{\{(subst:)?rfab\}\}\s*$/i, '');
                            modifiedWikitext = modifiedWikitext.trim() + "\n" + footerTemplate;
                        }
                        const intro = document.createElement('p');
                         if (isHoldOutcome) {
                             intro.innerHTML = `Ensure <code>${topTemplateCode}</code> is at the top, and the 'Bureaucrat chat' link and final tally are placed correctly below the header.`;
                         } else {
                             intro.innerHTML = `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(intro);
                        container.appendChild(createActionLinks(pageName, modifiedWikitext, 'Full RfX Page'));
                    } else {
                        // Content replacement failed
                        const warning = document.createElement('p');
                        warning.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.';
                        warning.style.color = 'orange';
                        container.appendChild(warning);
                        const instructions = document.createElement('p');
                        let manualContent = isHoldOutcome ? `${chatLink}\n${finaltallyTemplate}` : finaltallyTemplate;
                        instructions.innerHTML = `Please manually replace the content between the candidate header and the next section with:`;
                        container.appendChild(instructions);
                        container.appendChild(createCopyableBox(manualContent, 'Copy content to insert manually'));
                        if (!isHoldOutcome) {
                            const footerInstruction = document.createElement('p');
                            footerInstruction.innerHTML = `Also ensure <code>${footerTemplate}</code> is at the very bottom.`;
                            container.appendChild(footerInstruction);
                        }
                        container.appendChild(createActionLinks(pageName, modifiedWikitext, 'RfX Page (Manual Edit Needed)'));
                    }

                }).catch(err => {
                    loadingMsg.remove();
                    const errorMsg = document.createElement('p');
                    errorMsg.innerHTML = '<strong>Error:</strong> Error processing wikitext. Check console.';
                    errorMsg.style.color = 'red';
                    container.appendChild(errorMsg);
                    console.error("RfX Closer: Error in wikitext processing chain:", err);
                });
                return container;
            },
            completed: false,
            showFor: ['successful', 'unsuccessful', 'withdrawn', 'notnow', 'snow', 'onhold'] // Show for all outcomes
        },
        // Step 5: Start Bureaucrat Chat (Only for 'onhold')
        {
            title: 'Start Bureaucrat Chat',
            description: (selectedOutcome, votes) => {
                const container = document.createElement('div');
                const chatPageName = `${pageName}/Bureaucrat chat`; // Use full page name
                const chatPageDisplay = `${candidateSubpage}/Bureaucrat chat`; // Shorter display name

                const intro = document.createElement('p');
                intro.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(intro);

                const commentLabel = document.createElement('label');
                commentLabel.textContent = 'Optional initial comment (will be added under == Discussion ==):';
                commentLabel.style.display = 'block';
                commentLabel.style.marginBottom = '4px';
                container.appendChild(commentLabel);

                const commentTextarea = document.createElement('textarea');
                commentTextarea.className = 'rfx-crat-chat-textarea'; // Add specific class if needed
                commentTextarea.placeholder = `e.g., Initiating discussion per RfX hold... [[User:Barkeep49|Barkeep49]] ([[User talk:Barkeep49|talk]]) 22:19, 15 April 2025 (UTC)`; // Changed placeholder
                container.appendChild(commentTextarea);

                // Function to generate the chat page wikitext
                const getChatPageWikitext = () => {
                    const initialComment = commentTextarea.value.trim();
                    const discussionContent = initialComment ? initialComment : '';
                    return `{{Bureaucrat discussion header}}

== Discussion ==
${discussionContent}

== Recusals ==

== Summary ==`;
                };

                // Add Copy/Create links
                container.appendChild(createActionLinks(chatPageName, getChatPageWikitext, 'Bureaucrat Chat Page', true)); // isCreateLink = true

                return container;
            },
            completed: false,
            showFor: ['onhold'] // Only show for 'onhold'
        },
        // NEW Step 6: Notify Bureaucrats (Only for 'onhold')
        {
            title: 'Notify Bureaucrats',
            description: async (selectedOutcome) => {
                const container = document.createElement('div');
                const chatPageName = `${pageName}/Bureaucrat chat`;
                const chatPageLink = `[[${chatPageName}|bureaucrat chat]]`; // Wikilink for message

                const intro = document.createElement('p');
                intro.innerHTML = `Notify bureaucrats about the new chat page. Edit the message below if needed, then use the API button.`;
                container.appendChild(intro);

                const messageTextarea = document.createElement('textarea');
                messageTextarea.id = 'rfx-crat-notify-message';
                messageTextarea.className = 'rfx-onhold-notify-textarea';
                messageTextarea.value = `== Bureaucrat Chat ==\nYour input is requested at the freshly-created ${chatPageLink} for [[${pageName}]]. [[User:Barkeep49|Barkeep49]] ([[User talk:Barkeep49|talk]]) 22:19, 15 April 2025 (UTC)`;
                container.appendChild(messageTextarea);

                const buttonContainer = document.createElement('div');
                const postButton = document.createElement('button');
                postButton.id = 'rfx-crat-notify-button';
                postButton.textContent = 'Notify Bureaucrats via API';
                postButton.className = 'rfx-closer-action-button';
                postButton.disabled = true; // Disabled until crat list is loaded
                buttonContainer.appendChild(postButton);

                const statusArea = document.createElement('div');
                statusArea.id = 'rfx-crat-notify-status';
                statusArea.className = 'rfx-crat-notify-status';
                statusArea.textContent = 'Loading bureaucrat list...';
                buttonContainer.appendChild(statusArea); // Status inside button container

                container.appendChild(buttonContainer);

                // Fetch crat list and enable button
                let bureaucrats = [];
                try {
                    bureaucrats = await fetchBureaucrats();
                    if (bureaucrats.length > 0) {
                        statusArea.textContent = `Ready to notify ${bureaucrats.length} bureaucrats.`;
                        postButton.disabled = false;
                    } else {
                        // Handle case where fetchBureaucrats returned empty list (either group empty or error occurred)
                        const logCheck = console.history ? console.history.find(log => log.includes("API returned an empty list") || log.includes("Could not parse bureaucrat list")) : null; // Check console history if available
                        if (logCheck && logCheck.includes("API returned an empty list")) {
                            statusArea.textContent = 'Warning: The \'bureaucrat\' group appears to be empty on this wiki.';
                            statusArea.style.color = 'orange';
                        } else {
                            statusArea.textContent = 'Error: Could not load bureaucrat list (or list is empty). Check console for details.';
                            statusArea.style.color = 'red';
                        }
                    }
                } catch (error) { // Catch should already be handled in fetchBureaucrats, but double-check
                    statusArea.textContent = 'Error loading bureaucrat list.';
                    statusArea.style.color = 'red';
                }

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

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

                    if (bureaucrats.length === 0) {
                        statusArea.textContent = 'No bureaucrats found to notify.';
                        statusArea.style.color = 'orange';
                        return; // Don't proceed if list is empty
                    }

                    for (const cratUsername of bureaucrats) {
                        // Don't notify the closer themselves
                        if (cratUsername === closerUsername) {
                            console.log(`RfX Closer: Skipping notification for closer (${closerUsername})`);
                            continue;
                        }

                        const talkPage = `User talk:${cratUsername}`;
                        const progressItem = document.createElement('li');
                        progressItem.textContent = `Notifying ${cratUsername}...`;
                        progressList.appendChild(progressItem);
                        statusArea.scrollTop = statusArea.scrollHeight; // Scroll to bottom

                        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); // Small delay between edits
                    }

                    const finalStatus = document.createElement('p');
                    finalStatus.style.fontWeight = 'bold';
                    finalStatus.textContent = `Finished: ${successCount} successful, ${failCount} failed.`;
                    if (failCount > 0) {
                        finalStatus.style.color = 'orange';
                    } else {
                        finalStatus.style.color = 'green';
                        // Mark step complete only if all succeed? Or if any succeed? Let's go with any.
                        const stepElement = postButton.closest('.rfx-closer-step');
                        const stepIndex = parseInt(stepElement.dataset.step, 10);
                        if (!isNaN(stepIndex)) {
                            steps[stepIndex].completed = true;
                            stepElement.dataset.completed = true;
                            const checkbox = stepElement.querySelector('input[type="checkbox"]');
                            if(checkbox) checkbox.checked = true;
                        }
                        postButton.textContent = 'Notifications Sent'; // Keep disabled
                    }
                     statusArea.appendChild(finalStatus);
                     statusArea.scrollTop = statusArea.scrollHeight;
                     if (failCount > 0) postButton.disabled = false; // Re-enable if some failed

                });

                return container;
            },
            completed: false,
            showFor: ['onhold'] // Only show for 'onhold'
        },
        // NEW Step 7: Notify Candidate (Only for 'onhold')
        {
            title: 'Notify Candidate (On Hold)',
            description: (selectedOutcome) => {
                const container = document.createElement('div');
                const candidateTalkPage = `User talk:${actualCandidateUsername}`;
                const chatPageName = `${pageName}/Bureaucrat chat`;
                const chatPageLink = `[[${chatPageName}|bureaucrat chat]]`;

                const intro = document.createElement('p');
                intro.innerHTML = `Notify the candidate (${actualCandidateUsername}) about the 'on hold' status.`;
                container.appendChild(intro);

                const messageTextarea = document.createElement('textarea');
                messageTextarea.id = 'rfx-candidate-onhold-notify-message';
                messageTextarea.className = 'rfx-onhold-notify-textarea';
                messageTextarea.value = `== Your ${rfxType === 'adminship' ? 'RfA' : 'RfB'} ==\nHi ${actualCandidateUsername}, just letting you know that your ${rfxType} request has been placed on hold pending discussion amongst the bureaucrats. You can follow the discussion at the ${chatPageLink}. [[User:Barkeep49|Barkeep49]] ([[User talk:Barkeep49|talk]]) 22:19, 15 April 2025 (UTC)`;
                container.appendChild(messageTextarea);

                const buttonContainer = document.createElement('div');
                const postButton = document.createElement('button');
                postButton.id = 'rfx-candidate-onhold-notify-button';
                postButton.textContent = 'Post Notification via API';
                postButton.className = 'rfx-closer-action-button';
                buttonContainer.appendChild(postButton);

                // Add Manual Edit Link as Fallback
                const manualEditLink = document.createElement('a');
                manualEditLink.href = mw.util.getUrl(candidateTalkPage, { action: 'edit', section: 'new', sectiontitle: `Your ${rfxType === 'adminship' ? 'RfA' : 'RfB'}` });
                manualEditLink.target = '_blank';
                manualEditLink.className = 'rfx-notify-editlink'; // Reuse existing style
                manualEditLink.textContent = `Post Manually...`;
                buttonContainer.appendChild(manualEditLink);

                const statusArea = document.createElement('div');
                statusArea.id = 'rfx-candidate-onhold-notify-status';
                statusArea.className = 'rfx-candidate-onhold-notify-status'; // Specific class
                buttonContainer.appendChild(statusArea);

                container.appendChild(buttonContainer);

                postButton.addEventListener('click', async () => {
                    postButton.disabled = true;
                    statusArea.textContent = 'Posting message...';
                    statusArea.style.color = 'inherit';

                    const messageContent = messageTextarea.value.trim();
                    const editSummary = `Notifying candidate about RfX hold${tagLine}`;
                    const sectionTitle = `Your ${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';
                        // Mark step complete
                        const stepElement = postButton.closest('.rfx-closer-step');
                        const stepIndex = parseInt(stepElement.dataset.step, 10);
                        if (!isNaN(stepIndex)) {
                            steps[stepIndex].completed = true;
                            stepElement.dataset.completed = true;
                            const checkbox = stepElement.querySelector('input[type="checkbox"]');
                            if(checkbox) checkbox.checked = true;
                        }
                        postButton.textContent = 'Posted'; // Keep disabled
                    } else {
                        statusArea.textContent = `Error posting message: ${result.error}. Please post manually.`;
                        statusArea.style.color = 'red';
                        postButton.disabled = false; // Re-enable on error
                    }
                });

                return container;
            },
            completed: false,
            showFor: ['onhold'] // Only show for 'onhold'
        },
        // Renumbered Step 8: Process Promotion
        {
            title: 'Process Promotion',
            description: async (selectedOutcome) => { /* ... unchanged ... */
                const container = document.createElement('div');
                const text = document.createElement('p');
                text.textContent = 'For successful RfXs, configure rights changes and grant via API:';
                container.appendChild(text);
                const loadingStatus = document.createElement('p');
                loadingStatus.textContent = 'Loading current user groups...';
                loadingStatus.style.fontStyle = 'italic';
                container.appendChild(loadingStatus);
                const rightsContainer = document.createElement('div');
                container.appendChild(rightsContainer);
                const grantButton = document.createElement('button');
                grantButton.textContent = `Grant Rights via API`;
                grantButton.className = 'rfx-closer-action-button';
                grantButton.disabled = true;
                const statusArea = document.createElement('div');
                statusArea.className = 'rfx-closer-api-status';
                statusArea.style.marginTop = '5px';
                let removeCheckboxes = {};
                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 = rfxType === 'adminship' ? 'sysop' : 'bureaucrat';
                        const groupToAddLabel = rfxType === 'adminship' ? 'Administrator' : 'Bureaucrat';
                        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 (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 = {};
                            let hasRemovable = false;
                            currentGroups.forEach(groupName => {
                                if (!groupsToExclude.includes(groupName)) {
                                    hasRemovable = true;
                                    const checkbox = new OO.ui.CheckboxInputWidget({ selected: true });
                                    removeCheckboxes[groupName] = checkbox;
                                    const displayLabel = 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 {
                                const noRemoveMsg = document.createElement('p');
                                noRemoveMsg.textContent = 'User holds no other explicit groups to potentially remove.';
                                noRemoveMsg.style.fontSize = '0.9em'; noRemoveMsg.style.fontStyle = 'italic';
                                rightsContainer.appendChild(noRemoveMsg);
                            }
                        } else {
                            const noRemoveMsg = document.createElement('p');
                            noRemoveMsg.textContent = 'No groups removed when granting bureaucrat rights.';
                            noRemoveMsg.style.fontSize = '0.9em'; noRemoveMsg.style.fontStyle = 'italic';
                            rightsContainer.appendChild(noRemoveMsg);
                        }
                        const userGroups = mw.config.get('wgUserGroups');
                        const canGrant = userGroups && userGroups.includes('bureaucrat');
                        if (canGrant) { grantButton.disabled = false; }
                        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';
                }
                grantButton.addEventListener('click', async () => {
                    grantButton.disabled = true; statusArea.textContent = 'Processing...'; statusArea.style.color = 'inherit';
                    const groupToAdd = rfxType === 'adminship' ? 'sysop' : 'bureaucrat';
                    const promotionReason = `Per [[${pageName}|successful RfX]]${tagLine}`;
                    const groupsToRemoveList = [];
                    if (Object.keys(removeCheckboxes).length > 0) {
                        for (const groupName in removeCheckboxes) { if (removeCheckboxes[groupName].isSelected()) { groupsToRemoveList.push(groupName); } }
                    }
                    const groupsToRemoveString = groupsToRemoveList.length > 0 ? groupsToRemoveList.join('|') : null;
                    console.log("RfX Closer: Attempting grant.", { add: groupToAdd, remove: groupsToRemoveString });
                    try {
                        const alreadyHasGroup = await getUserGroups(actualCandidateUsername).then(g => g && g.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);
                        let finalMessage = ""; let finalColor = "orange"; let 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 && data.userrights.removed ? data.userrights.removed.map(g => g.group) : [];
                        if (groupsToRemoveString) {
                            if (actuallyRemoved.length > 0) { finalMessage += `Removed: ${actuallyRemoved.join(', ')}.`; }
                            else { finalMessage += ` (No selected groups were removed).`; }
                        }
                        statusArea.textContent = finalMessage; statusArea.style.color = finalColor;
                        if (stepCompleted) {
                            const stepElement = grantButton.closest('.rfx-closer-step');
                            const stepIndex = parseInt(stepElement.dataset.step, 10);
                            if (!isNaN(stepIndex)) {
                                steps[stepIndex].completed = true; stepElement.dataset.completed = true;
                                const checkbox = stepElement.querySelector('input[type="checkbox"]'); if(checkbox) checkbox.checked = true;
                            }
                        }
                    } catch (error) {
                        console.error('Userrights API Error:', error);
                        const errorInfo = error?.error?.info || 'Unknown error';
                        statusArea.textContent = `Error: ${errorInfo}. Please grant manually.`; statusArea.style.color = 'red';
                    } finally { grantButton.disabled = false; }
                });
                container.appendChild(grantButton); container.appendChild(statusArea);
                const list = document.createElement('ul'); list.style.paddingLeft = '20px'; list.style.marginTop = '10px';
                const refItem = document.createElement('li'); refItem.innerHTML = `Manual link: <a href="/wiki/Special:Userrights/${encodeURIComponent(actualCandidateUsername)}" target="_blank">Special:Userrights/${actualCandidateUsername}</a>`; list.appendChild(refItem);
                const rightsItem = document.createElement('li'); rightsItem.innerHTML = `Reference: <a href="/wiki/Special:ListGroupRights" target="_blank">Special:ListGroupRights</a>`; list.appendChild(rightsItem);
                container.appendChild(list);
                return container;
            },
            completed: false,
            showFor: ['successful'] // Only for successful
        },
        // Renumbered Step 9: Update Lists
        {
            title: 'Update Lists',
            description: async (selectedOutcome, votes) => {
                const container = document.createElement('div');

                // --- 1. Remove from main RfX list page ---
                const removeDiv = document.createElement('div'); removeDiv.style.marginBottom = '15px';
                const removeLinkText = displayBaseRfxPage;
                const removeInstruction = document.createElement('p');
                removeInstruction.innerHTML = `1. Update ${removeLinkText} (removing the RfX transclusion):`;
                removeDiv.appendChild(removeInstruction);
                const removeLoadingPara = document.createElement('p'); removeLoadingPara.textContent = `  Loading wikitext for ${removeLinkText}...`; removeLoadingPara.style.fontStyle = 'italic'; removeDiv.appendChild(removeLoadingPara); container.appendChild(removeDiv);
                try {
                    const basePageWikitext = await fetchPageWikitext(baseRfxPage); removeLoadingPara.remove();
                    if (basePageWikitext !== null) {
                        const escapedPageNameForRegex = 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(baseRfxPage, modifiedBasePageWikitext, removeLinkText));
                        } else {
                            const warningPara = document.createElement('p'); warningPara.innerHTML = `  Warning: Could not find <code>{{${pageName}}}</code> to remove from ${removeLinkText}. Remove manually.`; warningPara.style.color = 'orange'; removeDiv.appendChild(warningPara);
                            removeDiv.appendChild(createActionLinks(baseRfxPage, basePageWikitext, removeLinkText));
                        }
                    } else { throw new Error(`Failed to fetch wikitext for ${baseRfxPage}`); }
                } catch (error) { console.error("RfX Closer: Error processing base page wikitext:", error); removeLoadingPara.remove(); const errorPara = document.createElement('p'); errorPara.textContent = `  Error loading wikitext for ${removeLinkText}. Edit manually.`; errorPara.style.color = 'red'; removeDiv.appendChild(errorPara); }

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

                let generatedListEntry = null; let generatedRfarowTemplate = null; let yearlyListPageName = ''; let alphabeticalListPageName = ''; let 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}`;
                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); } }
                // Use closerUsername defined globally
                if (selectedOutcome === 'successful') {
                    isSuccessfulList = true;
                    yearlyListPageName = `Wikipedia:Successful ${rfxType} candidacies/${year}`;
                    generatedRfarowTemplate = `|{{rfarow|${actualCandidateUsername}||${formattedDate}|successful|${votes.support}|${votes.oppose}|${votes.neutral}|${closerUsername}}}`;
                } else if (['unsuccessful', 'withdrawn', 'notnow', 'snow'].includes(selectedOutcome)) {
                    isSuccessfulList = false;
                    let reasonText = 'Unsuccessful';
                    let 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;
                        case 'unsuccessful': reasonText = 'Unsuccessful'; rfarowResult = 'unsuccessful'; break;
                    }
                    yearlyListPageName = `Wikipedia:Unsuccessful ${rfxType} candidacies (Chronological)/${year}`;
                    const firstLetter = actualCandidateUsername.charAt(0).toUpperCase(); let alphaBase = `Wikipedia:Unsuccessful ${rfxType} candidacies`;
                    if (rfxType === 'adminship') { if (firstLetter >= 'A' && firstLetter <= 'Z') { alphabeticalListPageName = `${alphaBase}/${firstLetter}`; } else { alphabeticalListPageName = `${alphaBase} (Alphabetical)`; } }
                    else { alphabeticalListPageName = `${alphaBase} (Alphabetical)`; }
                    const isSubsequentNomination = /[_ ]\d+$/.test(pageName); const listPrefix = isSubsequentNomination ? '*:' : '*';
                    generatedListEntry = `${listPrefix} [[${pageName}|${actualCandidateUsername}]] ${formattedDate} - ${reasonText} ([[User:${closerUsername}|${closerUsername}]]) (${votes.support}/${votes.oppose}/${votes.neutral})`;
                    generatedRfarowTemplate = `|{{rfarow|${actualCandidateUsername}||${formattedDate}|${rfarowResult}|${votes.support}|${votes.oppose}|${votes.neutral}|${closerUsername}}}`;
                }

                // --- Handle Yearly Page Update ---
                if (yearlyListPageName) {
                    const yearlyDiv = document.createElement('div'); yearlyDiv.style.marginTop = '10px';
                    const yearlyInstruction = document.createElement('p');
                    yearlyInstruction.innerHTML = `2a. Update <a href="/wiki/${yearlyListPageName}" target="_blank">${yearlyListPageName}</a>:`;
                    yearlyDiv.appendChild(yearlyInstruction);

                    const knownIssueWarning = document.createElement('div');
                    knownIssueWarning.className = 'rfx-closer-known-issue';
                    knownIssueWarning.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.`;
                    yearlyDiv.appendChild(knownIssueWarning);

                    const yearlyLoading = document.createElement('p'); yearlyLoading.textContent = `  Loading...`; yearlyLoading.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 countDelta = 1;
                            const originalWikitextForCountCheck = modifiedYearlyWikitext; modifiedYearlyWikitext = updateCountInWikitext(modifiedYearlyWikitext, countRegex, countDelta);
                            if (modifiedYearlyWikitext !== originalWikitextForCountCheck) { modificationPerformed = true; countUpdated = true; } else { console.warn(`RfX Closer: Could not update count on ${yearlyListPageName}`); }
                            if (generatedRfarowTemplate) {
                                const escapedMonth = month.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
                                const activeMonthHeaderRegex = new RegExp(`^(\\|\\-\\s*\\n\\|\\s*colspan="[\\d]+".*\\{\\{Anchord\\|${escapedMonth}\\s+${year}\\}\\}.*?)$`, 'mi');
                                // Regex to find commented out header (captures comment tags and the header itself)
                                const commentedMonthHeaderRegex = new RegExp(`^(\\s*\\s*)$`, 'mi');

                                let insertionIndex = -1; let headerFound = false; let originalWikitextForInsertionCheck = modifiedYearlyWikitext;
                                let activeMatch = modifiedYearlyWikitext.match(activeMonthHeaderRegex);

                                if (activeMatch) {
                                    insertionIndex = activeMatch.index + activeMatch[0].length;
                                    headerFound = true;
                                    console.log(`RfX Closer: Found active month header for ${month} ${year}.`);
                                } else {
                                    let commentedMatch = modifiedYearlyWikitext.match(commentedMonthHeaderRegex);
                                    if (commentedMatch) {
                                        uncommentAttempted = true;
                                        const fullCommentedBlock = commentedMatch[0];
                                        const uncommentedHeader = commentedMatch[2].replace(/^\s*\|-\s*\n/, '').replace(/\s*-->\s*$/, ''); // Extract header content
                                        console.warn(`RfX Closer: Attempting to uncomment month header for ${month} ${year}. This might fail.`);
                                        modifiedYearlyWikitext = modifiedYearlyWikitext.replace(fullCommentedBlock, `|-\n${uncommentedHeader}`); // Replace comment with uncommented version
                                        modificationPerformed = true;
                                        // Find the insertion point *after* the now-uncommented header
                                        const searchStartIndex = commentedMatch.index; // Start search from original position
                                        const uncommentedHeaderIndex = modifiedYearlyWikitext.indexOf(uncommentedHeader, searchStartIndex);
                                        if (uncommentedHeaderIndex !== -1) {
                                            insertionIndex = uncommentedHeaderIndex + uncommentedHeader.length;
                                            headerFound = true;
                                            console.log(`RfX Closer: Header uncommented (assumed).`);
                                        } else {
                                            headerFound = false; // Failed to find it after replacement
                                            console.error(`RfX Closer: Failed to find header after attempting uncomment.`);
                                        }
                                    } else {
                                        headerFound = false;
                                        console.warn(`RfX Closer: Could not find active or commented header for ${month} ${year} on ${yearlyListPageName}.`);
                                    }
                                }

                                if (headerFound && insertionIndex !== -1) {
                                    let newlineIndex = modifiedYearlyWikitext.indexOf('\n', insertionIndex);
                                    if (newlineIndex === -1) { // Header is last line
                                        insertionIndex = modifiedYearlyWikitext.length; // Append to end
                                        if (modifiedYearlyWikitext.length > 0 && !modifiedYearlyWikitext.endsWith('\n')) {
                                            modifiedYearlyWikitext += '\n'; // Ensure newline before appending
                                            insertionIndex++;
                                        }
                                    } else {
                                        insertionIndex = newlineIndex + 1; // Insert on the next line
                                    }
                                    const insertionContent = '|-\n' + generatedRfarowTemplate + '\n';
                                    modifiedYearlyWikitext = modifiedYearlyWikitext.substring(0, insertionIndex) + insertionContent + modifiedYearlyWikitext.substring(insertionIndex);
                                    // Only set modificationPerformed if the insertion actually changed the text AND we didn't just uncomment
                                    if (modifiedYearlyWikitext !== originalWikitextForInsertionCheck && !uncommentAttempted) {
                                       modificationPerformed = true;
                                    }
                                    entryAdded = true;
                                    console.log(`RfX Closer: Entry added to ${yearlyListPageName}.`);
                                } 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).`; }

                            const yearlyNote = document.createElement('p'); yearlyNote.innerHTML = noteText; yearlyDiv.appendChild(yearlyNote);
                            if (modificationPerformed) {
                                yearlyDiv.appendChild(createActionLinks(yearlyListPageName, modifiedYearlyWikitext, yearlyListPageName));
                            }
                            if (!entryAdded && generatedRfarowTemplate) { 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(); const errorPara = document.createElement('p'); errorPara.innerHTML = `  Error processing ${yearlyListPageName}. Update manually.`; errorPara.style.color = 'red'; yearlyDiv.appendChild(errorPara); if (generatedRfarowTemplate) { yearlyDiv.appendChild(createCopyableBox(generatedRfarowTemplate, `Copy row template to manually insert`)); } }
                }

                // --- Handle Alphabetical List Page Update ---
                if (alphabeticalListPageName && !isSuccessfulList && generatedListEntry) {
                    const alphaDiv = document.createElement('div'); alphaDiv.style.marginTop = '15px';
                    const alphaInstruction = document.createElement('p');
                    alphaInstruction.innerHTML = `2b. Update <a href="/wiki/${alphabeticalListPageName}" target="_blank">${alphabeticalListPageName}</a>:`;
                    alphaDiv.appendChild(alphaInstruction);

                    const alphaLoading = document.createElement('p'); alphaLoading.textContent = `  Loading...`; alphaLoading.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 = `  `; // Start note on same line
                            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.`; }
                            const alphaNote = document.createElement('p'); alphaNote.innerHTML = alphaNoteText; alphaDiv.appendChild(alphaNote);
                            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(); const errorPara = document.createElement('p'); errorPara.innerHTML = `  Error processing ${alphabeticalListPageName}. Update manually.`; errorPara.style.color = 'red'; alphaDiv.appendChild(errorPara); if (generatedListEntry) { alphaDiv.appendChild(createCopyableBox(generatedListEntry, `Copy entry to manually insert`)); } }
                }

                 container.appendChild(addDiv);
                 return container;
            },
            // Show only for closing outcomes (not 'onhold')
            showFor: ['successful', 'unsuccessful', 'withdrawn', 'notnow', 'snow'],
            completed: false
        },
        // Renumbered Step 10: Notify Candidate (Closing Outcomes)
        {
            title: 'Notify Candidate (Closing)',
            description: (selectedOutcome) => {
                // This step should NOT be shown for 'onhold'
                if (selectedOutcome === 'onhold') {
                    return '(Notification for "on hold" is handled in Step 7.)';
                }

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

                const intro = document.createElement('p');
                intro.innerHTML = `Prepare message for <a href="/wiki/${encodeURIComponent(candidateTalkPage)}" target="_blank">${candidateTalkPage}</a>.`;
                container.appendChild(intro);

                const optionsDiv = document.createElement('div');
                optionsDiv.className = 'rfx-notify-options';
                container.appendChild(optionsDiv);

                const messageTextarea = document.createElement('textarea');
                messageTextarea.className = 'rfx-notify-textarea'; // Original class for this step
                messageTextarea.rows = 5; // Adjust as needed

                const copyBoxPlaceholder = document.createElement('div'); // Placeholder for copy box/links
                const buttonContainer = document.createElement('div'); // Container for buttons/status
                buttonContainer.style.marginTop = '10px';
                const statusArea = document.createElement('div'); // For API status
                statusArea.className = 'rfx-notify-status'; // Original class

                let messageContentGetter = () => ''; // Function to get current message content

                if (selectedOutcome === 'successful') {
                    const templateName = rfxType === 'adminship' ? 'New sysop' : 'New bureaucrat';
                    const defaultMessage = `{{subst:${templateName}}} [[User:Barkeep49|Barkeep49]] ([[User talk:Barkeep49|talk]]) 22:19, 15 April 2025 (UTC)`; // Use subst and signature

                    // Radio buttons for choice
                    const radioTemplateId = 'rfx-notify-template-radio';
                    const radioCustomId = 'rfx-notify-custom-radio';

                    const radioTemplateLabel = document.createElement('label');
                    radioTemplateLabel.htmlFor = radioTemplateId;
                    const radioTemplate = document.createElement('input');
                    radioTemplate.type = 'radio';
                    radioTemplate.name = 'rfx-notify-choice';
                    radioTemplate.id = radioTemplateId;
                    radioTemplate.value = 'template';
                    radioTemplate.checked = true; // Default to template
                    radioTemplateLabel.appendChild(radioTemplate);
                    radioTemplateLabel.appendChild(document.createTextNode(` Use {{${templateName}}}`));
                    optionsDiv.appendChild(radioTemplateLabel);

                    const radioCustomLabel = document.createElement('label');
                    radioCustomLabel.htmlFor = radioCustomId;
                    const radioCustom = document.createElement('input');
                    radioCustom.type = 'radio';
                    radioCustom.name = 'rfx-notify-choice';
                    radioCustom.id = radioCustomId;
                    radioCustom.value = 'custom';
                    radioCustomLabel.appendChild(radioCustom);
                    radioCustomLabel.appendChild(document.createTextNode(' Use custom message'));
                    optionsDiv.appendChild(radioCustomLabel);

                    // Textarea for custom message (initially hidden)
                    messageTextarea.value = `Congratulations! Your ${rfxType === 'adminship' ? 'RfA' : 'RfB'} was successful. [[User:Barkeep49|Barkeep49]] ([[User talk:Barkeep49|talk]]) 22:19, 15 April 2025 (UTC)`; // Default custom text with signature
                    messageTextarea.style.display = 'none';
                    container.appendChild(messageTextarea); // Add textarea to DOM
                    container.appendChild(copyBoxPlaceholder); // Add placeholder for copy box/links

                    // Function to update copy box based on radio selection
                    const updateSuccessActions = () => {
                        copyBoxPlaceholder.innerHTML = ''; // Clear previous box
                        if (radioTemplate.checked) {
                            messageContentGetter = () => defaultMessage;
                            // Use createActionLinks for the template
                            copyBoxPlaceholder.appendChild(createActionLinks(null, defaultMessage, `{{${templateName}}}`)); // No edit link needed here
                        } else {
                            messageContentGetter = () => messageTextarea.value;
                            // Pass the textarea element itself to createCopyableBox for live copying
                            copyBoxPlaceholder.appendChild(createCopyableBox(null, 'Copy the message above', false, messageTextarea));
                        }
                    };

                    // Event listeners for radio buttons
                    radioTemplate.addEventListener('change', () => {
                        messageTextarea.style.display = 'none';
                        updateSuccessActions();
                    });
                    radioCustom.addEventListener('change', () => {
                        messageTextarea.style.display = 'block';
                        updateSuccessActions();
                    });

                    // Initial copy box creation
                    updateSuccessActions();

                } else { // Unsuccessful outcomes (withdrawn, notnow, snow, unsuccessful)
                    // Determine reason text for message
                    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;
                        case 'unsuccessful': reasonPhrase = 'unsuccessful'; break; // Explicitly handle standard unsuccessful
                    }

                    const defaultMessage = `Hi ${actualCandidateUsername}. I'm ${closerUsername} and I have closed your ${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 ${rfxType === 'adminship' ? 'administrator' : 'bureaucrat'} rights after initially being unsuccessful. [[User:Barkeep49|Barkeep49]] ([[User talk:Barkeep49|talk]]) 22:19, 15 April 2025 (UTC)`; // Add signature
                    messageTextarea.value = defaultMessage;
                    container.appendChild(messageTextarea); // Add textarea to DOM
                    container.appendChild(copyBoxPlaceholder); // Add placeholder for copy box

                    messageContentGetter = () => messageTextarea.value; // Always get from textarea

                    // Create copy box linked to the textarea
                    copyBoxPlaceholder.appendChild(createCopyableBox(null, 'Copy the message above', false, messageTextarea));
                }

                // --- API Post Button ---
                const postButton = document.createElement('button');
                postButton.textContent = 'Post Notification via API';
                postButton.className = 'rfx-closer-action-button'; // Use existing style
                buttonContainer.appendChild(postButton);

                postButton.addEventListener('click', async () => {
                    postButton.disabled = true;
                    statusArea.textContent = 'Posting message...';
                    statusArea.style.color = 'inherit';

                    const messageContent = messageContentGetter(); // Get current message
                    const editSummary = `Notifying candidate of ${rfxType === 'adminship' ? 'RfA' : 'RfB'} outcome${tagLine}`;

                    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';
                        // Optionally mark step as complete
                        const stepElement = postButton.closest('.rfx-closer-step');
                        const stepIndex = parseInt(stepElement.dataset.step, 10);
                        if (!isNaN(stepIndex)) {
                            steps[stepIndex].completed = true;
                            stepElement.dataset.completed = true;
                            const checkbox = stepElement.querySelector('input[type="checkbox"]');
                            if(checkbox) checkbox.checked = true;
                        }
                        // Keep button disabled after success to prevent double-posting
                        postButton.textContent = 'Posted';

                    } else {
                        statusArea.textContent = `Error posting message: ${result.error}. Please post manually.`;
                        statusArea.style.color = 'red';
                        postButton.disabled = false; // Re-enable button on error
                    }
                });

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

                container.appendChild(buttonContainer); // Add button container
                container.appendChild(statusArea); // Add status area


                return container;
            },
            // Show only for closing outcomes (not 'onhold')
            showFor: ['successful', 'unsuccessful', 'withdrawn', 'notnow', 'snow'],
            completed: false
        }
    ];

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

    // Function to render a single step
    function renderStep(step, index, selectedOutcome = '', currentVotes = {}) {
        const stepElement = document.createElement('div');
        stepElement.className = 'rfx-closer-step';
        stepElement.dataset.step = index; // Use the actual index in the array
        stepElement.dataset.completed = step.completed;

        const titleElement = document.createElement('h3');
        const descriptionContainer = document.createElement('div');
        descriptionContainer.className = 'rfx-closer-step-description';

        // Calculate display number (1-based index)
        const displayIndex = index + 1;

        if (step.isSelectionStep) {
            // Step 3: Outcome Selector (index 3, display 4)
            titleElement.textContent = `${displayIndex}. ${step.title}`;
            descriptionContainer.textContent = step.description;

            const templateSelector = document.createElement('select');
            templateSelector.id = 'rfx-outcome-selector';
            // ** UPDATED Options including 'onhold' **
            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);

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

        } else {
            // Other steps
            const checkbox = document.createElement('input');
            checkbox.type = 'checkbox';
            checkbox.checked = step.completed;
            checkbox.dataset.stepIndex = index; // Store the actual 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}`)); // Use displayIndex

            // Use Promise.resolve() to handle both async and sync description functions
            Promise.resolve().then(() => {
                if (typeof step.description === 'function') {
                    // Only call description function if the step should be shown
                    if (!step.showFor || step.showFor.includes(selectedOutcome)) {
                        // Ensure actualCandidateUsername is up-to-date before calling description
                        return step.description(selectedOutcome, currentVotes); // Return the promise/value
                    } else {
                         // Provide specific message if step is hidden due to outcome
                         if (selectedOutcome === 'onhold' && (index === 8 || index === 9)) { // Steps 9 & 10 (Update Lists, Notify Closing)
                             return '(This step is not applicable for the "on hold" outcome.)';
                         }
                         if (selectedOutcome !== 'successful' && index === 7) { // Step 8 (Process Promotion)
                             return '(This step is only applicable for successful outcomes.)';
                         }
                         if (selectedOutcome !== 'onhold' && (index === 5 || index === 6)) { // Steps 6 & 7 (Notify Crats, Notify Candidate On Hold)
                             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;
                 }
            }).then(content => {
                // This block runs after the description function (async or sync) completes
                if (content instanceof HTMLElement || content instanceof DocumentFragment) {
                    // Clear previous content if re-rendering async content
                    while (descriptionContainer.firstChild) {
                        descriptionContainer.removeChild(descriptionContainer.firstChild);
                    }
                    descriptionContainer.appendChild(content);
                } else {
                    descriptionContainer.textContent = content || '';
                }
            }).catch(error => {
                // Handle errors from async description functions (like getUserGroups)
                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 based on 'showFor' and selectedOutcome
        // Steps 0-4 (display 1-5) always show.
        // Others depend on outcome.
        if (index <= 4) { // Steps 0, 1, 2, 3, 4 (display 1-5)
             stepElement.style.display = 'block';
        } else {
             stepElement.style.display = (step.showFor && step.showFor.includes(selectedOutcome)) ? 'block' : 'none';
        }

        return stepElement;
    }

    // Function to render all steps
    function renderAllSteps(selectedOutcome = '', currentVotes = {}) {
        stepsContainer.innerHTML = ''; // Clear existing steps
        steps.forEach((step, index) => {
            const stepElement = renderStep(step, index, selectedOutcome, currentVotes);
            stepsContainer.appendChild(stepElement);
        });
        // Re-attach selector value
        const selector = document.getElementById('rfx-outcome-selector');
        if (selector) selector.value = selectedOutcome;
    }

    // Event handler for outcome selection change
    function handleOutcomeChange(event) {
        const selectedOutcome = event.target.value;
        const currentVotes = getCurrentVoteCounts();

        // Mark step 3 (index 3) as completed if an outcome is selected
        if (steps[3]) { // Check if step exists (it should)
            steps[3].completed = selectedOutcome !== '';
        }

        // Determine if it's a closing outcome (not 'onhold' or empty)
        // This affects whether list pages are pre-fetched
        const isClosingOutcome = selectedOutcome && !['', 'onhold'].includes(selectedOutcome);

        // Pre-fetch wikitext if needed
        const promises = [fetchRfaData()]; // Ensure summary data promise is always included

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

        if (isClosingOutcome) {
            // Fetch pages needed for closing (base list, yearly list, alpha list)
            promises.push(fetchPageWikitext(baseRfxPage));
             const year = new Date().getFullYear();
             let yearlyListPageName = '';
             let alphabeticalListPageName = '';
             if (selectedOutcome === 'successful') {
                 yearlyListPageName = `Wikipedia:Successful ${rfxType} candidacies/${year}`;
             } else if (['unsuccessful', 'withdrawn', 'notnow', 'snow'].includes(selectedOutcome)) {
                 yearlyListPageName = `Wikipedia:Unsuccessful ${rfxType} candidacies (Chronological)/${year}`;
                 const firstLetter = actualCandidateUsername.charAt(0).toUpperCase();
                 let alphaBase = `Wikipedia:Unsuccessful ${rfxType} candidacies`;
                 if (rfxType === 'adminship') {
                     if (firstLetter >= 'A' && firstLetter <= 'Z') {
                         alphabeticalListPageName = `${alphaBase}/${firstLetter}`;
                     } else {
                         alphabeticalListPageName = `${alphaBase} (Alphabetical)`;
                     }
                 } else {
                     alphabeticalListPageName = `${alphaBase} (Alphabetical)`;
                 }
             }
             if (yearlyListPageName) {
                 promises.push(fetchPageWikitext(yearlyListPageName));
             }
             if (alphabeticalListPageName) {
                 promises.push(fetchPageWikitext(alphabeticalListPageName));
             }
        } else if (selectedOutcome === 'onhold') {
            // For 'onhold', pre-fetch bureaucrat list (needed for Step 6)
            promises.push(fetchBureaucrats());
        }


        // Wait for necessary data fetches before re-rendering
        Promise.all(promises).then(() => {
        // Re-render all steps with the new outcome and current votes
             renderAllSteps(selectedOutcome, currentVotes);
        }).catch(error => {
             console.error("RfX Closer: Error during pre-fetch in handleOutcomeChange:", error);
             // Still try to render, steps should handle their own errors
              renderAllSteps(selectedOutcome, currentVotes);
        });
    }


    // --- Event Listeners ---

    // Draggable Header
    let isDragging = false;
    let dragStartX, dragStartY, containerStartX, containerStartY;

    header.addEventListener('mousedown', (e) => {
        // Only drag if not clicking on a button inside the header
        if (e.target.tagName === 'BUTTON') return;
        isDragging = true;
        dragStartX = e.clientX;
        dragStartY = e.clientY;
        containerStartX = container.offsetLeft;
        containerStartY = container.offsetTop;
        container.style.cursor = 'grabbing';
        container.style.userSelect = 'none'; // Prevent text selection while dragging
        // Ensure position is fixed or absolute for dragging
        if (window.getComputedStyle(container).position === 'static') {
            container.style.position = 'fixed'; // Or 'absolute' depending on desired behavior
            // Recalculate start positions if position changed
            containerStartX = container.offsetLeft;
            containerStartY = container.offsetTop;
        }
        // Reset transform if using top/left for positioning
        container.style.transform = '';
        // Set initial top/left based on current position
        container.style.left = containerStartX + 'px';
        container.style.top = containerStartY + 'px';

    });

    document.addEventListener('mousemove', (e) => {
        if (!isDragging) return;
        const dx = e.clientX - dragStartX;
        const dy = e.clientY - dragStartY;
        container.style.left = (containerStartX + dx) + 'px';
        container.style.top = (containerStartY + dy) + 'px';
    });

    document.addEventListener('mouseup', () => {
        if (isDragging) {
            isDragging = false;
            container.style.cursor = 'grab';
            container.style.userSelect = '';
        }
    });


    // Collapse/Expand Button
    let isCollapsed = false;
    collapseButton.addEventListener('click', () => {
        isCollapsed = !isCollapsed;
        contentAndInputContainer.style.display = isCollapsed ? 'none' : 'flex';
        collapseButton.innerHTML = isCollapsed ? '+' : '−';
        // If using top/left for dragging, don't reset top/transform here
        // container.style.top = isCollapsed ? '10px' : '50%';
        // container.style.transform = isCollapsed ? 'translateY(0)' : 'translateY(-50%)';
        container.style.maxHeight = isCollapsed ? '' : '90vh';
    });


    // Close Button
    closeButton.addEventListener('click', () => {
        container.style.display = 'none';
    });

    // Launch Button (created later)

    // --- Initialization ---
    // Add mw.util dependency for getUrl
    mw.loader.using(['oojs-ui-core', 'oojs-ui-widgets', 'mediawiki.api', 'mediawiki.widgets.DateInputWidget', 'mediawiki.util'], function() {
         // Fetch summary data first, then render initial steps
         fetchRfaData().then(() => {
             updateInputFields(); // Update inputs based on fetched data
             // Don't fetch wikitext until needed (outcome selected)
             renderAllSteps(); // Initial render

             // --- Add event listeners for dynamic percentage update ---
             const supportInput = document.getElementById('support-count');
             const opposeInput = document.getElementById('oppose-count');
             if (supportInput) {
                 supportInput.addEventListener('input', updatePercentageDisplay);
             }
             if (opposeInput) {
                 opposeInput.addEventListener('input', updatePercentageDisplay);
             }
         });


        // Create and add launch button to the toolbar
        const launchButton = document.createElement('a');
        launchButton.id = 'rfx-closer-launch';
        launchButton.textContent = 'RfX Closer';
        launchButton.href = '#'; // Prevent page jump
        launchButton.addEventListener('click', (e) => {
            e.preventDefault();
            if (container.style.display === 'none' || container.style.display === '') {
                // Fetch summary data if not already fetched or if an error occurred
                if (!rfaData || fetchErrorOccurred) {
                    fetchRfaData().then(() => {
                        updateInputFields();
                        // Re-render steps based on new data and potentially selected outcome
                        renderAllSteps(document.getElementById('rfx-outcome-selector')?.value || '', getCurrentVoteCounts());
                        container.style.display = 'flex';
                    });
                } else {
                    // Data exists, just show
                    container.style.display = 'flex';
                }
            } else {
                container.style.display = 'none';
            }
        });

        // Add launch button to the 'Tools' menu (p-tb)
        console.log("RfX Closer: Attempting to add launch button..."); // DEBUGGING
        const pageTools = document.querySelector('#p-tb ul');
        if (pageTools) {
            const li = document.createElement('li');
            li.id = 'rfx-closer-launch-li';
            li.appendChild(launchButton);
            pageTools.appendChild(li);
            console.log("RfX Closer: Launch button potentially added."); // DEBUGGING
        } else {
             // Log if the tools menu wasn't found
             console.warn("RfX Closer: Could not find the tools menu ('#p-tb ul') to add the launch button.");
        }
    }); // End mw.loader.using

})();