/**
 * WP PowerSuite Admin JavaScript
 *
 * Module management UI with filtering, sorting, and AJAX toggles.
 *
 * @package WP_PowerSuite\Admin\Assets
 * @since 1.0.0
 */

(function() {
	'use strict';

	/** Category tabs after All and Favorites (must match ModuleCategories::ORDER). */
	const WPPS_CATEGORY_TAB_ORDER = ['Security', 'Login & Users', 'Admin', 'Content', 'Media', 'SEO & Visibility', 'Speed', 'Site Tools'];

	/** Inline refresh icon Ã¢â‚¬â€ inherits button text color via currentColor. */
	const WPPS_REFRESH_ICON = '<svg class="wp-powersuite-refresh-icon" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" focusable="false"><path d="M17.65 6.35A7.958 7.958 0 0 0 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08A5.99 5.99 0 0 1 12 18c-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>';

	// Main app object
	const WPPSAdmin = {
		modules: [],
		enabled: [],
		favorites: [],
		moduleOrder: [],
		categories: [],
		filteredModules: [],
		viewMode: 'grid',
		filters: {
			search: '',
			category: '', // '' = All; '__favorites' = Favorites tab; else named category slug
			status: '',
			tier: ''
		},
		sortBy: 'enabled-first', // Default: Enabled First
		descriptionMode: 'show', // 'show', 'hide', or 'truncate' (grid only)
		showCategory: true, // Show category column on All and Favorites tabs; hide when a single topic tab is selected
		gridColumns: 3,
		mobileBreakpoint: 782,
		selectedModules: [], // For bulk operations
		settingsCache: {}, // Cache for module settings HTML
		toggleQueue: [], // Queue for individual toggles so rapid clicks are processed one-by-one
		toggleInProgress: false,
		bulkAborted: false,
		savedScrollPosition: null, // Restore scroll after closing settings modal
		modalAccessibility: {
			lastFocus: null,
			trapHandler: null,
			modalEl: null
		},

		/**
		 * Modules list container (JS-rendered cards; preserves no-JS fallback in shell).
		 */
		getModulesListEl: function() {
			return document.getElementById('wp-powersuite-modules-list')
				|| document.getElementById('wp-powersuite-modules');
		},

		/**
		 * Outer modules shell (loading, status region, no-JS table).
		 */
		getModulesShellEl: function() {
			return document.getElementById('wp-powersuite-modules');
		},

		isMobileLayout: function() {
			return window.matchMedia('(max-width: ' + this.mobileBreakpoint + 'px)').matches;
		},

		getEffectiveViewMode: function() {
			return this.isMobileLayout() ? 'grid' : this.viewMode;
		},

		getEffectiveDescriptionMode: function() {
			return this.isMobileLayout() ? 'truncate' : this.descriptionMode;
		},

		getEffectiveGridColumns: function() {
			return this.isMobileLayout() ? 1 : this.gridColumns;
		},

		isModalVisible: function(modal) {
			if (!modal) {
				return false;
			}
			const inlineDisplay = modal.style.display;
			if (inlineDisplay === 'none') {
				return false;
			}
			if (inlineDisplay === 'flex' || inlineDisplay === 'block') {
				return true;
			}
			return window.getComputedStyle(modal).display !== 'none';
		},

		getModalDialog: function(modal) {
			return modal ? modal.querySelector('[role="dialog"]') : null;
		},

		getFocusableElements: function(container) {
			if (!container) {
				return [];
			}
			return Array.from(container.querySelectorAll(
				'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]):not([type="hidden"]), select:not([disabled]), [tabindex]:not([tabindex="-1"])'
			)).filter(function(el) {
				return el.offsetWidth > 0 || el.offsetHeight > 0 || el.getClientRects().length > 0;
			});
		},

		activateModalAccessibility: function(modal) {
			const dialog = this.getModalDialog(modal);
			if (!dialog || !modal) {
				return;
			}

			this.deactivateModalAccessibility();

			this.modalAccessibility.lastFocus = document.activeElement;
			this.modalAccessibility.modalEl = modal;
			modal.setAttribute('aria-hidden', 'false');

			const self = this;
			this.modalAccessibility.trapHandler = function(e) {
				if (e.key !== 'Tab' || !self.isModalVisible(modal)) {
					return;
				}
				const focusable = self.getFocusableElements(dialog);
				if (!focusable.length) {
					return;
				}
				const first = focusable[0];
				const last = focusable[focusable.length - 1];
				if (e.shiftKey && document.activeElement === first) {
					e.preventDefault();
					last.focus();
				} else if (!e.shiftKey && document.activeElement === last) {
					e.preventDefault();
					first.focus();
				}
			};
			dialog.addEventListener('keydown', this.modalAccessibility.trapHandler);

			const focusable = this.getFocusableElements(dialog);
			if (focusable.length) {
				focusable[0].focus();
			} else {
				dialog.setAttribute('tabindex', '-1');
				dialog.focus();
			}
		},

		deactivateModalAccessibility: function() {
			const modal = this.modalAccessibility.modalEl;
			const dialog = modal ? this.getModalDialog(modal) : null;
			if (modal) {
				modal.setAttribute('aria-hidden', 'true');
			}
			if (dialog && this.modalAccessibility.trapHandler) {
				dialog.removeEventListener('keydown', this.modalAccessibility.trapHandler);
			}
			const lastFocus = this.modalAccessibility.lastFocus;
			if (lastFocus && typeof lastFocus.focus === 'function') {
				lastFocus.focus();
			}
			this.modalAccessibility.lastFocus = null;
			this.modalAccessibility.trapHandler = null;
			this.modalAccessibility.modalEl = null;
		},

		announceModulesStatus: function(count) {
			const status = document.getElementById('wp-powersuite-modules-status');
			if (!status) {
				return;
			}
			const strings = window.wpPowerSuite && window.wpPowerSuite.strings;
			if (count === 0) {
				status.textContent = (strings && strings.modulesNone)
					|| 'No modules match the current filters.';
				return;
			}
			const template = strings && strings.modulesLoaded;
			if (template && typeof template === 'string') {
				status.textContent = template.replace('%d', String(count));
			} else {
				status.textContent = count + ' modules displayed.';
			}
		},

		/**
		 * Initialize the admin interface
		 */
		init: function() {
			const adminRoot = document.querySelector('.wp-powersuite-admin');
			if (!adminRoot) {
				return;
			}
			adminRoot.classList.add('is-js-ready');
			this.loadData();
			this.syncViewModeClass();
			this.reorderCategoryFilterButtons();
			// This must happen before applyFilters() and renderModules() to prevent flash
			this.updateCategoryButtonState();
			this.applyFilters(); // Apply filters after loading data and setting category
			this.bindEvents();
			this.bindMobileLayoutListener();
			this.renderModules();
			this.initializeLayoutControls();
			// Expose for modules loaded via AJAX (e.g. Custom Image Sizes Regenerate link)
			if (window.wpPowerSuite) {
				window.wpPowerSuite.openAdvancedSettings = this.openAdvancedSettings.bind(this);
			}
			this.consumeDeepLinkQueryParams();

			const reopenModal = sessionStorage.getItem('wpps_reopen_modal');
			if (reopenModal) {
				sessionStorage.removeItem('wpps_reopen_modal');
				const customPageModules = ['coming-soon-maintenance', 'admin-menu-organizer', 'options-page-builder', 'basic-forms', 'code-snippet', 'admin-columns-customizer', 'temporary-login', 'redirect-manager', 'customizer-reset', 'backup-restore', 'login-page-customizor'];
				if (customPageModules.includes(reopenModal)) {
					const url = this.getCustomPageUrl(reopenModal);
					if (url && url !== '#') {
						window.location.href = url;
						return;
					}
				}
				setTimeout(() => {
					this.openAdvancedSettings(reopenModal);
				}, 500);
			}
		},


		/**
		 * Open a module modal from admin deep links (?wpps_module=&wpps_section=).
		 */
		consumeDeepLinkQueryParams: function() {
			const urlParams = new URLSearchParams(window.location.search);
			const moduleId = urlParams.get('wpps_module');
			if (!moduleId) {
				return;
			}

			sessionStorage.setItem('wpps_reopen_modal', moduleId);

			const section = urlParams.get('wpps_section');
			if (section) {
				sessionStorage.setItem('wpps_reopen_modal_section', section);
			} else {
				sessionStorage.removeItem('wpps_reopen_modal_section');
			}

			urlParams.delete('wpps_module');
			urlParams.delete('wpps_section');
			const nextSearch = urlParams.toString();
			const nextUrl = window.location.pathname + (nextSearch ? '?' + nextSearch : '') + window.location.hash;
			window.history.replaceState({}, '', nextUrl);
		},

		/**
		 * Load data from DOM attributes
		 */
		loadData: function() {
			const container = document.querySelector('.wp-powersuite-admin');
			if (!container) {
				console.error('WP PowerSuite: Container .wp-powersuite-admin not found');
				return;
			}

			try {
				const modulesJson = container.dataset.modules || '{}';
				const enabledJson = container.dataset.enabled || '[]';
				const favoritesJson = container.dataset.favorites || '[]';
				const moduleOrderJson = container.dataset.moduleOrder || '[]';
				const categoriesJson = container.dataset.categories || '[]';
				
				this.modules = JSON.parse(modulesJson);
				this.enabled = JSON.parse(enabledJson);
				this.favorites = JSON.parse(favoritesJson);
				this.moduleOrder = JSON.parse(moduleOrderJson);
				this.categories = JSON.parse(categoriesJson);
				this.viewMode = container.dataset.viewMode || 'grid';
				this.descriptionMode = container.dataset.descriptionMode || 'show';
				this.licenseValid = container.dataset.licenseValid === '1';
				this.licensePageUrl = container.dataset.licensePageUrl || '';

				if (typeof this.modules === 'object' && this.modules !== null && !Array.isArray(this.modules)) {
					this.modules = Object.values(this.modules);
				} else if (!Array.isArray(this.modules)) {
					this.modules = [];
				}

				if (!Array.isArray(this.enabled)) {
					this.enabled = [];
				}
				if (!Array.isArray(this.favorites)) {
					this.favorites = [];
				}
				if (!Array.isArray(this.moduleOrder)) {
					this.moduleOrder = [];
				}
				if (!Array.isArray(this.categories)) {
					this.categories = [];
				}

				// Description mode only applies in grid view; list view hides description
				
				// Category: URL wins; else optional server bootstrap (e.g. Favorites for curated defaults); else All.
				const urlParams = new URLSearchParams(window.location.search);
				const urlCategory = urlParams.get('category');
				if (urlCategory && (urlCategory === '__favorites' || this.categories.includes(urlCategory))) {
					this.filters.category = urlCategory;
				} else {
					const boot = (container.dataset.bootstrapCategory || '').trim();
					if (boot && (boot === '__favorites' || this.categories.includes(boot))) {
						this.filters.category = boot;
					} else {
						this.filters.category = '';
					}
				}
				const ac = this.filters.category;
				this.showCategory = ac === '' || ac === '__favorites';

				const savedGridCols = localStorage.getItem('wpps_grid_columns');
				if (savedGridCols !== null) {
					this.gridColumns = parseInt(savedGridCols, 10) || 3;
				}

			} catch (e) {
				console.error('WP PowerSuite: Error loading module data', e);
				console.error('Modules JSON:', container.dataset.modules);
				this.modules = [];
				this.enabled = [];
				this.favorites = [];
				this.categories = [];
				this.filteredModules = [];
			}
		},

		/**
		 * Bind event listeners
		 */
		bindEvents: function() {
			// Advanced settings modal
			this.bindModalEvents();
			// License required modal
			this.bindLicenseModalEvents();

			// Category filter buttons
			document.addEventListener('click', (e) => {
				const btn = e.target.closest('.wp-powersuite-category-filter');
				if (btn) {
					e.preventDefault();
					const category = btn.dataset.category || '';
					this.setCategoryFilter(category);
				}
			});

			window.addEventListener('popstate', (e) => {
				const urlParams = new URLSearchParams(window.location.search);
				const urlCategory = urlParams.get('category');
				if (urlCategory && (urlCategory === '__favorites' || this.categories.includes(urlCategory))) {
					this.filters.category = urlCategory;
				} else {
					this.filters.category = '';
				}
				const ac = this.filters.category;
				this.showCategory = ac === '' || ac === '__favorites';
				this.updateCategoryButtonState();
				this.applyFilters();
			});

			const refreshButton = document.querySelector('.wp-powersuite-refresh-page');
			if (refreshButton) {
				refreshButton.addEventListener('click', (e) => {
					e.preventDefault();
					const originalText = refreshButton.innerHTML;
					refreshButton.innerHTML = WPPS_REFRESH_ICON + ' ' + this.i18n('Refreshing...', 'wp-powersuite');
					refreshButton.disabled = true;
					// Reload the page
					window.location.href = window.location.href;
				});
			}

			const modalRefreshButton = document.querySelector('.wp-powersuite-modal__refresh');
			if (modalRefreshButton) {
				modalRefreshButton.addEventListener('click', (e) => {
					e.preventDefault();
					const modal = document.getElementById('wp-powersuite-modal');
					const moduleId = modal?.dataset.currentModuleId;
					
					if (moduleId) {
						sessionStorage.setItem('wpps_reopen_modal', moduleId);
					}
					
					modalRefreshButton.innerHTML = WPPS_REFRESH_ICON + ' ' + this.i18n('Refreshing...', 'wp-powersuite');
					modalRefreshButton.disabled = true;
					
					// Reload the page
					window.location.href = window.location.href;
				});
			}

			// Layout controls - description mode icon (single button, cycles on click)
			const descModeBtn = document.querySelector('.wp-powersuite-description-mode-btn');
			if (descModeBtn) {
				this.updateDescriptionModeIcon();
				descModeBtn.addEventListener('click', () => {
					const order = ['show', 'truncate', 'hide'];
					const idx = order.indexOf(this.descriptionMode);
					const nextIdx = (idx + 1) % order.length;
					this.descriptionMode = order[nextIdx];
					this.saveDescriptionMode(this.descriptionMode);
					this.updateDescriptionModeIcon();
					this.renderModules();
				});
			}

			const gridColumnsSelect = document.getElementById('wp-powersuite-grid-columns');
			if (gridColumnsSelect) {
				gridColumnsSelect.value = this.gridColumns;
				gridColumnsSelect.addEventListener('change', (e) => {
					this.gridColumns = parseInt(e.target.value, 10);
					localStorage.setItem('wpps_grid_columns', this.gridColumns);
					this.updateGridColumns();
				});
			}

			// Search
			const searchInput = document.getElementById('wp-powersuite-search');
			if (searchInput) {
				searchInput.addEventListener('input', (e) => {
					this.filters.search = e.target.value.toLowerCase();
					this.applyFilters();
				});
			}


			// Status filter
			const statusFilter = document.getElementById('wp-powersuite-filter-status');
			if (statusFilter) {
				statusFilter.addEventListener('change', (e) => {
					this.filters.status = e.target.value;
					this.applyFilters();
				});
			}

			// Tier filter (Pro / Free)
			const tierFilter = document.getElementById('wp-powersuite-filter-tier');
			if (tierFilter) {
				tierFilter.addEventListener('change', (e) => {
					this.filters.tier = e.target.value;
					this.applyFilters();
				});
			}

			// Sort
			const sortSelect = document.getElementById('wp-powersuite-sort');
			if (sortSelect) {
				sortSelect.value = this.sortBy;
				sortSelect.addEventListener('change', (e) => {
					this.sortBy = e.target.value;
					this.applyFilters();
				});
			}

			// View toggle
			const viewToggles = document.querySelectorAll('.wp-powersuite-view-toggle');
			viewToggles.forEach(toggle => {
				toggle.addEventListener('click', (e) => {
					const view = e.currentTarget.dataset.view;
					if (this.isMobileLayout() && view === 'list') {
						return;
					}
					this.setViewMode(view);
				});
			});

			// Delegate module actions
			const modulesContainer = document.getElementById('wp-powersuite-modules');
			if (modulesContainer) {
				modulesContainer.addEventListener('click', (e) => {
					// Queued so rapid clicks don't race.
					if (e.target.closest('.wp-powersuite-toggle')) {
						e.preventDefault();
						const moduleId = e.target.closest('.wp-powersuite-module').dataset.moduleId;
						const module = this.modules.find(m => m.id === moduleId);
						if (module && module.is_pro && !this.licenseValid) {
							this.openLicenseRequiredModal();
							return;
						}
						this.queueToggle(moduleId);
					}

					if (e.target.closest('.wp-powersuite-favorite')) {
						e.preventDefault();
						const moduleId = e.target.closest('.wp-powersuite-module').dataset.moduleId;
						this.toggleFavorite(moduleId);
					}

					if (e.target.classList.contains('wp-powersuite-module-checkbox')) {
						const moduleId = e.target.value;
						if (e.target.checked) {
							if (!this.selectedModules.includes(moduleId)) {
								this.selectedModules.push(moduleId);
							}
						} else {
							this.selectedModules = this.selectedModules.filter(id => id !== moduleId);
						}
						this.updateBulkActions();
						this.updateSelectAll();
					}

					// Select all checkbox
					if (e.target.classList.contains('wp-powersuite-select-all')) {
						const checkboxes = modulesContainer.querySelectorAll('.wp-powersuite-module-checkbox');
						checkboxes.forEach(cb => {
							cb.checked = e.target.checked;
							const moduleId = cb.value;
							if (e.target.checked) {
								if (!this.selectedModules.includes(moduleId)) {
									this.selectedModules.push(moduleId);
								}
							} else {
								this.selectedModules = this.selectedModules.filter(id => id !== moduleId);
							}
						});
						this.updateBulkActions();
					}

					// Bulk actions
					if (e.target.classList.contains('wp-powersuite-bulk-disable')) {
						e.preventDefault();
						this.bulkDisableModules();
					}
					if (e.target.classList.contains('wp-powersuite-bulk-clear')) {
						e.preventDefault();
						this.clearSelection();
					}
				});
			}
		},

		/**
		 * Apply filters and sorting
		 */
		applyFilters: function() {
			let filtered = [...this.modules];

			// Category column: visible on All and Favorites tabs
			const curCat = this.filters.category || '';
			this.showCategory = curCat === '' || curCat === '__favorites';

			if (this.filters.search) {
				filtered = filtered.filter(module => {
					return module.name.toLowerCase().includes(this.filters.search) ||
						module.description.toLowerCase().includes(this.filters.search) ||
						module.id.toLowerCase().includes(this.filters.search);
				});
			}

			// Category filter (or Favorites tab)
			const fc = this.filters.category || '';
			if (fc === '__favorites') {
				filtered = filtered.filter(module => this.favorites.includes(module.id));
			} else if (fc) {
				filtered = filtered.filter(module => module.category === fc);
			}

			// Status filter
			if (this.filters.status) {
				filtered = filtered.filter(module => {
					const isEnabled = this.enabled.includes(module.id);
					return this.filters.status === 'enabled' ? isEnabled : !isEnabled;
				});
			}

			// Tier filter (Pro / Free)
			if (this.filters.tier === 'pro') {
				filtered = filtered.filter(module => module.is_pro || module.is_freemium);
			} else if (this.filters.tier === 'free') {
				filtered = filtered.filter(module => !module.is_pro);
			}

			filtered.sort((a, b) => {
				switch (this.sortBy) {
					case 'name-asc':
						return a.name.localeCompare(b.name);
					case 'name-desc':
						return b.name.localeCompare(a.name);
					case 'enabled-first': {
						const aEnabled = this.enabled.includes(a.id);
						const bEnabled = this.enabled.includes(b.id);
						if (aEnabled !== bEnabled) {
							return aEnabled ? -1 : 1;
						}
						return a.name.localeCompare(b.name);
					}
					case 'disabled-first': {
						const aEnabled = this.enabled.includes(a.id);
						const bEnabled = this.enabled.includes(b.id);
						if (aEnabled !== bEnabled) {
							return aEnabled ? 1 : -1;
						}
						return a.name.localeCompare(b.name);
					}
					default:
						return a.name.localeCompare(b.name);
				}
			});

			this.filteredModules = filtered;
			this.renderModules();
		},

		/**
		 * Render modules
		 */
		renderModules: function() {
			const shell = this.getModulesShellEl();
			const container = this.getModulesListEl();
			const emptyState = document.getElementById('wp-powersuite-empty');

			if (!container) {
				return;
			}

			const loading = shell ? shell.querySelector('.wp-powersuite-loading') : null;
			if (loading) {
				loading.style.display = 'none';
			}

			if (this.filteredModules.length === 0) {
				container.innerHTML = '';
				if (emptyState) {
					emptyState.style.display = 'block';
				}
				this.announceModulesStatus(0);
				return;
			}

			if (emptyState) {
				emptyState.style.display = 'none';
			}

			const effectiveViewMode = this.getEffectiveViewMode();

			container.className = 'wp-powersuite-modules-list wp-powersuite-view-' + effectiveViewMode;

			if (effectiveViewMode === 'grid') {
				this.updateGridColumns();
			}

			if (effectiveViewMode === 'list') {
				container.innerHTML = this.renderListView();
			} else {
				container.innerHTML = this.renderGridView();
			}

			this.announceModulesStatus(this.filteredModules.length);

			this.updateViewToggle();

			this.initSortable();
		},

		/**
		 * Get enabled modules that conflict with the given module (for display).
		 */
		getConflictingEnabled: function(module) {
			const conflicts = module.conflicts || [];
			if (!conflicts.length) return [];
			return conflicts
				.filter(id => this.enabled.includes(id))
				.map(id => this.modules.find(m => m.id === id))
				.filter(Boolean);
		},

		/**
		 * Get required modules that are not currently enabled.
		 */
		getMissingDependencies: function(module) {
			const dependencies = Array.isArray(module.dependencies) ? module.dependencies : [];
			if (!dependencies.length) return [];
			return dependencies
				.filter(id => !this.enabled.includes(id))
				.map(id => this.modules.find(m => m.id === id) || { id: id, name: id });
		},

		isModuleLocked: function(module) {
			return !!(module && ((module.is_pro && !this.licenseValid) || module.requires_pro_addon));
		},

		canActivateLicense: function() {
			if (window.wpPowerSuite && typeof window.wpPowerSuite.canActivateLicense !== 'undefined') {
				return !!window.wpPowerSuite.canActivateLicense;
			}
			return !!(window.wpPowerSuite && window.wpPowerSuite.licensePageUrl);
		},

		licenseModalMode: function(module) {
			if ((module && module.requires_pro_addon) || !this.canActivateLicense()) {
				return 'pro-addon';
			}
			return 'license';
		},

		moduleLockTitle: function(module) {
			const strings = (window.wpPowerSuite && window.wpPowerSuite.strings) || {};
			if ((module && module.requires_pro_addon) || !this.canActivateLicense()) {
				return strings.proAddonRequired || this.i18n('This module requires the WP PowerSuite Pro add-on.', 'wp-powersuite');
			}
			return strings.licenseRequired || this.i18n('Activate license to enable', 'wp-powersuite');
		},

		renderModuleTierBadge: function(module, options = {}) {
			const modal = options.modal === true;
			if (module.is_freemium) {
				const cls = modal ? ' wpps-freemium-badge--modal' : '';
				const label = this.i18n('Free + Pro', 'wp-powersuite') || 'Free + Pro';
				return ' <span class="wpps-freemium-badge' + cls + '">' + this.escapeHtml(label) + '</span>';
			}
			if (module.is_pro) {
				const cls = modal ? ' wpps-pro-badge--modal' : '';
				const label = modal
					? (this.i18n('Premium', 'wp-powersuite') || 'Premium')
					: (this.i18n('Pro', 'wp-powersuite') || 'Pro');
				return ' <span class="wpps-pro-badge' + cls + '">' + this.escapeHtml(label) + '</span>';
			}
			return '';
		},

		/**
		 * Red conflict tooltip icon for footer/actions (disabled modules only).
		 */
		renderConflictTooltip: function(module, isEnabled) {
			const conflictingEnabled = this.getConflictingEnabled(module);
			if (isEnabled || !conflictingEnabled.length) {
				return '';
			}
			const conflictsWith = (window.wpPowerSuite && window.wpPowerSuite.strings && window.wpPowerSuite.strings.conflictsWith) || 'Conflicts with';
			const names = conflictingEnabled.map(m => this.escapeHtml(m.name)).join(', ');
			const label = conflictingEnabled.map(m => m.name).join(', ');
			return `
				<span class="wp-powersuite-conflict-wrap">
					<button type="button" class="wp-powersuite-conflict-trigger" aria-label="${this.escapeHtml(conflictsWith + ': ' + label)}">
						<span class="dashicons dashicons-warning" aria-hidden="true"></span>
					</button>
					<div class="wp-powersuite-conflict-hover-panel" role="tooltip">
						<p>${this.escapeHtml(conflictsWith)}: ${names}</p>
					</div>
				</span>
			`;
		},

		/**
		 * Refresh conflict tooltips after enabled list changes.
		 */
		updateConflictIndicators: function() {
			document.querySelectorAll('.wp-powersuite-module[data-module-id]').forEach(el => {
				const moduleId = el.getAttribute('data-module-id');
				const module = this.modules.find(m => m.id === moduleId);
				if (!module) {
					return;
				}
				const isEnabled = this.enabled.includes(moduleId);
				const html = this.renderConflictTooltip(module, isEnabled);
				const container = el.querySelector('.wp-powersuite-toggle-group');
				if (!container) {
					return;
				}
				const existing = container.querySelector('.wp-powersuite-conflict-wrap');
				if (existing) {
					existing.remove();
				}
				if (html) {
					container.insertAdjacentHTML('beforeend', html);
				}
			});
		},

		/**
		 * Render grid view
		 */
		renderGridView: function() {
			const descriptionMode = this.getEffectiveDescriptionMode();

			return this.filteredModules.map(module => {
				const isEnabled = this.enabled.includes(module.id);
				const isFavorite = this.favorites.includes(module.id);
				const isPremiumLocked = this.isModuleLocked(module);
				const missingDependencies = this.getMissingDependencies(module);
				const conflictTooltip = this.renderConflictTooltip(module, isEnabled);
				const dependencyWarning = !isEnabled && missingDependencies.length > 0
					? `<div class="wp-powersuite-module__dependency">
						<span class="dashicons dashicons-admin-plugins"></span>
						${this.escapeHtml(this.i18n('Requires', 'wp-powersuite'))}: ${missingDependencies.map(m => this.escapeHtml(m.name)).join(', ')}
					</div>`
					: '';
				
				return `
					<div class="wp-powersuite-module wp-powersuite-module--grid" data-module-id="${this.escapeHtml(module.id)}">
						<div class="wp-powersuite-module__header">
							<h3 class="wp-powersuite-module__title">${this.escapeHtml(module.name)}${this.renderModuleTierBadge(module)}</h3>
							<div class="wp-powersuite-module__header-actions">
								<button class="wp-powersuite-favorite ${isFavorite ? 'is-active' : ''}" type="button" title="${isFavorite ? 'Remove from favorites' : 'Add to favorites'}">
									<span class="dashicons dashicons-star-filled"></span>
								</button>
								${(descriptionMode === 'hide' || descriptionMode === 'truncate') ? `
								<span class="wp-powersuite-description-info-wrap">
									<button type="button" class="wp-powersuite-description-info" aria-label="${this.escapeHtml(this.i18n('View description', 'wp-powersuite'))}">
										<span class="dashicons dashicons-info" aria-hidden="true"></span>
									</button>
									<div class="wp-powersuite-description-hover-panel" role="tooltip">
										<p>${this.escapeHtml(module.description || this.i18n('No description available.', 'wp-powersuite'))}</p>
									</div>
								</span>
								` : ''}
							</div>
						</div>
						<div class="wp-powersuite-module__body${descriptionMode === 'hide' ? ' wp-powersuite-module__body--no-description' : ''}">
							${(descriptionMode === 'show' || descriptionMode === 'truncate') ? `<p class="wp-powersuite-module__description${descriptionMode === 'truncate' ? ' wp-powersuite-module__description--truncate' : ''}">${this.escapeHtml(module.description || 'No description available.')}</p>` : ''}
							${dependencyWarning}
							${this.showCategory && descriptionMode !== 'hide' ? `
							<div class="wp-powersuite-module__meta">
								<span class="wp-powersuite-module__category">${this.escapeHtml(this.formatCategory(module.category))}</span>
							</div>
							` : ''}
						</div>
						<div class="wp-powersuite-module__footer">
							<div class="wp-powersuite-toggle-group">
								${isPremiumLocked && !isEnabled ? `
									<div class="wp-powersuite-toggle-wrapper wp-powersuite-toggle-wrapper--locked">
										<button class="wp-powersuite-toggle wp-powersuite-toggle--locked button button-secondary" type="button">
											${this.i18n('Enable', 'wp-powersuite')}
										</button>
										<span class="wp-powersuite-lock-icon" title="${this.escapeHtml(this.moduleLockTitle(module))}">
											<span class="dashicons dashicons-lock"></span>
										</span>
									</div>
								` : `
									<button class="wp-powersuite-toggle button ${isEnabled ? 'button-primary' : 'button-secondary'}" type="button">
										${isEnabled ? 'Disable' : 'Enable'}
									</button>
								`}
								${conflictTooltip}
							</div>
							<div class="wp-powersuite-module__footer-right">
								${module.has_custom_page && isEnabled ? `
									<a href="${this.escapeHtml(module.custom_page_url || this.getCustomPageUrl(module.id))}" class="wp-powersuite-custom-page-link" title="${this.escapeHtml('Open ' + module.name)}">
										<span class="dashicons dashicons-admin-links"></span>
									</a>
								` : ''}
								${module.has_advanced_settings && !module.has_custom_page && isEnabled ? `
									<button class="wp-powersuite-advanced-settings" type="button" title="${this.escapeHtml('Advanced Settings')}" data-module-id="${this.escapeHtml(module.id)}">
										<span class="dashicons dashicons-admin-generic"></span>
									</button>
								` : ''}
								${module.has_advanced_settings && !module.has_custom_page && !isEnabled ? `
									<button class="wp-powersuite-advanced-settings wp-powersuite-advanced-settings--disabled" type="button" title="${this.escapeHtml('Enable module to access settings')}" disabled>
										<span class="dashicons dashicons-admin-generic"></span>
									</button>
								` : ''}
								<span class="wp-powersuite-module__status ${isEnabled ? 'is-enabled' : 'is-disabled'}">
									${isEnabled ? 'Enabled' : 'Disabled'}
								</span>
							</div>
						</div>
					</div>
				`;
			}).join('');
		},

		/**
		 * Render list view
		 */
		renderListView: function() {
			const hasSelected = this.selectedModules.length > 0;
			return `
				<div class="wp-powersuite-bulk-actions" style="${hasSelected ? '' : 'display: none;'}">
					<div class="wp-powersuite-bulk-actions__selected">
						${this.i18n('Selected:', 'wp-powersuite')} <strong>${this.selectedModules.length}</strong>
					</div>
					<div class="wp-powersuite-bulk-actions__buttons">
						<button type="button" class="button wp-powersuite-bulk-disable">${this.i18n('Disable Selected', 'wp-powersuite')}</button>
						<button type="button" class="button wp-powersuite-bulk-clear">${this.i18n('Clear Selection', 'wp-powersuite')}</button>
					</div>
				</div>
				<table class="wp-list-table widefat fixed striped">
					<thead>
						<tr>
							<td class="check-column">
								<input type="checkbox" id="wp-powersuite-select-all" class="wp-powersuite-select-all" />
							</td>
							<th class="column-name">${this.i18n('Name', 'wp-powersuite')}</th>
							${(this.descriptionMode === 'show' || this.descriptionMode === 'truncate') ? `<th class="column-description">${this.i18n('Description', 'wp-powersuite')}</th>` : ''}
							${this.showCategory ? `<th class="column-category">${this.i18n('Category', 'wp-powersuite')}</th>` : ''}
							<th class="column-actions">${this.i18n('Actions', 'wp-powersuite')}</th>
						</tr>
					</thead>
					<tbody>
						${this.filteredModules.map(module => {
							const isEnabled = this.enabled.includes(module.id);
							const isFavorite = this.favorites.includes(module.id);
							const isSelected = this.selectedModules.includes(module.id);
							const isPremiumLocked = this.isModuleLocked(module);
							const missingDependencies = this.getMissingDependencies(module);
							const conflictTooltip = this.renderConflictTooltip(module, isEnabled);
							const dependencyWarning = !isEnabled && missingDependencies.length > 0
								? ` <span class="wp-powersuite-module__dependency-inline"><span class="dashicons dashicons-admin-plugins"></span> ${this.escapeHtml(this.i18n('Requires', 'wp-powersuite'))}: ${missingDependencies.map(m => this.escapeHtml(m.name)).join(', ')}</span>`
								: '';
							
							return `
								<tr class="wp-powersuite-module wp-powersuite-module--list ${isSelected ? 'selected' : ''}" data-module-id="${this.escapeHtml(module.id)}">
									<th class="check-column">
										<input type="checkbox" class="wp-powersuite-module-checkbox" value="${this.escapeHtml(module.id)}" ${isSelected ? 'checked' : ''} />
									</th>
									<td class="column-name">
										<strong>${this.escapeHtml(module.name)}${this.renderModuleTierBadge(module)}</strong>${dependencyWarning}
									</td>
									${(this.descriptionMode === 'show' || this.descriptionMode === 'truncate') ? `<td class="column-description"><span class="wp-powersuite-list-description${this.descriptionMode === 'truncate' ? ' wp-powersuite-module__description--truncate' : ''}">${this.escapeHtml(module.description || '')}</span></td>` : ''}
									${this.showCategory ? `<td class="column-category">${this.escapeHtml(this.formatCategory(module.category))}</td>` : ''}
									<td class="column-actions">
										<div class="wp-powersuite-actions-wrapper">
											<div class="wp-powersuite-toggle-group">
												${isPremiumLocked && !isEnabled ? `
													<div class="wp-powersuite-toggle-wrapper wp-powersuite-toggle-wrapper--locked">
														<button class="wp-powersuite-toggle wp-powersuite-toggle--locked button button-secondary" type="button">
															${this.i18n('Enable', 'wp-powersuite')}
														</button>
														<span class="wp-powersuite-lock-icon" title="${this.escapeHtml(this.moduleLockTitle(module))}">
															<span class="dashicons dashicons-lock"></span>
														</span>
													</div>
												` : `
													<button class="wp-powersuite-toggle button ${isEnabled ? 'button-primary' : 'button-secondary'}" type="button">
														${isEnabled ? 'Disable' : 'Enable'}
													</button>
												`}
												${conflictTooltip}
											</div>
											${module.has_custom_page && isEnabled ? `
												<a href="${this.escapeHtml(module.custom_page_url || this.getCustomPageUrl(module.id))}" class="wp-powersuite-custom-page-link-icon" title="${this.escapeHtml('Open ' + module.name)}">
													<span class="dashicons dashicons-admin-links"></span>
												</a>
											` : ''}
											${module.has_advanced_settings && !module.has_custom_page && isEnabled ? `
												<button class="wp-powersuite-advanced-settings-icon" type="button" title="${this.escapeHtml('Advanced Settings')}" data-module-id="${this.escapeHtml(module.id)}">
													<span class="dashicons dashicons-admin-generic"></span>
												</button>
											` : ''}
											${module.has_advanced_settings && !module.has_custom_page && !isEnabled ? `
												<span class="wp-powersuite-advanced-settings-icon wp-powersuite-advanced-settings-icon--disabled" title="${this.escapeHtml('Enable module to access settings')}">
													<span class="dashicons dashicons-admin-generic"></span>
												</span>
											` : ''}
											${(this.descriptionMode === 'hide' || this.descriptionMode === 'truncate') ? `
											<span class="wp-powersuite-description-info-wrap">
												<button type="button" class="wp-powersuite-description-info" aria-label="${this.escapeHtml(this.i18n('View description', 'wp-powersuite'))}">
													<span class="dashicons dashicons-info" aria-hidden="true"></span>
												</button>
												<div class="wp-powersuite-description-hover-panel" role="tooltip">
													<p>${this.escapeHtml(module.description || this.i18n('No description available.', 'wp-powersuite'))}</p>
												</div>
											</span>
											` : ''}
											<button class="wp-powersuite-favorite ${isFavorite ? 'is-active' : ''}" type="button" title="${isFavorite ? 'Remove from favorites' : 'Add to favorites'}">
												<span class="dashicons dashicons-star-filled"></span>
											</button>
											</div>
									</td>
								</tr>
							`;
						}).join('')}
					</tbody>
				</table>
			`;
		},

		/**
		 * Toggle module enable/disable
		 */
		toggleModule: function(moduleId, forceEnable = null, silent = false) {
			const module = this.modules.find(m => m.id === moduleId);
			if (!module) {
				return Promise.reject(new Error('Module not found'));
			}

			const isEnabled = this.enabled.includes(moduleId);
			const enable = forceEnable !== null ? forceEnable : !isEnabled;

			// License check: block enabling premium modules when license invalid
			if (enable && this.isModuleLocked(module)) {
				const msg = this.moduleLockTitle(module);
				if (!silent) {
					this.openLicenseRequiredModal(this.licenseModalMode(module));
				}
				return Promise.reject(new Error(msg));
			}

			// Conflict check: block enabling if this module conflicts with already-enabled modules
			if ( enable ) {
				if ( module.enable_confirm && !silent ) {
					if ( !confirm(module.enable_confirm) ) {
						return Promise.reject(new Error('cancelled'));
					}
				}

				const conflicting = this.getConflictingEnabled(module);
				if (conflicting.length > 0) {
					const msg = (window.wpPowerSuite && window.wpPowerSuite.strings && window.wpPowerSuite.strings.conflictEnableWarning) || this.i18n('This module conflicts with enabled module(s). Disable them first to enable this one.', 'wp-powersuite');
					if (!silent) {
						this.showToast(msg, 4500);
					}
					return Promise.reject(new Error(msg));
				}
				const missingDependencies = this.getMissingDependencies(module);
				if (missingDependencies.length > 0) {
					const msg = this.i18n('Enable required module(s) first:', 'wp-powersuite') + ' ' + missingDependencies.map(m => m.name).join(', ');
					if (!silent) {
						this.showToast(msg, 4500);
					}
					return Promise.reject(new Error(msg));
				}
			}

			const button = !silent ? document.querySelector(`[data-module-id="${moduleId}"] .wp-powersuite-toggle`) : null;
			
			if (button) {
				button.disabled = true;
				button.textContent = enable ? this.i18n('Enabling...', 'wp-powersuite') : this.i18n('Disabling...', 'wp-powersuite');
			}

			// AJAX request
			const formData = new FormData();
			formData.append('action', window.wpPowerSuite.action.toggleModule);
			formData.append('nonce', window.wpPowerSuite.nonce);
			formData.append('module_id', moduleId);
			formData.append('enable', enable ? '1' : '0');

			return fetch(window.wpPowerSuite.ajaxUrl, {
				method: 'POST',
				body: formData
			})
			.then(response => response.json())
			.then(data => {
				if (data.success && data.data) {
					if (data.data.module_state) {
						// Use the complete state from server
						this.enabled = data.data.module_state.enabled_list || [];
						this.favorites = data.data.module_state.favorites_list || [];
					} else {
						// Fallback: update based on enabled flag
						if (data.data.enabled) {
							if (!this.enabled.includes(moduleId)) {
								this.enabled.push(moduleId);
							}
						} else {
							this.enabled = this.enabled.filter(id => id !== moduleId);
						}
					}

					if (!silent) {
						try {
							this.updateModuleUI(moduleId, enable);
							this.updateConflictIndicators();
						} catch (uiError) {
							// Enable/disable already saved — don't surface a DOM glitch as a toggle failure.
							console.error('WP PowerSuite: UI update error after toggle', uiError);
						}

						const module = this.modules.find(m => m.id === moduleId);
						const hasCustomPage = module && module.has_custom_page;

						if (!enable || (enable && !hasCustomPage)) {
							this.showRefreshButton();
						}
					}
					return data;
				} else {
					const errorMessage = data.data?.message || this.i18n('An error occurred. Please try again.', 'wp-powersuite');
					if (!silent) {
						this.showToast(errorMessage, 4500);
					}
					if (button) {
						button.disabled = false;
						button.textContent = isEnabled ? 'Disable' : 'Enable';
					}
					const handledError = new Error(errorMessage);
					handledError.wpPowerSuiteHandled = true;
					throw handledError;
				}
			})
			.catch(error => {
				console.error('WP PowerSuite: Toggle error', error);
				if (!silent && !error.wpPowerSuiteHandled) {
					alert(this.i18n('An error occurred. Please try again.', 'wp-powersuite'));
				}
				if (button) {
					button.disabled = false;
					button.textContent = isEnabled ? 'Disable' : 'Enable';
				}
				throw error;
			});
		},

		/**
		 * Queue a single toggle so rapid clicks are processed one at a time (avoids race errors).
		 */
		queueToggle: function(moduleId) {
			const module = this.modules.find(m => m.id === moduleId);
			if (!module) return;

			const isEnabled = this.enabled.includes(moduleId);
			const enable = !isEnabled;

			if (enable) {
				if (this.isModuleLocked(module)) {
					this.openLicenseRequiredModal(this.licenseModalMode(module));
					return;
				}
				const conflicting = this.getConflictingEnabled(module);
				if (conflicting.length > 0) {
					const msg = (window.wpPowerSuite && window.wpPowerSuite.strings && window.wpPowerSuite.strings.conflictEnableWarning) || this.i18n('This module conflicts with enabled module(s). Disable them first to enable this one.', 'wp-powersuite');
					this.showToast(msg, 4500);
					return;
				}
				const missingDependencies = this.getMissingDependencies(module);
				if (missingDependencies.length > 0) {
					this.showToast(this.i18n('Enable required module(s) first:', 'wp-powersuite') + ' ' + missingDependencies.map(m => m.name).join(', '), 4500);
					return;
				}
			}

			this.toggleQueue.push({ moduleId: moduleId });
			this.processToggleQueue();
		},

		/**
		 * Process toggle queue one request at a time.
		 */
		processToggleQueue: function() {
			if (this.toggleInProgress || this.toggleQueue.length === 0) return;

			this.toggleInProgress = true;
			const item = this.toggleQueue.shift();
			const moduleId = item.moduleId;
			const isEnabled = this.enabled.includes(moduleId);
			const enable = !isEnabled;

			this.toggleModule(moduleId, enable, false)
				.then(() => {
					this.toggleInProgress = false;
					this.processToggleQueue();
				})
				.catch(() => {
					this.toggleInProgress = false;
					this.processToggleQueue();
				});
		},

		/**
		 * Toggle favorite
		 */
		toggleFavorite: function(moduleId) {
			const isFavorite = this.favorites.includes(moduleId);
			const module = this.modules.find(m => m.id === moduleId);
			const moduleName = module ? module.name : '';

			// Optimistic update
			if (isFavorite) {
				this.favorites = this.favorites.filter(id => id !== moduleId);
			} else {
				this.favorites.push(moduleId);
				this.showToast(moduleName ? (this.i18n('Added to favorites', 'wp-powersuite') + ': ' + moduleName) : this.i18n('Added to favorites', 'wp-powersuite'));
			}
			this.renderModules();

			// AJAX request
			const formData = new FormData();
			formData.append('action', window.wpPowerSuite.action.toggleFavorite);
			formData.append('nonce', window.wpPowerSuite.nonce);
			formData.append('module_id', moduleId);
			formData.append('favorite', !isFavorite ? '1' : '0');

			fetch(window.wpPowerSuite.ajaxUrl, {
				method: 'POST',
				body: formData
			})
			.then(response => response.json())
			.then(data => {
				if (data.success && data.data) {
					if (data.data.favorites) {
						this.favorites = data.data.favorites || [];
					}
					// Re-render with updated state
					this.renderModules();
				} else {
					// Revert on error
					if (isFavorite) {
						this.favorites.push(moduleId);
					} else {
						this.favorites = this.favorites.filter(id => id !== moduleId);
					}
					this.renderModules();
					const errorMessage = data.data?.message || this.i18n('An error occurred. Please try again.', 'wp-powersuite');
					alert(errorMessage);
				}
			})
			.catch(error => {
				console.error('WP PowerSuite: Favorite toggle error', error);
				// Revert on error
				if (isFavorite) {
					this.favorites.push(moduleId);
				} else {
					this.favorites = this.favorites.filter(id => id !== moduleId);
				}
				this.renderModules();
			});
		},

		/**
		 * Set view mode
		 */
		setViewMode: function(mode) {
			if (this.isMobileLayout() && mode === 'list') {
				return;
			}

			this.viewMode = mode;

			this.syncViewModeClass();

			// Description mode icons visible for both grid and list
			this.updateDescriptionModeIcon();

			this.renderModules();
			
			const formData = new FormData();
			formData.append('action', 'wp_powersuite_save_view_mode');
			formData.append('nonce', window.wpPowerSuite.nonce);
			formData.append('view_mode', mode);

			fetch(window.wpPowerSuite.ajaxUrl, {
				method: 'POST',
				body: formData
			}).catch(error => {
				console.error('WP PowerSuite: Save view mode error', error);
			});
		},

		/**
		 * Save description mode via AJAX
		 */
		saveDescriptionMode: function(mode) {
			const formData = new FormData();
			formData.append('action', 'wp_powersuite_save_description_mode');
			formData.append('nonce', window.wpPowerSuite.nonce);
			formData.append('description_mode', mode);

			fetch(window.wpPowerSuite.ajaxUrl, {
				method: 'POST',
				body: formData
			}).catch(error => {
				console.error('WP PowerSuite: Save description mode error', error);
			});
		},

		/**
		 * Update the single description mode icon to match current mode
		 */
		updateDescriptionModeIcon: function() {
			const btn = document.querySelector('.wp-powersuite-description-mode-btn');
			const icon = document.querySelector('.wp-powersuite-description-mode-icon');
			if (!btn || !icon) return;

			const icons = { show: 'dashicons-visibility', truncate: 'dashicons-editor-contract', hide: 'dashicons-hidden' };
			const titles = {
				show: this.i18n('Show Description', 'wp-powersuite'),
				truncate: this.i18n('Truncate Description', 'wp-powersuite'),
				hide: this.i18n('Hide Description', 'wp-powersuite')
			};
			const mode = this.descriptionMode || 'show';
			icon.className = 'wp-powersuite-description-mode-icon dashicons ' + (icons[mode] || icons.show);
			btn.dataset.mode = mode;
			btn.title = titles[mode] || titles.show;
		},

		/**
		 * Show a toast message (e.g. when adding to favorites)
		 */
		showToast: function(message, duration) {
			duration = duration || 3000;
			let toastEl = document.getElementById('wp-powersuite-toast');
			if (toastEl) {
				toastEl.remove();
			}
			toastEl = document.createElement('div');
			toastEl.id = 'wp-powersuite-toast';
			toastEl.className = 'wp-powersuite-toast';
			toastEl.setAttribute('role', 'alert');
			toastEl.textContent = message;
			document.body.appendChild(toastEl);
			setTimeout(function() {
				if (toastEl && toastEl.parentNode) {
					toastEl.parentNode.removeChild(toastEl);
				}
			}, duration);
		},

		/**
		 * Keep All + Favorites first; then Security â†’ Login & Users â†’ Admin â†’ Content â†’ Media â†’ SEO & Visibility â†’ Speed â†’ Site Tools.
		 */
		reorderCategoryFilterButtons: function() {
			const wrap = document.querySelector('.wp-powersuite-admin__category-buttons');
			if (!wrap) {
				return;
			}
			const buttons = Array.from(wrap.querySelectorAll('.wp-powersuite-category-filter'));
			const byCat = {};
			buttons.forEach(function(btn) {
				byCat[btn.dataset.category || ''] = btn;
			});
			const order = ['', '__favorites'].concat(WPPS_CATEGORY_TAB_ORDER);
			const placed = new Set();
			order.forEach(function(cat) {
				if (byCat[cat]) {
					wrap.appendChild(byCat[cat]);
					placed.add(cat);
				}
			});
			buttons
				.filter(function(btn) {
					return !placed.has(btn.dataset.category || '');
				})
				.sort(function(a, b) {
					return (a.textContent || '').localeCompare(b.textContent || '');
				})
				.forEach(function(btn) {
					wrap.appendChild(btn);
				});
		},

		/**
		 * Update category button active state
		 */
		updateCategoryButtonState: function() {
			const category = this.filters.category || '';
			document.querySelectorAll('.wp-powersuite-category-filter').forEach(btn => {
				btn.classList.remove('is-active');
				const btnCategory = btn.dataset.category || '';
				const isActive = btnCategory === category;
				if (isActive) {
					btn.classList.add('is-active');
				}
				btn.setAttribute('aria-pressed', isActive ? 'true' : 'false');
			});
		},

		/**
		 * Show refresh button when changes are made
		 */
		showRefreshButton: function() {
			const refreshButton = document.querySelector('.wp-powersuite-refresh-page');
			if (refreshButton) {
				refreshButton.style.display = 'inline-flex';
			}
		},

		/**
		 * Hide refresh button
		 */
		hideRefreshButton: function() {
			const refreshButton = document.querySelector('.wp-powersuite-refresh-page');
			if (refreshButton) {
				refreshButton.style.display = 'none';
			}
		},

		/**
		 * Update only the specific module's UI without re-rendering everything
		 */
		updateModuleUI: function(moduleId, isEnabled) {
			const moduleElement = document.querySelector(`[data-module-id="${moduleId}"]`);
			if (!moduleElement) {
				return;
			}

			const module = this.modules.find(m => m.id === moduleId);
			const hasAdvancedSettings = module && module.has_advanced_settings && !module.has_custom_page;

			const toggleWrapper = moduleElement.querySelector('.wp-powersuite-toggle-wrapper--locked');
			const toggleButton = moduleElement.querySelector('.wp-powersuite-toggle');
			const isPremiumLocked = this.isModuleLocked(module);
			if (toggleButton) {
				if (isPremiumLocked && !isEnabled) {
					if (!toggleWrapper) {
						const newWrapper = document.createElement('div');
						newWrapper.className = 'wp-powersuite-toggle-wrapper wp-powersuite-toggle-wrapper--locked';
						newWrapper.innerHTML = '<button class="wp-powersuite-toggle wp-powersuite-toggle--locked button button-secondary" type="button">' + this.i18n('Enable', 'wp-powersuite') + '</button><span class="wp-powersuite-lock-icon" title="' + this.escapeHtml(this.moduleLockTitle(module)) + '"><span class="dashicons dashicons-lock"></span></span>';
						toggleButton.replaceWith(newWrapper);
					}
				} else {
					// Normal toggle button
					if (toggleWrapper) {
						const btn = toggleWrapper.querySelector('.wp-powersuite-toggle') || toggleButton;
						toggleWrapper.replaceWith(btn);
					}
					toggleButton.textContent = isEnabled ? this.i18n('Disable', 'wp-powersuite') : this.i18n('Enable', 'wp-powersuite');
					toggleButton.disabled = false;
					toggleButton.classList.remove('wp-powersuite-toggle--locked', 'button-primary', 'button-secondary');
					toggleButton.classList.add(isEnabled ? 'button-primary' : 'button-secondary');
				}
			}

			const statusElement = moduleElement.querySelector('.wp-powersuite-module__status');
			if (statusElement) {
				statusElement.textContent = isEnabled ? this.i18n('Enabled', 'wp-powersuite') : this.i18n('Disabled', 'wp-powersuite');
				statusElement.classList.remove('is-enabled', 'is-disabled');
				statusElement.classList.add(isEnabled ? 'is-enabled' : 'is-disabled');
			}

			const settingsButton = moduleElement.querySelector('.wp-powersuite-advanced-settings');
			const settingsButtonDisabled = moduleElement.querySelector('.wp-powersuite-advanced-settings--disabled');
			
			if (hasAdvancedSettings) {
				if (isEnabled) {
					if (settingsButtonDisabled) {
						const newButton = document.createElement('button');
						newButton.className = 'wp-powersuite-advanced-settings';
						newButton.type = 'button';
						newButton.title = this.i18n('Advanced Settings', 'wp-powersuite');
						newButton.setAttribute('data-module-id', moduleId);
						newButton.innerHTML = '<span class="dashicons dashicons-admin-generic"></span>';
						settingsButtonDisabled.replaceWith(newButton);
					} else if (settingsButton) {
						settingsButton.disabled = false;
						settingsButton.classList.remove('wp-powersuite-advanced-settings--disabled');
						settingsButton.title = this.i18n('Advanced Settings', 'wp-powersuite');
					}
				} else if (settingsButton) {
					const newButton = document.createElement('button');
					newButton.className = 'wp-powersuite-advanced-settings wp-powersuite-advanced-settings--disabled';
					newButton.type = 'button';
					newButton.title = this.i18n('Enable module to access settings', 'wp-powersuite');
					newButton.disabled = true;
					newButton.innerHTML = '<span class="dashicons dashicons-admin-generic"></span>';
					settingsButton.replaceWith(newButton);
				} else if (settingsButtonDisabled) {
					settingsButtonDisabled.disabled = true;
				}
			}

			const settingsIcon = moduleElement.querySelector('.wp-powersuite-advanced-settings-icon');
			const settingsIconDisabled = moduleElement.querySelector('.wp-powersuite-advanced-settings-icon--disabled');
			
			if (hasAdvancedSettings) {
				if (isEnabled) {
					if (settingsIconDisabled) {
						const newIcon = document.createElement('button');
						newIcon.className = 'wp-powersuite-advanced-settings-icon';
						newIcon.type = 'button';
						newIcon.title = this.i18n('Advanced Settings', 'wp-powersuite');
						newIcon.setAttribute('data-module-id', moduleId);
						newIcon.innerHTML = '<span class="dashicons dashicons-admin-generic"></span>';
						settingsIconDisabled.replaceWith(newIcon);
					} else if (settingsIcon) {
						settingsIcon.classList.remove('wp-powersuite-advanced-settings-icon--disabled');
						settingsIcon.title = this.i18n('Advanced Settings', 'wp-powersuite');
					}
				} else if (settingsIcon) {
					const newIcon = document.createElement('span');
					newIcon.className = 'wp-powersuite-advanced-settings-icon wp-powersuite-advanced-settings-icon--disabled';
					newIcon.title = this.i18n('Enable module to access settings', 'wp-powersuite');
					newIcon.innerHTML = '<span class="dashicons dashicons-admin-generic"></span>';
					settingsIcon.replaceWith(newIcon);
				} else if (settingsIconDisabled) {
					settingsIconDisabled.classList.add('wp-powersuite-advanced-settings-icon--disabled');
				}
			}

			const hasCustomPage = module && module.has_custom_page;
			if (hasCustomPage) {
				const linkClass = moduleElement.classList.contains('wp-powersuite-module--list')
					? 'wp-powersuite-custom-page-link-icon'
					: 'wp-powersuite-custom-page-link';
				const existingLink = moduleElement.querySelector('.' + linkClass);
				const url = (module && module.custom_page_url) || this.getCustomPageUrl(moduleId);
				const title = this.escapeHtml('Open ' + (module.name || ''));

				if (module && url !== '#') {
					module.custom_page_url = url;
				}
				if (isEnabled) {
					if (url !== '#') {
						if (existingLink) {
							existingLink.href = url;
							existingLink.title = title;
							existingLink.style.pointerEvents = '';
						} else {
							const a = document.createElement('a');
							a.href = url;
							a.className = linkClass;
							a.title = title;
							a.innerHTML = '<span class="dashicons dashicons-admin-links"></span>';

							if (moduleElement.classList.contains('wp-powersuite-module--list')) {
								// Toggle lives inside .wp-powersuite-toggle-group, so insert after that
								// group — not via toggle.nextSibling (which is not a child of wrapper).
								const wrapper = moduleElement.querySelector('.wp-powersuite-actions-wrapper');
								const toggleGroup = wrapper && wrapper.querySelector('.wp-powersuite-toggle-group');
								if (toggleGroup) {
									toggleGroup.after(a);
								} else if (wrapper) {
									wrapper.insertBefore(a, wrapper.firstChild);
								}
							} else {
								const footerRight = moduleElement.querySelector('.wp-powersuite-module__footer-right');
								if (footerRight) {
									footerRight.insertBefore(a, footerRight.firstChild);
								}
							}
						}
					}
				} else if (existingLink) {
					existingLink.remove();
				}
			}
		},


		/**
		 * Set category filter from button
		 */
		setCategoryFilter: function(category) {
			this.filters.category = category;

			const url = new URL(window.location);
			if (category) {
				url.searchParams.set('category', category);
			} else {
				url.searchParams.delete('category');
			}
			window.history.pushState({ category: category }, '', url);

			this.showCategory = !category || category === '' || category === '__favorites';

			this.updateCategoryButtonState();

			this.applyFilters();
		},

		/**
		 * Update grid columns CSS
		 */
		updateGridColumns: function() {
			const container = this.getModulesListEl();
			if (container && this.getEffectiveViewMode() === 'grid') {
				container.style.gridTemplateColumns = 'repeat(' + this.getEffectiveGridColumns() + ', 1fr)';
			}
		},

		bindMobileLayoutListener: function() {
			const mq = window.matchMedia('(max-width: ' + this.mobileBreakpoint + 'px)');
			const onChange = () => {
				this.syncViewModeClass();
				this.updateGridColumns();
				this.renderModules();
			};

			if (typeof mq.addEventListener === 'function') {
				mq.addEventListener('change', onChange);
			} else if (typeof mq.addListener === 'function') {
				mq.addListener(onChange);
			}
		},

		/**
		 * Initialize layout controls
		 */
		initializeLayoutControls: function() {
			if (this.getEffectiveViewMode() === 'grid') {
				this.updateGridColumns();
			}
		},

		/**
		 * Update view toggle active state
		 */
		updateViewToggle: function() {
			const effectiveViewMode = this.getEffectiveViewMode();
			const toggles = document.querySelectorAll('.wp-powersuite-view-toggle');
			toggles.forEach(toggle => {
				const isActive = toggle.dataset.view === effectiveViewMode;
				toggle.classList.toggle('is-active', isActive);
				toggle.setAttribute('aria-pressed', isActive ? 'true' : 'false');
			});
		},

		/**
		 * Format category name
		 */
		formatCategory: function(category) {
			if (!category) return '';
			// If category already contains a space, preserve it and just capitalize words
			if (category.indexOf(' ') !== -1) {
				return category.replace(/\b\w/g, l => l.toUpperCase());
			}
			// Otherwise, replace hyphens with spaces and capitalize
			return category.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
		},

		/**
		 * Get Custom Page URL
		 */
		getCustomPageUrl: function(moduleId) {
			var pageMap = {
				'code-snippet': 'powersuite-code-snippet',
				'customizer-reset': 'themes.php?page=wpps-customizer-reset',
				'admin-columns-customizer': 'powersuite-admin-columns-customizer',
				'admin-color-customizer': 'powersuite-admin-color-customizer',
				'admin-menu-organizer': 'powersuite-admin-menu-organizer',
				'temporary-login': 'powersuite-temporary-login',
				'magic-login': 'powersuite-magic-login',
				'options-page-builder': 'powersuite-options-page-builder',
				'basic-forms': 'powersuite-basic-forms',
				'ai-alt-text-generator': 'powersuite-ai-alt-text',
				'coming-soon-maintenance': 'powersuite-coming-soon-maintenance',
				'redirect-manager': 'powersuite-redirect-manager',
				'backup-restore': 'powersuite-backup-restore',
				'database-cleanup': 'powersuite-database-cleanup',
				'login-page-customizor': 'powersuite-login-page-customizor',
			};
			
			if (pageMap[moduleId]) {
				var page = pageMap[moduleId];
				if (page.indexOf('?') !== -1) {
					return ajaxurl.replace('admin-ajax.php', page);
				}
				return ajaxurl.replace('admin-ajax.php', 'admin.php?page=' + page);
			}
			return '#';
		},

		/**
		 * Escape HTML
		 */
		escapeHtml: function(text) {
			const div = document.createElement('div');
			div.textContent = text;
			return div.innerHTML;
		},

		/**
		 * Notice block with plain-text message (avoids XSS from AJAX error strings).
		 */
		createNoticeElement: function(classNames, message) {
			const notice = document.createElement('div');
			notice.className = classNames;
			notice.setAttribute('data-wpps-plugin', 'true');
			const p = document.createElement('p');
			p.textContent = message == null ? '' : String(message);
			notice.appendChild(p);
			return notice;
		},

		/**
		 * Show WordPress admin spinner + label on a button while an async action runs.
		 */
		setButtonBusy: function(btn, busy, loadingLabel) {
			if (!btn) {
				return;
			}
			if (!btn.dataset.wppsDefaultLabel) {
				btn.dataset.wppsDefaultLabel = (btn.textContent || '').trim();
			}
			btn.disabled = !!busy;
			btn.classList.toggle('is-busy', !!busy);
			btn.setAttribute('aria-busy', busy ? 'true' : 'false');
			if (busy) {
				const label = loadingLabel || (this.i18n('Loading...', 'wp-powersuite') || 'Loading...');
				btn.innerHTML = '<span class="spinner is-active" aria-hidden="true"></span><span>' + label + '</span>';
			} else {
				btn.textContent = btn.dataset.wppsDefaultLabel || '';
			}
		},

		fetchJsonResponse: function(formData) {
			return fetch(ajaxurl, { method: 'POST', body: formData }).then(function(r) {
				return r.text().then(function(text) {
					try {
						return JSON.parse(text);
					} catch (e) {
						throw new Error(text.trim().substring(0, 200) || 'Invalid server response.');
					}
				});
			});
		},

		/**
		 * Keep container class in sync with grid vs list (for CSS that shows grid column control).
		 */
		syncViewModeClass: function() {
			const container = document.querySelector('.wp-powersuite-admin');
			if (!container) {
				return;
			}
			const effectiveViewMode = this.getEffectiveViewMode();
			container.classList.remove('wp-powersuite-admin--view-grid', 'wp-powersuite-admin--view-list');
			container.classList.add(effectiveViewMode === 'list' ? 'wp-powersuite-admin--view-list' : 'wp-powersuite-admin--view-grid');
			container.classList.toggle('wp-powersuite-admin--mobile', this.isMobileLayout());
		},

		/**
		 * Simple i18n placeholder
		 */
		i18n: function(text, domain) {
			return window.wpPowerSuite?.strings?.[text.toLowerCase().replace(/\s+/g, '')] || text;
		},

		/**
		 * Bind modal events
		 */
		bindModalEvents: function() {
			const self = this;

			document.addEventListener('click', function(e) {
				const btn = e.target.closest('.wp-powersuite-advanced-settings, .wp-powersuite-advanced-settings-icon');
				if (btn) {
					e.preventDefault();
					const moduleId = btn.dataset.moduleId;
					if (moduleId) {
						self.openAdvancedSettings(moduleId);
					}
				}
			});

			const modal = document.getElementById('wp-powersuite-modal');
			if (modal) {
				// Turnstile accordion toggles (same pattern as Code Snippet).
				modal.addEventListener('click', function(tsEv) {
					const tsHeader = tsEv.target.closest('.wpps-ts-accordion-header');
					if (!tsHeader || !modal.contains(tsHeader)) {
						return;
					}
					tsEv.preventDefault();
					const tsAcc = tsHeader.closest('.wpps-ts-accordion');
					if (!tsAcc) {
						return;
					}
					const tsOpen = tsAcc.classList.toggle('is-open');
					tsHeader.setAttribute('aria-expanded', tsOpen ? 'true' : 'false');
				});

				modal.addEventListener('keydown', function(tsEv) {
					if (tsEv.key !== 'Enter' && tsEv.key !== ' ') {
						return;
					}
					const tsHeader = tsEv.target.closest('.wpps-ts-accordion-header');
					if (!tsHeader || !modal.contains(tsHeader)) {
						return;
					}
					tsEv.preventDefault();
					const tsAcc = tsHeader.closest('.wpps-ts-accordion');
					if (!tsAcc) {
						return;
					}
					const tsOpen = tsAcc.classList.toggle('is-open');
					tsHeader.setAttribute('aria-expanded', tsOpen ? 'true' : 'false');
				});

				var nestedModalIds = '#tl-login-modal, #tl-info-modal, #tl-confirm-modal, #tl-logs-modal';
				modal.addEventListener('click', function(e) {
					if (e.target.closest(nestedModalIds)) return;
					if (e.target.closest('.wp-powersuite-modal__close') ||
					    e.target.closest('.wp-powersuite-modal__cancel')) {
						self.closeAdvancedSettings();
					}
				});

				modal.addEventListener('click', function(e) {
					if (e.target.closest(nestedModalIds)) return;
					if (e.target === modal || e.target.classList.contains('wp-powersuite-modal__overlay')) {
						self.closeAdvancedSettings();
					}
				});

				const saveBtn = modal.querySelector('.wp-powersuite-modal__save');
				if (saveBtn) {
					saveBtn.addEventListener('click', function() {
						self.saveAdvancedSettings();
					});
				}

				document.addEventListener('keydown', function(e) {
					if (e.key === 'Escape' && modal.style.display !== 'none') {
						self.closeAdvancedSettings();
					}
				});
			}
		},

		/**
		 * Bind license required modal events
		 */
		bindLicenseModalEvents: function() {
			const modal = document.getElementById('wp-powersuite-license-modal');
			if (!modal) return;

			const self = this;
			const closeModal = () => self.closeLicenseRequiredModal();

			modal.querySelectorAll('.wp-powersuite-license-modal__close').forEach(btn => {
				btn.addEventListener('click', closeModal);
			});

			modal.querySelector('.wp-powersuite-modal__overlay')?.addEventListener('click', closeModal);

			document.addEventListener('keydown', function escHandler(e) {
				if (e.key === 'Escape' && modal.style.display === 'flex') {
					closeModal();
				}
			});
		},

		/**
		 * Open license required modal
		 */
		openLicenseRequiredModal: function(mode) {
			const modal = document.getElementById('wp-powersuite-license-modal');
			if (!modal) return;

			const addonMode = mode === 'pro-addon' || !this.canActivateLicense();
			const licenseMsg = modal.querySelector('[data-role="license-msg"]');
			const addonMsg = modal.querySelector('[data-role="pro-addon-msg"]');
			const licenseCta = modal.querySelector('[data-role="license-cta"]');
			const addonCta = modal.querySelector('[data-role="pro-addon-cta"]');
			const title = modal.querySelector('#wp-powersuite-license-modal-title');
			const url = (window.wpPowerSuite && window.wpPowerSuite.licensePageUrl) || this.licensePageUrl;
			const productUrl = (window.wpPowerSuite && window.wpPowerSuite.productUrl) || '';

			if (licenseMsg) {
				licenseMsg.hidden = addonMode;
			}
			if (addonMsg) {
				addonMsg.hidden = !addonMode;
			}
			if (licenseCta) {
				licenseCta.hidden = addonMode;
				if (url) {
					licenseCta.href = url;
				}
			}
			if (addonCta) {
				addonCta.hidden = !addonMode;
				if (productUrl) {
					addonCta.href = productUrl;
				}
			}
			if (title) {
				const icon = title.querySelector('.dashicons');
				title.textContent = '';
				if (icon) {
					title.appendChild(icon);
					title.appendChild(document.createTextNode(' '));
				}
				title.appendChild(document.createTextNode(
					addonMode
						? (this.i18n('WP PowerSuite Pro required', 'wp-powersuite') || 'WP PowerSuite Pro required')
						: (this.i18n('License Required', 'wp-powersuite') || 'License Required')
				));
			}

			modal.style.display = 'flex';
			document.body.classList.add('wp-powersuite-modal-open');
			this.activateModalAccessibility(modal);
		},

		/**
		 * Close license required modal
		 */
		closeLicenseRequiredModal: function() {
			const modal = document.getElementById('wp-powersuite-license-modal');
			if (modal) {
				this.deactivateModalAccessibility();
				modal.style.display = 'none';
				document.body.classList.remove('wp-powersuite-modal-open');
			}
		},

		/**
		 * Open advanced settings modal
		 */
		openAdvancedSettings: function(moduleId) {
			const modal = document.getElementById('wp-powersuite-modal');
			const module = this.modules.find(m => m.id === moduleId);
			
			if (!modal || !module) return;

			const isEnabled = this.enabled.includes(moduleId);
			if (!isEnabled) {
				this.showToast(this.i18n('Please enable the module first to access its settings.', 'wp-powersuite'));
				return;
			}

			this.savedScrollPosition = window.scrollY || document.documentElement.scrollTop;

			modal.dataset.currentModuleId = moduleId;

			const modalRefreshBtn = modal.querySelector('.wp-powersuite-modal__refresh');
			if (modalRefreshBtn) {
				modalRefreshBtn.style.display = 'none';
			}

			const moduleNameEl = modal.querySelector('.wp-powersuite-modal__module-name');
			if (moduleNameEl) {
				moduleNameEl.innerHTML = ': ' + this.escapeHtml(module.name) + this.renderModuleTierBadge(module, { modal: true });
			}

			modal.style.display = 'flex';
			document.body.classList.add('wp-powersuite-modal-open');
			document.body.style.overflow = 'hidden';
			this.activateModalAccessibility(modal);

			this.loadAdvancedSettings(moduleId);
		},

		/**
		 * Close advanced settings modal
		 */
		closeAdvancedSettings: function() {
			this.customCssCodeEditor = null;
			this.customAdminCssEditors = null;
			this.customAdminCssDirty = false;
			this.headerFooterCodeEditors = null;
			this.wpConfigEditor = null;
			this.wpConfigEditorDirty = false;
			this.robotsTxtEditor = null;
			this.adsTxtEditor = null;
			const modal = document.getElementById('wp-powersuite-modal');
			const scrollY = this.savedScrollPosition;
			this.savedScrollPosition = null;
			if (modal) {
				this.hideHfcModalThemeToggle(modal);
				this.deactivateModalAccessibility();
				const selfClose = this;
				modal.querySelectorAll('.wpps-modal-tooltip.is-active').forEach(function(root) {
					root.classList.remove('is-active', 'wpps-modal-tooltip--panel-ready');
					selfClose.clearModalTooltipPanelStyles(root);
				});
				modal.style.display = 'none';
				modal.classList.remove('wp-powersuite-modal--dropdown-open');
				modal.querySelectorAll('.wpps-ct-multiselect[data-wpps-multiselect-initialized="1"]').forEach(function(root) {
					if (typeof root._wppsMultiselectClose === 'function') {
						root._wppsMultiselectClose();
					}
				});
				document.body.classList.remove('wp-powersuite-modal-open');
				document.body.style.overflow = '';
				const settings = modal.querySelector('.wp-powersuite-modal__settings');
				if (settings) {
					settings.style.display = 'none';
				}
				delete modal.dataset.currentModuleId;
			}
			// Restore scroll position after closing so user stays at same place (smooth)
			if (scrollY != null && scrollY > 0) {
				window.requestAnimationFrame(function() {
					window.scrollTo({ top: scrollY, behavior: 'smooth' });
				});
			}
		},

		/**
		 * Remove inline placement from a modal help tooltip (fixed panel).
		 */
		clearModalTooltipPanelStyles: function(root) {
			if (!root) {
				return;
			}
			const panel = root.querySelector('.wpps-modal-tooltip__panel');
			if (!panel) {
				return;
			}
			panel.classList.remove('wpps-modal-tooltip__panel--above');
			panel.style.left = '';
			panel.style.top = '';
			panel.style.width = '';
			panel.style.maxWidth = '';
			panel.style.removeProperty('--wpps-tip-x');
		},

		/**
		 * Scroll/resize: keep fixed tooltip panels aligned with triggers.
		 */
		ensureModalTooltipViewportListeners: function(modal) {
			if (!modal || modal.dataset.wppsModalTooltipViewport === '1') {
				return;
			}
			modal.dataset.wppsModalTooltipViewport = '1';
			const self = this;
			const reposition = function() {
				modal.querySelectorAll('.wpps-modal-tooltip.is-active').forEach(function(root) {
					self.positionModalTooltipPanel(root);
				});
			};
			const bodyEl = modal.querySelector('.wp-powersuite-modal__body');
			if (bodyEl) {
				bodyEl.addEventListener('scroll', reposition, { passive: true });
			}
			window.addEventListener('resize', reposition);
		},

		/**
		 * Place `.wpps-modal-tooltip__panel` in viewport coordinates (fixed) and clamp to the screen.
		 */
		positionModalTooltipPanel: function(root) {
			if (!root || !root.classList.contains('is-active')) {
				return;
			}
			const trigger = root.querySelector('.wpps-modal-tooltip__trigger');
			const panel = root.querySelector('.wpps-modal-tooltip__panel');
			if (!trigger || !panel) {
				return;
			}
			const tr = trigger.getBoundingClientRect();
			const margin = 10;
			const gapBelow = 10;
			const gapAbove = 18;
			const vw = window.innerWidth;
			const vh = window.innerHeight;
			const maxW = Math.max(160, Math.min(320, Math.floor(vw - margin * 2)));
			panel.style.boxSizing = 'border-box';
			panel.style.width = maxW + 'px';
			panel.style.maxWidth = maxW + 'px';

			const ph = panel.offsetHeight || panel.scrollHeight || 80;
			const spaceBelow = vh - tr.bottom - margin;
			const spaceAbove = tr.top - margin;
			// Prefer above when it fits so the panel does not cover fields under the row (dense module forms).
			// Only open downward when above is too tight, or when below has room and above does not.
			let placeBelow;
			if (spaceAbove >= ph) {
				placeBelow = false;
			} else if (spaceBelow >= ph) {
				placeBelow = true;
			} else {
				placeBelow = spaceBelow >= spaceAbove;
			}

			let top;
			if (placeBelow) {
				top = tr.bottom + gapBelow;
				panel.classList.remove('wpps-modal-tooltip__panel--above');
			} else {
				top = tr.top - gapAbove - ph;
				panel.classList.add('wpps-modal-tooltip__panel--above');
			}

			let left = tr.left + tr.width / 2 - maxW / 2;
			left = Math.max(margin, Math.min(left, vw - margin - maxW));
			top = Math.max(margin, Math.min(top, vh - margin - ph));

			panel.style.left = Math.round(left) + 'px';
			panel.style.top = Math.round(top) + 'px';

			const tipRaw = tr.left + tr.width / 2 - left;
			const tipClamped = Math.max(18, Math.min(maxW - 18, tipRaw));
			panel.style.setProperty('--wpps-tip-x', Math.round(tipClamped) + 'px');
		},

		/**
		 * Hover/focus driven help bubbles inside #wp-powersuite-modal (overflow-safe fixed layer).
		 */
		bindModalTooltipRoots: function(container) {
			const modal = document.getElementById('wp-powersuite-modal');
			if (!modal || !container || !modal.contains(container)) {
				return;
			}
			this.ensureModalTooltipViewportListeners(modal);
			const self = this;
			container.querySelectorAll('.wpps-modal-tooltip').forEach(function(root) {
				if (root.dataset.wppsTooltipJsBound === '1') {
					return;
				}
				root.dataset.wppsTooltipJsBound = '1';
				let hideTimer = null;
				const panel = root.querySelector('.wpps-modal-tooltip__panel');
				const cancelHide = function() {
					if (hideTimer) {
						clearTimeout(hideTimer);
						hideTimer = null;
					}
				};
				const show = function() {
					cancelHide();
					modal.querySelectorAll('.wpps-modal-tooltip.is-active').forEach(function(other) {
						if (other !== root) {
							other.classList.remove('is-active', 'wpps-modal-tooltip--panel-ready');
							self.clearModalTooltipPanelStyles(other);
						}
					});
					root.classList.add('is-active');
					root.classList.remove('wpps-modal-tooltip--panel-ready');
					window.requestAnimationFrame(function() {
						self.positionModalTooltipPanel(root);
						root.classList.add('wpps-modal-tooltip--panel-ready');
					});
				};
				const hide = function() {
					cancelHide();
					hideTimer = window.setTimeout(function() {
						root.classList.remove('wpps-modal-tooltip--panel-ready');
						root.classList.remove('is-active');
						self.clearModalTooltipPanelStyles(root);
						hideTimer = null;
					}, 220);
				};
				root.addEventListener('mouseenter', show);
				root.addEventListener('mouseleave', function(e) {
					if (e.relatedTarget && root.contains(e.relatedTarget)) {
						return;
					}
					hide();
				});
				if (panel) {
					panel.addEventListener('mouseenter', cancelHide);
					panel.addEventListener('mouseleave', hide);
				}
				const trig = root.querySelector('.wpps-modal-tooltip__trigger');
				if (trig) {
					trig.addEventListener('focus', show);
					trig.addEventListener('blur', function(ev) {
						if (!ev.relatedTarget || !root.contains(ev.relatedTarget)) {
							hide();
						}
					});
				}
			});
		},

		/**
		 * Load advanced settings via AJAX (with caching)
		 */
		loadAdvancedSettings: function(moduleId, forceRefresh = false) {
			this.customCssCodeEditor = null;
			this.customAdminCssEditors = null;
			this.customAdminCssDirty = false;
			this.headerFooterCodeEditors = null;
			const modal = document.getElementById('wp-powersuite-modal');
			const loading = modal.querySelector('.wp-powersuite-modal__loading');
			const settings = modal.querySelector('.wp-powersuite-modal__settings');

			if (!forceRefresh && this.settingsCache[moduleId]) {
				loading.style.display = 'none';
				this.renderSettingsHTML(moduleId, this.settingsCache[moduleId].html, settings);
				return;
			}

			loading.style.display = 'block';
			settings.style.display = 'none';

			const formData = new FormData();
			formData.append('action', 'wp_powersuite_load_module_settings');
			formData.append('module_id', moduleId);
			formData.append('nonce', window.wpPowerSuite.nonce);

			fetch(window.wpPowerSuite.ajaxUrl, { method: 'POST', body: formData })
			.then(r => r.text())
			.then(text => {
				let data;
				try {
					data = JSON.parse(text);
				} catch (e) {
					const preview = text.trim().substring(0, 80).replace(/</g, '&lt;');
					throw new Error('Server returned an error instead of JSON. Check your PHP error log. Response: ' + preview + (text.length > 80 ? 'Ã¢â‚¬Â¦' : ''));
				}
				return data;
			})
			.then(data => {
				loading.style.display = 'none';
				if (data.success && data.data && data.data.html) {
					this.settingsCache[moduleId] = { html: data.data.html, timestamp: Date.now() };
					this.renderSettingsHTML(moduleId, data.data.html, settings);
				} else {
					delete this.settingsCache[moduleId];
					settings.innerHTML = '';
					const errNotice = this.createNoticeElement('notice notice-error', data.data?.message || 'Failed to load');
					settings.appendChild(errNotice);
					settings.style.display = 'block';
				}
			})
			.catch(err => {
				delete this.settingsCache[moduleId];
				loading.style.display = 'none';
				settings.innerHTML = '';
				const errNotice = this.createNoticeElement('notice notice-error', err.message || 'Request failed');
				settings.appendChild(errNotice);
				settings.style.display = 'block';
			});
		},

		/**
		 * Render cached settings HTML
		 */
		renderCachedSettings: function(moduleId, settings, loading) {
			const cached = this.settingsCache[moduleId];
			if (!cached) return;

			loading.style.display = 'none';
			this.renderSettingsHTML(moduleId, cached.html, settings);
		},

		/**
		 * Render settings HTML (extract scripts, insert HTML, initialize form)
		 */
		renderSettingsHTML: function(moduleId, html, settings) {
			const tempDiv = document.createElement('div');
			tempDiv.innerHTML = html;
			
			const scripts = tempDiv.querySelectorAll('script');
			const scriptsToExecute = [];
			
			scripts.forEach(script => {
				scriptsToExecute.push({
					text: script.textContent || script.innerText,
					type: script.type || 'text/javascript'
				});
				script.remove(); // Remove script from temp div
			});
			
			settings.innerHTML = tempDiv.innerHTML;
			settings.removeAttribute('data-wpps-robots-init');
			settings.removeAttribute('data-wpps-ads-init');
			
			const submitSections = settings.querySelectorAll('p.submit, .submit');
			submitSections.forEach(section => {
				section.remove();
			});
			
			this.initializeAdvancedSettingsForm(settings);
			
			scriptsToExecute.forEach(scriptData => {
				try {
					if (scriptData.type === 'text/javascript' || scriptData.type === 'application/javascript' || !scriptData.type) {
						const script = document.createElement('script');
						script.type = 'text/javascript';
						script.textContent = scriptData.text;
						document.body.appendChild(script);
						document.body.removeChild(script); // Remove after execution
					}
				} catch (error) {
					console.error('WP PowerSuite: Error executing module script', error);
				}
			});
			
			// Trigger custom event for modules that need to initialize scripts after modal load
			const event = new CustomEvent('wp-powersuite-modal-loaded', {
				detail: { moduleId: moduleId }
			});
			document.dispatchEvent(event);
			
			this.initRoleMultiselects(settings);
			this.bindModalTooltipRoots(settings);
			
			settings.style.display = 'block';
			if (moduleId === 'custom-admin-css') {
				this.initCustomAdminCssEditors(settings);
			}
			if (moduleId === 'login-page-customizor') {
				this.initCustomAdminCssEditor(settings);
			}
			if (moduleId === 'header-footer-code') {
				this.initHeaderFooterCodeEditors(settings);
			}
			if (moduleId === 'quick-debug-toggle') {
				this.initQuickDebugToggle(settings);
			}
			if (moduleId === 'wp-config-editor') {
				this.initWpConfigEditor(settings);
			}
			if (moduleId === 'robots-txt-manager') {
				this.initRobotsTxtManager(settings);
			}
			if (moduleId === 'ads-txt-manager') {
				this.initAdsTxtManager(settings);
			}
			if (moduleId === 'coming-soon-maintenance') {
				this.initComingSoonWysiwyg(settings);
			}
			// Regenerate Thumbnails: footer button triggers Start; Download Media Library: no save
			const modalEl = document.getElementById('wp-powersuite-modal');
			const saveBtn = modalEl ? modalEl.querySelector('.wp-powersuite-modal__save') : null;
			if (saveBtn) {
				if (moduleId === 'download-media-library') {
					saveBtn.style.display = 'none';
				} else {
					saveBtn.style.display = '';
					if (moduleId === 'regenerate-thumbnails') {
						saveBtn.textContent = this.i18n('Regenerate Thumbnails', 'wp-powersuite') || 'Regenerate Thumbnails';
					} else {
						saveBtn.textContent = this.i18n('Save Settings', 'wp-powersuite') || 'Save Settings';
					}
				}
			}
			if (moduleId !== 'header-footer-code' && moduleId !== 'custom-admin-css') {
				this.hideHfcModalThemeToggle(modalEl);
			}

		},

		/**
		 * Mark WYSIWYG textareas for Coming Soon & Maintenance so form collector finds them.
		 */
		initComingSoonWysiwyg: function(settings) {
			if (!settings) return;
			const mid = 'coming-soon-maintenance';
			['heading', 'description'].forEach(function(key) {
				const ta = settings.querySelector('#' + mid + '_' + key);
				if (ta && !ta.classList.contains('wp-powersuite-advanced-setting')) {
					ta.classList.add('wp-powersuite-advanced-setting');
					ta.setAttribute('data-setting-key', key);
				}
			});
		},

		/**
		 * Initialize code editors for Custom Admin CSS module (Code Snippetâ€“style).
		 */
		initCustomAdminCssEditors: function(settings) {
			if (!settings || !window.wp || !window.wp.codeEditor) return;
			this.customAdminCssEditors = [];
			this.customAdminCssDirty = false;
			const self = this;

			this.ensureCodeEditorUnloadWarning();

			window.requestAnimationFrame(function() {
				self.customAdminCssEditors = self.initPowerSuiteCodeEditors(settings, {
					mode: 'css',
					onDirty: function() {
						self.customAdminCssDirty = true;
					},
					onSave: function(ta) {
						self.triggerHeaderFooterSave(ta);
					}
				});
				const modal = document.getElementById('wp-powersuite-modal');
				self.setupHfcModalThemeToggle(modal, settings, self.customAdminCssEditors);
			});
		},

		/**
		 * Initialize code editor for login custom CSS and other legacy CSS textareas.
		 */
		initCustomAdminCssEditor: function(settings) {
			if (!settings || !window.wp || !window.wp.codeEditor) {
				return;
			}

			const ta = settings.querySelector('textarea.wp-powersuite-css-editor[data-setting-key="custom_css"]')
				|| settings.querySelector('#login-page-customizor_custom_css');
			if (!ta || (ta.nextElementSibling && ta.nextElementSibling.classList.contains('CodeMirror'))) {
				return;
			}

			const self = this;
			window.requestAnimationFrame(function() {
				try {
					self.customCssCodeEditor = window.wp.codeEditor.initialize(ta, {
						codemirror: {
							mode: 'css',
							lineNumbers: true,
							lineWrapping: true,
							styleActiveLine: true,
							autoRefresh: true,
							indentUnit: 2,
							tabSize: 2,
							indentWithTabs: false,
							matchBrackets: true,
							autoCloseBrackets: true
						}
					});
				} catch (e) {
					if (window.console && typeof window.console.warn === 'function') {
						window.console.warn('WP PowerSuite: Custom Admin CSS editor failed to initialize.', e);
					}
				}
			});
		},

		/**
		 * Shared Code Snippetâ€“style editor init (used by Header & Footer Code and other modules).
		 *
		 * @param {Element} container Root element containing .wpps-cs-code-panel blocks.
		 * @param {Object}  options   { maxLength, onDirty, onSave, mode }
		 * @return {Array} Editor instances { editor, panel, textarea }.
		 */
		initPowerSuiteCodeEditors: function(container, options) {
			if (!container || !window.wp || !window.wp.codeEditor) {
				return [];
			}
			options = options || {};
			const self = this;
			const instances = [];
			const panels = container.querySelectorAll('.wpps-cs-code-panel');

			panels.forEach(function(panel) {
				const ta = panel.querySelector('textarea.wpps-cs-code-editor');
				if (!ta) {
					return;
				}
				if (ta.nextElementSibling && ta.nextElementSibling.classList.contains('CodeMirror')) {
					return;
				}

				const height = parseInt(panel.getAttribute('data-editor-height'), 10) || 220;
				panel.style.setProperty('--cs-editor-height', height + 'px');

				try {
					const ed = window.wp.codeEditor.initialize(ta, {
						codemirror: {
							mode: options.mode || 'htmlmixed',
							lineNumbers: true,
							lineWrapping: true,
							styleActiveLine: true,
							matchBrackets: true,
							autoCloseBrackets: true,
							autoCloseTags: true,
							matchTags: { bothTags: true },
							indentUnit: 4,
							tabSize: 4,
							indentWithTabs: false,
							height: height + 'px'
						}
					});
					if (!ed || !ed.codemirror) {
						return;
					}
					const cm = ed.codemirror;
					cm.setOption('styleActiveLine', { nonEmpty: true });
					cm.setOption('extraKeys', {
						'Ctrl-F': 'findPersistent',
						'Cmd-F': 'findPersistent',
						'Ctrl-S': function() {
							if (typeof options.onSave === 'function') {
								options.onSave(ta);
							}
						},
						'Cmd-S': function() {
							if (typeof options.onSave === 'function') {
								options.onSave(ta);
							}
						}
					});

					cm.on('change', function() {
						if (typeof options.onDirty === 'function') {
							options.onDirty();
						}
					});

					instances.push({ editor: ed, panel: panel, textarea: ta });
				} catch (e) {
					if (window.console && typeof window.console.warn === 'function') {
						window.console.warn('WP PowerSuite: Code editor failed to initialize.', e);
					}
				}
			});

			this.applyPowerSuiteEditorTheme(container, instances);

			return instances;
		},

		applyPowerSuiteEditorTheme: function(container, instances) {
			if (!container) return;
			const storageKey = 'wppsCsEditorTheme';
			let theme = 'light';
			try {
				theme = window.localStorage.getItem(storageKey) === 'dark' ? 'dark' : 'light';
			} catch (e) {}
			const isDark = theme === 'dark';
			container.querySelectorAll('.wpps-cs-code-wrap').forEach(function(wrap) {
				wrap.classList.toggle('wpps-cs-theme-dark', isDark);
			});
			const list = instances || this.headerFooterCodeEditors || this.customAdminCssEditors || [];
			list.forEach(function(item) {
				const ed = item && item.editor ? item.editor : item;
				if (ed && ed.codemirror) {
					ed.codemirror.setOption('theme', isDark ? 'wpps-dark' : 'default');
					ed.codemirror.refresh();
				}
			});
			document.querySelectorAll('.wpps-hfc-modal-theme-toggle, .wpps-hfc-theme-toggle, .wpps-cs-theme-toggle').forEach(function(btn) {
				btn.setAttribute('aria-pressed', isDark ? 'true' : 'false');
				const label = btn.querySelector('.wpps-cs-theme-toggle-label');
				if (label) {
					label.textContent = isDark ? 'Light' : 'Dark';
				}
			});
		},

		setupHfcModalThemeToggle: function(modal, settingsContainer, editorInstances) {
			if (!modal) return;
			const footer = modal.querySelector('.wp-powersuite-modal__footer');
			const cancelBtn = modal.querySelector('.wp-powersuite-modal__cancel');
			if (!footer || !cancelBtn) return;

			let btn = modal.querySelector('.wpps-hfc-modal-theme-toggle');
			if (!btn) {
				btn = document.createElement('button');
				btn.type = 'button';
				btn.className = 'button wpps-cs-editor-tool-btn wpps-hfc-modal-theme-toggle';
				btn.setAttribute('aria-pressed', 'false');
				btn.setAttribute('title', this.i18n('Toggle editor dark mode', 'wp-powersuite') || 'Toggle editor dark mode');
				btn.innerHTML = '<span class="wpps-cs-theme-icon" aria-hidden="true">'
					+ '<svg class="wpps-cs-ico wpps-cs-ico-moon" viewBox="0 0 24 24" width="15" height="15"><path fill="currentColor" d="M12.3 2.6a9.5 9.5 0 1 0 9.1 12.2 7.6 7.6 0 0 1-9.5-9.5c0-.93.17-1.83.4-2.7z"/></svg>'
					+ '<svg class="wpps-cs-ico wpps-cs-ico-sun" viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="4.2"/><path d="M12 2.5v2.2M12 19.3v2.2M4.4 4.4l1.6 1.6M18 18l1.6 1.6M2.5 12h2.2M19.3 12h2.2M4.4 19.6l1.6-1.6M18 6l1.6-1.6"/></svg>'
					+ '</span>'
					+ '<span class="wpps-cs-theme-toggle-label">' + this.escapeHtml(this.i18n('Dark', 'wp-powersuite') || 'Dark') + '</span>';
				footer.insertBefore(btn, cancelBtn);
				const self = this;
				btn.addEventListener('click', function() {
					const storageKey = 'wppsCsEditorTheme';
					let current = 'light';
					try {
						current = window.localStorage.getItem(storageKey) === 'dark' ? 'dark' : 'light';
					} catch (err) {}
					const next = current === 'dark' ? 'light' : 'dark';
					try {
						window.localStorage.setItem(storageKey, next);
					} catch (err) {}
					const settings = modal.querySelector('.wp-powersuite-hfc-settings')
						|| modal.querySelector('.wp-powersuite-cac-settings')
						|| settingsContainer;
					const instances = btn._wppsEditorInstances
						|| self.headerFooterCodeEditors
						|| self.customAdminCssEditors;
					self.applyPowerSuiteEditorTheme(settings, instances);
				});
			}
			btn._wppsEditorInstances = editorInstances || btn._wppsEditorInstances || null;
			btn.style.display = '';
			const settings = settingsContainer
				|| modal.querySelector('.wp-powersuite-hfc-settings')
				|| modal.querySelector('.wp-powersuite-cac-settings');
			const instances = editorInstances || this.headerFooterCodeEditors || this.customAdminCssEditors;
			this.applyPowerSuiteEditorTheme(settings, instances);
		},

		hideHfcModalThemeToggle: function(modal) {
			if (!modal) return;
			const btn = modal.querySelector('.wpps-hfc-modal-theme-toggle');
			if (btn) {
				btn.style.display = 'none';
			}
		},

		savePowerSuiteCodeEditors: function(instances) {
			if (!Array.isArray(instances)) return;
			instances.forEach(function(item) {
				const ed = item && item.editor ? item.editor : item;
				if (ed && ed.codemirror) {
					ed.codemirror.save();
				}
			});
		},

		/**
		 * Warn before leaving the page when code editors have unsaved changes.
		 */
		ensureCodeEditorUnloadWarning: function() {
			if (this.codeEditorUnloadBound) {
				return;
			}
			this.codeEditorUnloadBound = true;
			const self = this;
			window.addEventListener('beforeunload', function(e) {
				const hfcDirty = self.headerFooterCodeDirty
					&& Array.isArray(self.headerFooterCodeEditors)
					&& self.headerFooterCodeEditors.length;
				const cacDirty = self.customAdminCssDirty
					&& Array.isArray(self.customAdminCssEditors)
					&& self.customAdminCssEditors.length;
				if (hfcDirty || cacDirty) {
					e.preventDefault();
					e.returnValue = '';
				}
			});
		},

		/**
		 * Initialize code editors for Header & Footer Code module.
		 */
		initHeaderFooterCodeEditors: function(settings) {
			if (!settings || !window.wp || !window.wp.codeEditor) return;
			this.headerFooterCodeEditors = [];
			this.headerFooterCodeDirty = false;
			const self = this;

			this.ensureCodeEditorUnloadWarning();

			window.requestAnimationFrame(function() {
				self.headerFooterCodeEditors = self.initPowerSuiteCodeEditors(settings, {
					onDirty: function() {
						self.headerFooterCodeDirty = true;
					},
					onSave: function(ta) {
						self.triggerHeaderFooterSave(ta);
					}
				});
				const modal = document.getElementById('wp-powersuite-modal');
				self.setupHfcModalThemeToggle(modal, settings, self.headerFooterCodeEditors);
			});
		},

		/**
		 * Trigger the module settings save from inside a Header & Footer editor (Ctrl/Cmd+S).
		 */
		triggerHeaderFooterSave: function(ta) {
			const modal = ta.closest('.wp-powersuite-modal');
			const saveBtn = modal ? modal.querySelector('.wp-powersuite-modal__save') : null;
			if (saveBtn) {
				saveBtn.click();
			}
		},

		/**
		 * Initialize role multiselect dropdowns inside a container (for modal-loaded content).
		 * Supports .wpps-ct-multiselect class pattern.
		 */
		initRoleMultiselects: function(container) {
			if (!container) return;
			const roots = container.querySelectorAll('.wpps-ct-multiselect');
			roots.forEach(root => {
				if (root.getAttribute('data-wpps-multiselect-initialized') === '1') return;
				const prefix = 'wpps-ct-multiselect';
				const trigger = root.querySelector('.' + prefix + '__trigger');
				const triggerText = root.querySelector('.' + prefix + '__trigger-text');
				const dropdown = root.querySelector('.' + prefix + '__dropdown');
				const search = root.querySelector('.' + prefix + '__search');
				const options = root.querySelectorAll('.' + prefix + '__option');
				const hiddenClass = prefix + '__option--hidden';
				if (!trigger || !triggerText || !dropdown || !search) return;
				function countChecked() {
					let n = 0;
					options.forEach(o => {
						const cb = o.querySelector('input[type="checkbox"]');
						if (cb && cb.checked) n++;
					});
					return n;
				}
				function updateTriggerText() {
					const n = countChecked();
					const none = root.dataset.labelNone || 'Select roles...';
					const single = root.dataset.labelSingle || '1 selected';
					const multi = (root.dataset.labelMulti || '%d selected').replace('%d', String(n));
					triggerText.textContent = n === 0 ? none : (n === 1 ? single : multi);
				}
				function filterOptions(q) {
					q = (q || '').toLowerCase().trim();
					options.forEach(o => {
						const label = (o.dataset.label || '').toLowerCase();
						const slug = (o.dataset.slug || '').toLowerCase();
						const show = !q || label.indexOf(q) !== -1 || slug.indexOf(q) !== -1;
						o.classList.toggle(hiddenClass, !show);
					});
				}
				const fixedClass = prefix + '__dropdown--fixed';
				const modalEl = container.closest('.wp-powersuite-modal');
				const modalBodyEl = modalEl ? modalEl.querySelector('.wp-powersuite-modal__body') : null;
				function positionDropdownFixed() {
					if (dropdown.hasAttribute('hidden') || !modalEl) {
						return;
					}
					const rect = trigger.getBoundingClientRect();
					const maxHeight = 280;
					dropdown.classList.add(fixedClass);
					dropdown.style.position = 'fixed';
					dropdown.style.zIndex = '100050';
					dropdown.style.width = Math.round(rect.width) + 'px';
					dropdown.style.left = Math.round(rect.left) + 'px';
					let top = Math.round(rect.bottom + 4);
					if (top + maxHeight > window.innerHeight) {
						top = Math.max(8, Math.round(rect.top - maxHeight - 4));
					}
					dropdown.style.top = top + 'px';
				}
				function resetDropdownPosition() {
					dropdown.classList.remove(fixedClass);
					dropdown.style.position = '';
					dropdown.style.zIndex = '';
					dropdown.style.width = '';
					dropdown.style.left = '';
					dropdown.style.top = '';
				}
				function bindDropdownPositionListeners() {
					if (!modalEl) {
						return;
					}
					positionDropdownFixed();
					window.addEventListener('scroll', positionDropdownFixed, true);
					window.addEventListener('resize', positionDropdownFixed);
					if (modalBodyEl) {
						modalBodyEl.addEventListener('scroll', positionDropdownFixed);
					}
				}
				function unbindDropdownPositionListeners() {
					window.removeEventListener('scroll', positionDropdownFixed, true);
					window.removeEventListener('resize', positionDropdownFixed);
					if (modalBodyEl) {
						modalBodyEl.removeEventListener('scroll', positionDropdownFixed);
					}
					resetDropdownPosition();
				}
				function toggleDropdown(open) {
					const isOpen = open === undefined ? dropdown.hasAttribute('hidden') : !!open;
					if (isOpen) {
						dropdown.removeAttribute('hidden');
						trigger.setAttribute('aria-expanded', 'true');
						search.value = '';
						filterOptions('');
						if (modalEl) {
							bindDropdownPositionListeners();
						}
						setTimeout(() => search.focus(), 50);
					} else {
						dropdown.setAttribute('hidden', '');
						trigger.setAttribute('aria-expanded', 'false');
						unbindDropdownPositionListeners();
					}
				}
				root._wppsMultiselectClose = function() {
					if (!dropdown.hasAttribute('hidden')) {
						toggleDropdown(false);
					}
				};
				// Use capture phase so we run before any inline module scripts that also bind to the trigger
				trigger.addEventListener('click', e => {
					e.preventDefault();
					e.stopPropagation();
					e.stopImmediatePropagation();
					toggleDropdown();
				}, true);
				search.addEventListener('input', () => filterOptions(search.value));
				search.addEventListener('keydown', e => { if (e.key === 'Escape') toggleDropdown(false); });
				root.querySelectorAll('.' + prefix + '__action').forEach(btn => {
					btn.addEventListener('click', e => {
						e.preventDefault();
						e.stopPropagation();
						const action = btn.dataset.action;
						const visible = Array.prototype.filter.call(options, o => !o.classList.contains(hiddenClass));
						if (action === 'select-all') visible.forEach(o => { const cb = o.querySelector('input[type="checkbox"]'); if (cb) cb.checked = true; });
						else if (action === 'clear') options.forEach(o => { const cb = o.querySelector('input[type="checkbox"]'); if (cb) cb.checked = false; });
						updateTriggerText();
					});
				});
				options.forEach(o => {
					const cb = o.querySelector('input[type="checkbox"]');
					if (cb) {
						cb.addEventListener('change', updateTriggerText);
					}
					o.addEventListener('click', e => {
						if (e.target !== cb) { e.preventDefault(); cb.checked = !cb.checked; updateTriggerText(); }
					});
				});
				// Defer outside-click close to next tick so the same click that opened the dropdown cannot close it
				document.addEventListener('click', e => {
					const target = e.target;
					setTimeout(() => {
						if (!root.contains(target)) toggleDropdown(false);
					}, 0);
				});
				updateTriggerText();
				root.setAttribute('data-wpps-multiselect-initialized', '1');
			});
		},

		/**
		 * Initialize Quick Debug Toggle module
		 */
		initWpConfigEditor: function(container) {
			const self = this;
			if (!container || container.getAttribute('data-wpps-wpconfig-init') === '1') {
				if (container) {
					const ta = container.querySelector('#wpps-wpconfig-content');
					this.wpConfigEditorResolveInstance(ta);
					this.initWpConfigCodeEditor(container, ta);
				}
				return;
			}
			container.setAttribute('data-wpps-wpconfig-init', '1');

			const ta = container.querySelector('#wpps-wpconfig-content');
			const revealBtn = container.querySelector('.wpps-wpconfig-reveal');
			const maskNotice = container.querySelector('.wpps-wpconfig-mask-notice');

			this.wpConfigEditorDirty = false;
			this.wpConfigEditorPendingContent = null;

			this.initWpConfigCodeEditor(container, ta);

			if (!this.wpConfigEditorUnloadBound) {
				this.wpConfigEditorUnloadBound = true;
				window.addEventListener('beforeunload', function(e) {
					if (self.wpConfigEditorDirty) {
						e.preventDefault();
						e.returnValue = '';
					}
				});
			}

			container.querySelectorAll('.wpps-wpconfig-load').forEach(btn => {
				btn.addEventListener('click', () => self.wpConfigEditorLoad(ta, revealBtn, maskNotice, false, btn));
			});

			if (revealBtn) {
				revealBtn.addEventListener('click', function() {
					if (!confirm(self.i18n('Reveal database credentials and security keys in the editor? Only continue on a trusted device.', 'wp-powersuite') || 'Reveal database credentials and security keys in the editor? Only continue on a trusted device.')) {
						return;
					}
					self.wpConfigEditorLoad(ta, revealBtn, maskNotice, true, revealBtn);
				});
			}

			container.querySelectorAll('.wpps-wpconfig-backup').forEach(btn => {
				btn.addEventListener('click', function() {
					self.wpConfigEditorCreateBackup(btn);
				});
			});
		},

		wpConfigEditorResolveInstance: function(ta) {
			if (ta && ta._wppsCodeEditor && ta._wppsCodeEditor.codemirror) {
				this.wpConfigEditor = ta._wppsCodeEditor;
				return ta._wppsCodeEditor;
			}
			if (this.wpConfigEditor && this.wpConfigEditor.codemirror) {
				if (ta) {
					ta._wppsCodeEditor = this.wpConfigEditor;
				}
				return this.wpConfigEditor;
			}
			return null;
		},

		initWpConfigCodeEditor: function(container, ta, onReady) {
			const self = this;
			const done = function() {
				if (self.wpConfigEditorPendingContent !== null && self.wpConfigEditor && self.wpConfigEditor.codemirror) {
					self.wpConfigEditor.codemirror.setValue(self.wpConfigEditorPendingContent);
					self.wpConfigEditor.codemirror.refresh();
					self.wpConfigEditorPendingContent = null;
				}
				if (self.wpConfigEditor && self.wpConfigEditor.codemirror) {
					self.wpConfigEditor.codemirror.refresh();
				}
				if (typeof onReady === 'function') {
					onReady();
				}
			};

			if (!ta || !window.wp || !window.wp.codeEditor) {
				done();
				return;
			}

			if (ta.nextElementSibling && ta.nextElementSibling.classList.contains('CodeMirror')) {
				if (self.wpConfigEditorResolveInstance(ta)) {
					done();
					return;
				}
				ta.nextElementSibling.remove();
				ta.style.display = '';
				ta.removeAttribute('data-cm-active');
			}

			window.requestAnimationFrame(function() {
				try {
					const ed = window.wp.codeEditor.initialize(ta, {
						codemirror: {
							mode: 'application/x-httpd-php',
							lineNumbers: true,
							lineWrapping: true,
							styleActiveLine: true,
							autoRefresh: true,
							indentUnit: 4,
							tabSize: 4,
							indentWithTabs: false,
							matchBrackets: true,
							autoCloseBrackets: true
						}
					});
					self.wpConfigEditor = ed;
					ta._wppsCodeEditor = ed;
					if (ed && ed.codemirror) {
						ed.codemirror.on('change', function() {
							self.wpConfigEditorDirty = true;
						});
						ed.codemirror.refresh();
						window.setTimeout(function() {
							ed.codemirror.refresh();
						}, 50);
					}
				} catch (e) {
					if (window.console && typeof window.console.warn === 'function') {
						window.console.warn('WP PowerSuite: wp-config editor failed to initialize.', e);
					}
				}
				done();
			});
		},

		wpConfigEditorGetContent: function(ta) {
			const ed = this.wpConfigEditorResolveInstance(ta);
			if (ed && ed.codemirror) {
				ed.codemirror.save();
				return ed.codemirror.getValue();
			}
			return ta ? ta.value : '';
		},

		wpConfigEditorSetContent: function(ta, content) {
			const ed = this.wpConfigEditorResolveInstance(ta);
			if (ed && ed.codemirror) {
				ed.codemirror.setValue(content);
				ed.codemirror.refresh();
				this.wpConfigEditorPendingContent = null;
			} else if (ta) {
				ta.value = content;
				this.wpConfigEditorPendingContent = content;
			}
		},

		wpConfigEditorSave: function(settings, saveBtn) {
			const self = this;
			const ta = settings ? settings.querySelector('#wpps-wpconfig-content') : null;
			const form = settings ? settings.querySelector('.wp-powersuite-module-settings-form') : null;
			const content = this.wpConfigEditorGetContent(ta);
			const confirmMsg = this.i18n('Warning: Editing wp-config.php can break your site. Have you created a backup? Only proceed if you are sure of your changes.', 'wp-powersuite') || 'Warning: Editing wp-config.php can break your site. Have you created a backup? Only proceed if you are sure of your changes.';

			if (!confirm(confirmMsg)) {
				return;
			}

			const savingLabel = this.i18n('Saving...', 'wp-powersuite') || 'Saving...';
			if (saveBtn) {
				this.setButtonBusy(saveBtn, true, savingLabel);
			}

			const formData = new FormData();
			formData.append('action', 'wp_powersuite_config_editor_save');
			formData.append('nonce', window.wpPowerSuite.nonce);
			formData.append('content', content);

			fetch(ajaxurl, { method: 'POST', body: formData })
				.then(r => r.json())
				.then(function(response) {
					if (saveBtn) {
						self.setButtonBusy(saveBtn, false);
					}
					if (form) {
						form.querySelectorAll('.notice').forEach(function(n) { n.remove(); });
					}
					if (settings) {
						settings.querySelectorAll('.notice').forEach(function(n) { n.remove(); });
					}
					const message = response.data && response.data.message
						? response.data.message
						: (response.success
							? (self.i18n('Config saved.', 'wp-powersuite') || 'Config saved.')
							: (self.i18n('Failed to save wp-config.php file.', 'wp-powersuite') || 'Failed to save wp-config.php file.'));
					const notice = self.createNoticeElement(
						response.success ? 'notice notice-success is-dismissible' : 'notice notice-error is-dismissible',
						message
					);
					if (form) {
						form.insertBefore(notice, form.firstChild);
					} else if (settings) {
						settings.insertBefore(notice, settings.firstChild);
					}
					if (response.success) {
						self.wpConfigEditorDirty = false;
						setTimeout(function() { notice.remove(); }, 3000);
					}
				})
				.catch(function() {
					if (saveBtn) {
						self.setButtonBusy(saveBtn, false);
					}
					const notice = self.createNoticeElement(
						'notice notice-error is-dismissible',
						self.i18n('An error occurred while saving the file.', 'wp-powersuite') || 'An error occurred while saving the file.'
					);
					if (form) {
						form.insertBefore(notice, form.firstChild);
					}
				});
		},

		wpConfigEditorLoad: function(ta, revealBtn, maskNotice, revealSecrets, triggerBtn) {
			const self = this;
			if (!ta) return;
			const editorRoot = ta.closest('.wpps-wpconfig-editor');
			const loadBtn = triggerBtn || (editorRoot ? editorRoot.querySelector('.wpps-wpconfig-load') : null);
			const busyBtn = revealSecrets && revealBtn ? revealBtn : loadBtn;
			const loadingLabel = revealSecrets
				? (self.i18n('Revealing...', 'wp-powersuite') || 'Revealing...')
				: (self.i18n('Loading...', 'wp-powersuite') || 'Loading...');

			if (busyBtn) {
				self.setButtonBusy(busyBtn, true, loadingLabel);
			}

			this.wpConfigEditorSetContent(ta, '');
			if (maskNotice) maskNotice.style.display = 'none';
			if (revealBtn) revealBtn.style.display = 'none';
			const formData = new FormData();
			formData.append('action', 'wp_powersuite_config_editor_get');
			formData.append('nonce', window.wpPowerSuite.nonce);
			if (revealSecrets) {
				formData.append('reveal_secrets', '1');
			}
			self.fetchJsonResponse(formData)
				.then(data => {
					if (data.success && data.data && data.data.content !== undefined) {
						self.wpConfigEditorSetContent(ta, data.data.content);
						self.wpConfigEditorDirty = false;
						ta.dataset.wppsConfigLoaded = '1';
						const masked = !!data.data.masked;
						if (maskNotice) maskNotice.style.display = masked ? 'block' : 'none';
						if (revealBtn) revealBtn.style.display = masked ? 'inline-block' : 'none';
						ta.dataset.secretsRevealed = revealSecrets ? '1' : '0';
					} else {
						alert(data.data?.message || 'Could not load config.');
					}
				})
				.catch(function(err) {
					alert(err && err.message ? err.message : 'Request failed.');
				})
				.finally(function() {
					if (busyBtn) {
						self.setButtonBusy(busyBtn, false);
					}
				});
		},

		wpConfigEditorCreateBackup: function(btn) {
			const self = this;
			if (btn) {
				self.setButtonBusy(btn, true, self.i18n('Creating...', 'wp-powersuite') || 'Creating...');
			}
			const formData = new FormData();
			formData.append('action', 'wp_powersuite_config_editor_backup');
			formData.append('nonce', window.wpPowerSuite.nonce);
			self.fetchJsonResponse(formData)
				.then(data => {
					alert(data.data?.message || (data.success ? 'Backup created.' : 'Backup failed.'));
				})
				.catch(function(err) {
					alert(err && err.message ? err.message : 'Request failed.');
				})
				.finally(function() {
					if (btn) {
						self.setButtonBusy(btn, false);
					}
				});
		},


		initRobotsTxtManager: function(container) {
			const self = this;
			if (!container) {
				return;
			}

			const wrap = container.querySelector('.wpps-robots-txt');
			if (!wrap) {
				return;
			}

			const ta = wrap.querySelector('#wpps-robots-content');
			const sitemapUrl = wrap.getAttribute('data-sitemap-url') || wrap.dataset.sitemapUrl || '';

			if (wrap.getAttribute('data-wpps-robots-init') === '1') {
				if (ta) {
					this.robotsTxtResolveEditor(ta);
					this.initRobotsCodeEditor(ta);
				}
				return;
			}
			wrap.setAttribute('data-wpps-robots-init', '1');

			this.initRobotsCodeEditor(ta);

			wrap.querySelectorAll('.wpps-robots-load').forEach(btn => {
				btn.addEventListener('click', () => self.robotsTxtLoad(ta, btn));
			});
			wrap.querySelectorAll('.wpps-robots-add-sitemap').forEach(btn => {
				btn.addEventListener('click', () => self.robotsTxtAddSitemap(ta, sitemapUrl, wrap));
			});
			wrap.querySelectorAll('.wpps-robots-backup').forEach(btn => {
				btn.addEventListener('click', () => self.robotsTxtCreateBackup(btn, wrap));
			});
		},

		robotsTxtResolveEditor: function(ta) {
			if (ta && ta._wppsCodeEditor && ta._wppsCodeEditor.codemirror) {
				this.robotsTxtEditor = ta._wppsCodeEditor;
				return ta._wppsCodeEditor;
			}
			if (this.robotsTxtEditor && this.robotsTxtEditor.codemirror) {
				if (ta) {
					ta._wppsCodeEditor = this.robotsTxtEditor;
				}
				return this.robotsTxtEditor;
			}
			return null;
		},

		initRobotsCodeEditor: function(ta) {
			const self = this;
			if (!ta || !window.wp || !window.wp.codeEditor) {
				return;
			}

			if (ta.nextElementSibling && ta.nextElementSibling.classList.contains('CodeMirror')) {
				if (self.robotsTxtResolveEditor(ta)) {
					self.robotsTxtEditor.codemirror.refresh();
					return;
				}
				ta.nextElementSibling.remove();
				ta.style.display = '';
			}

			window.requestAnimationFrame(function() {
				try {
					const ed = window.wp.codeEditor.initialize(ta, {
						codemirror: {
							mode: 'text/plain',
							lineNumbers: true,
							lineWrapping: true,
							styleActiveLine: true,
							autoRefresh: true,
							indentUnit: 4,
							tabSize: 4,
							indentWithTabs: false
						}
					});
					self.robotsTxtEditor = ed;
					ta._wppsCodeEditor = ed;
					if (ed && ed.codemirror) {
						ed.codemirror.refresh();
						window.setTimeout(function() {
							ed.codemirror.refresh();
						}, 50);
					}
				} catch (e) {
					if (window.console && typeof window.console.warn === 'function') {
						window.console.warn('WP PowerSuite: robots.txt editor failed to initialize.', e);
					}
				}
			});
		},

		robotsTxtGetContent: function(ta) {
			const ed = this.robotsTxtResolveEditor(ta);
			if (ed && ed.codemirror) {
				ed.codemirror.save();
				return ed.codemirror.getValue();
			}
			return ta ? ta.value : '';
		},

		robotsTxtSetContent: function(ta, content) {
			const ed = this.robotsTxtResolveEditor(ta);
			if (ed && ed.codemirror) {
				ed.codemirror.setValue(content);
				ed.codemirror.refresh();
				return;
			}
			if (ta) {
				ta.value = content;
			}
		},

		insertModuleNotice: function(form, settings, message, isSuccess) {
			if (form) {
				form.querySelectorAll('.notice').forEach(function(n) { n.remove(); });
			}
			if (settings) {
				settings.querySelectorAll('.notice').forEach(function(n) { n.remove(); });
			}
			const notice = this.createNoticeElement(
				isSuccess ? 'notice notice-success is-dismissible' : 'notice notice-error is-dismissible',
				message
			);
			if (form) {
				form.insertBefore(notice, form.firstChild);
			} else if (settings) {
				settings.insertBefore(notice, settings.firstChild);
			}
			if (isSuccess) {
				window.setTimeout(function() { notice.remove(); }, 3000);
			}
			return notice;
		},

		robotsTxtLoad: function(ta, triggerBtn) {
			const self = this;
			if (!ta) {
				return;
			}
			const loadingLabel = self.i18n('Loading...', 'wp-powersuite') || 'Loading...';
			if (triggerBtn) {
				self.setButtonBusy(triggerBtn, true, loadingLabel);
			}

			const formData = new FormData();
			formData.append('action', 'wp_powersuite_robots_txt_get');
			formData.append('nonce', window.wpPowerSuite.nonce);

			self.fetchJsonResponse(formData)
				.then(data => {
					if (data.success && data.data && data.data.content !== undefined) {
						const content = data.data.content;
						if (/^\s*<(!DOCTYPE|html)/i.test(content)) {
							alert(self.i18n('Could not load robots.txt: received an HTML page instead of plain text.', 'wp-powersuite') || 'Could not load robots.txt: received an HTML page instead of plain text.');
							return;
						}
						self.robotsTxtSetContent(ta, content);
					} else {
						alert(data.data?.message || (self.i18n('Could not load robots.txt.', 'wp-powersuite') || 'Could not load robots.txt.'));
					}
				})
				.catch(function(err) {
					alert(err.message || (self.i18n('Request failed.', 'wp-powersuite') || 'Request failed.'));
				})
				.finally(function() {
					if (triggerBtn) {
						self.setButtonBusy(triggerBtn, false);
					}
				});
		},

		robotsTxtAddSitemap: function(ta, url, wrap) {
			const self = this;
			if (!ta) {
				return;
			}
			if (!url) {
				alert(self.i18n('Sitemap URL is not available.', 'wp-powersuite') || 'Sitemap URL is not available.');
				return;
			}
			const line = 'Sitemap: ' + url;
			const cur = this.robotsTxtGetContent(ta).trim();
			if (cur && cur.split('\n').some(function(row) {
				return row.trim().toLowerCase() === line.toLowerCase();
			})) {
				const form = wrap ? wrap.closest('.wp-powersuite-module-settings-form') : null;
				const settings = wrap ? wrap.closest('.wp-powersuite-modal__settings') : null;
				self.insertModuleNotice(
					form,
					settings,
					self.i18n('That sitemap line is already in the editor.', 'wp-powersuite') || 'That sitemap line is already in the editor.',
					true
				);
				return;
			}
			this.robotsTxtSetContent(ta, cur ? cur + '\n' + line : line);
		},

		robotsTxtCreateBackup: function(btn, wrap) {
			const self = this;
			const form = wrap ? wrap.closest('.wp-powersuite-module-settings-form') : null;
			const settings = wrap ? wrap.closest('.wp-powersuite-modal__settings') : null;
			if (btn) {
				self.setButtonBusy(btn, true, self.i18n('Creating...', 'wp-powersuite') || 'Creating...');
			}

			const formData = new FormData();
			formData.append('action', 'wp_powersuite_robots_txt_backup');
			formData.append('nonce', window.wpPowerSuite.nonce);

			self.fetchJsonResponse(formData)
				.then(function(data) {
					const message = data.data && data.data.message
						? data.data.message
						: (data.success
							? (self.i18n('Backup created.', 'wp-powersuite') || 'Backup created.')
							: (self.i18n('Failed to create backup.', 'wp-powersuite') || 'Failed to create backup.'));
					self.insertModuleNotice(form, settings, message, !!data.success);
				})
				.catch(function(err) {
					self.insertModuleNotice(form, settings, err.message || (self.i18n('Request failed.', 'wp-powersuite') || 'Request failed.'), false);
				})
				.finally(function() {
					if (btn) {
						self.setButtonBusy(btn, false);
					}
				});
		},

		robotsTxtSave: function(settings, saveBtn) {
			const self = this;
			const ta = settings ? settings.querySelector('#wpps-robots-content') : null;
			const form = settings ? settings.querySelector('.wp-powersuite-module-settings-form') : null;
			const content = this.robotsTxtGetContent(ta);

			if (saveBtn) {
				self.setButtonBusy(saveBtn, true, self.i18n('Saving...', 'wp-powersuite') || 'Saving...');
			}

			const formData = new FormData();
			formData.append('action', 'wp_powersuite_robots_txt_save');
			formData.append('content', content);
			formData.append('nonce', window.wpPowerSuite.nonce);

			self.fetchJsonResponse(formData)
				.then(function(data) {
					const message = data.data && data.data.message
						? data.data.message
						: (data.success
							? (self.i18n('Robots.txt saved.', 'wp-powersuite') || 'Robots.txt saved.')
							: (self.i18n('Failed to save robots.txt file.', 'wp-powersuite') || 'Failed to save robots.txt file.'));
					self.insertModuleNotice(form, settings, message, !!data.success);
				})
				.catch(function(err) {
					self.insertModuleNotice(
						form,
						settings,
						err.message || (self.i18n('An error occurred while saving the file.', 'wp-powersuite') || 'An error occurred while saving the file.'),
						false
					);
				})
				.finally(function() {
					if (saveBtn) {
						self.setButtonBusy(saveBtn, false);
					}
				});
		},

		initAdsTxtManager: function(container) {
			const self = this;
			if (!container) {
				return;
			}

			const wrap = container.querySelector('.wpps-ads-txt');
			if (!wrap) {
				return;
			}

			const ta = wrap.querySelector('.wpps-ads-code-editor');
			if (wrap.getAttribute('data-wpps-ads-init') === '1') {
				if (ta) {
					this.adsTxtResolveEditor(ta);
					this.initAdsCodeEditor(ta);
				}
				return;
			}
			wrap.setAttribute('data-wpps-ads-init', '1');

			this.initAdsCodeEditor(ta);

			wrap.querySelectorAll('.wpps-ads-backup').forEach(btn => {
				btn.addEventListener('click', () => self.adsTxtCreateBackup(btn, wrap));
			});
		},

		adsTxtResolveEditor: function(ta) {
			if (ta && ta._wppsCodeEditor && ta._wppsCodeEditor.codemirror) {
				this.adsTxtEditor = ta._wppsCodeEditor;
				return ta._wppsCodeEditor;
			}
			if (this.adsTxtEditor && this.adsTxtEditor.codemirror) {
				if (ta) {
					ta._wppsCodeEditor = this.adsTxtEditor;
				}
				return this.adsTxtEditor;
			}
			return null;
		},

		initAdsCodeEditor: function(ta) {
			const self = this;
			if (!ta || !window.wp || !window.wp.codeEditor) {
				return;
			}

			if (ta.nextElementSibling && ta.nextElementSibling.classList.contains('CodeMirror')) {
				if (self.adsTxtResolveEditor(ta)) {
					self.adsTxtEditor.codemirror.refresh();
					return;
				}
				ta.nextElementSibling.remove();
				ta.style.display = '';
			}

			window.requestAnimationFrame(function() {
				try {
					const ed = window.wp.codeEditor.initialize(ta, {
						codemirror: {
							mode: 'text/plain',
							lineNumbers: true,
							lineWrapping: true,
							styleActiveLine: true,
							autoRefresh: true,
							indentUnit: 4,
							tabSize: 4,
							indentWithTabs: false
						}
					});
					self.adsTxtEditor = ed;
					ta._wppsCodeEditor = ed;
					if (ed && ed.codemirror) {
						ed.codemirror.refresh();
						window.setTimeout(function() {
							ed.codemirror.refresh();
						}, 50);
					}
				} catch (e) {
					if (window.console && typeof window.console.warn === 'function') {
						window.console.warn('WP PowerSuite: ads.txt editor failed to initialize.', e);
					}
				}
			});
		},

		adsTxtCreateBackup: function(btn, wrap) {
			const self = this;
			const form = wrap ? wrap.closest('.wp-powersuite-module-settings-form') : null;
			const settings = wrap ? wrap.closest('.wp-powersuite-modal__settings') : null;
			if (btn) {
				self.setButtonBusy(btn, true, self.i18n('Creating...', 'wp-powersuite') || 'Creating...');
			}

			const formData = new FormData();
			formData.append('action', 'wp_powersuite_ads_txt_backup');
			formData.append('nonce', window.wpPowerSuite.nonce);

			self.fetchJsonResponse(formData)
				.then(function(data) {
					const message = data.data && data.data.message
						? data.data.message
						: (data.success
							? (self.i18n('Backup created.', 'wp-powersuite') || 'Backup created.')
							: (self.i18n('Failed to create backup.', 'wp-powersuite') || 'Failed to create backup.'));
					self.insertModuleNotice(form, settings, message, !!data.success);
				})
				.catch(function(err) {
					self.insertModuleNotice(form, settings, err.message || (self.i18n('Request failed.', 'wp-powersuite') || 'Request failed.'), false);
				})
				.finally(function() {
					if (btn) {
						self.setButtonBusy(btn, false);
					}
				});
		},

		initQuickDebugToggle: function(container) {
			const self = this;
			container.querySelectorAll('.wpps-debug-enable').forEach(btn => {
				btn.addEventListener('click', function() {
					if (confirm(self.i18n('Enable debug mode? A backup of wp-config.php will be created.', 'wp-powersuite') || 'Enable debug mode? A backup of wp-config.php will be created.')) {
						self.quickDebugToggle('enable', btn);
					}
				});
			});
			container.querySelectorAll('.wpps-debug-disable').forEach(btn => {
				btn.addEventListener('click', function() {
					if (confirm(self.i18n('Disable debug mode?', 'wp-powersuite') || 'Disable debug mode?')) {
						self.quickDebugToggle('disable', btn);
					}
				});
			});
			container.querySelectorAll('.wpps-debug-toggle-const').forEach(btn => {
				btn.addEventListener('click', function() {
					const constant = btn.getAttribute('data-constant');
					const value = btn.getAttribute('data-value') === '1';
					const actionText = value ? 'enable' : 'disable';
					const isRisky = btn.getAttribute('data-risky') === '1';
					let confirmProduction = false;

					if (isRisky && constant === 'WP_DEBUG_DISPLAY' && value) {
						const typed = prompt(self.i18n('DANGER: This production site will show PHP errors to visitors. Type DISPLAY to confirm enabling WP_DEBUG_DISPLAY.', 'wp-powersuite') || 'DANGER: This production site will show PHP errors to visitors. Type DISPLAY to confirm enabling WP_DEBUG_DISPLAY.');
						if (typed !== 'DISPLAY') {
							return;
						}
						confirmProduction = true;
					} else if (!confirm(self.i18n('This will modify wp-config.php to ' + actionText + ' ' + constant + '. Continue?', 'wp-powersuite') || 'This will modify wp-config.php to ' + actionText + ' ' + constant + '. Continue?')) {
						return;
					}

					self.quickDebugToggleConstant(constant, value, btn, confirmProduction);
				});
			});
			container.querySelectorAll('.wpps-debug-view-log').forEach(btn => {
				btn.addEventListener('click', function() {
					self.quickDebugViewLog(btn);
				});
			});
			container.querySelectorAll('.wpps-debug-clear-log').forEach(btn => {
				btn.addEventListener('click', function() {
					if (confirm(self.i18n('Clear the debug log?', 'wp-powersuite') || 'Clear the debug log?')) {
						self.quickDebugClearLog(btn);
					}
				});
			});
			container.querySelectorAll('.wpps-debug-protect-log').forEach(btn => {
				btn.addEventListener('click', function() {
					self.quickDebugProtectLog(btn);
				});
			});
			container.querySelectorAll('.wpps-debug-copy-code').forEach(btn => {
				btn.addEventListener('click', function() {
					const pre = btn.closest('.wpps-debug-manual-wrap')?.querySelector('.wpps-debug-manual-code code');
					const code = pre ? pre.textContent : '';
					if (code && navigator.clipboard && navigator.clipboard.writeText) {
						navigator.clipboard.writeText(code).then(function() {
							const orig = btn.textContent;
							btn.textContent = self.i18n('Copied!', 'wp-powersuite') || 'Copied!';
							setTimeout(function() { btn.textContent = orig; }, 2000);
						});
					}
				});
			});
		},

		quickDebugToggle: function(action, btn) {
			const self = this;
			const loadingLabel = action === 'enable'
				? (self.i18n('Enabling...', 'wp-powersuite') || 'Enabling...')
				: (self.i18n('Disabling...', 'wp-powersuite') || 'Disabling...');
			if (btn) {
				self.setButtonBusy(btn, true, loadingLabel);
			}
			const formData = new FormData();
			formData.append('action', 'wp_powersuite_debug_toggle');
			formData.append('debug_action', action);
			formData.append('nonce', window.wpPowerSuite.nonce);

			self.fetchJsonResponse(formData)
				.then(data => {
					if (data.success) {
						alert(data.data?.message || 'Done.');
						location.reload();
					} else {
						alert(data.data?.message || 'Action failed.');
					}
				})
				.catch(err => { alert(err && err.message ? err.message : 'Request failed.'); console.error(err); })
				.finally(function() {
					if (btn) {
						self.setButtonBusy(btn, false);
					}
				});
		},

		quickDebugToggleConstant: function(constant, value, btn, confirmProduction) {
			const self = this;
			const loadingLabel = (self.i18n('Updating...', 'wp-powersuite') || 'Updating...');
			if (btn) {
				self.setButtonBusy(btn, true, loadingLabel);
			}
			const formData = new FormData();
			formData.append('action', 'wp_powersuite_debug_toggle');
			formData.append('operation', 'toggle_constant');
			formData.append('constant', constant);
			formData.append('value', value ? '1' : '0');
			if (confirmProduction) {
				formData.append('confirm_production_display', '1');
			}
			formData.append('nonce', window.wpPowerSuite.nonce);
			self.fetchJsonResponse(formData)
				.then(data => {
					if (data.success) {
						location.reload();
					} else {
						alert(data.data?.message || 'Action failed.');
					}
				})
				.catch(function(err) {
					alert(err && err.message ? err.message : 'Request failed.');
				})
				.finally(function() {
					if (btn) {
						self.setButtonBusy(btn, false);
					}
				});
		},

		quickDebugClearLog: function(btn) {
			const self = this;
			if (btn) {
				self.setButtonBusy(btn, true, self.i18n('Clearing...', 'wp-powersuite') || 'Clearing...');
			}
			const formData = new FormData();
			formData.append('action', 'wp_powersuite_debug_toggle');
			formData.append('operation', 'clear_log');
			formData.append('nonce', window.wpPowerSuite.nonce);
			self.fetchJsonResponse(formData)
				.then(data => {
					if (data.success) {
						const card = btn.closest('.wpps-debug-log-card');
						if (card) {
							const desc = card.querySelector('.description');
							if (desc) desc.textContent = (self.i18n ? self.i18n('Size: %s', 'wp-powersuite') : 'Size: %s').replace('%s', '0 B');
						}
					} else {
						alert(data.data?.message || 'Action failed.');
					}
				})
				.catch(function(err) {
					alert(err && err.message ? err.message : 'Request failed.');
				})
				.finally(function() {
					if (btn) {
						self.setButtonBusy(btn, false);
					}
				});
		},

		quickDebugProtectLog: function(btn) {
			const self = this;
			if (btn) {
				self.setButtonBusy(btn, true, self.i18n('Protecting...', 'wp-powersuite') || 'Protecting...');
			}
			const formData = new FormData();
			formData.append('action', 'wp_powersuite_debug_toggle');
			formData.append('operation', 'protect_log');
			formData.append('nonce', window.wpPowerSuite.nonce);
			self.fetchJsonResponse(formData)
				.then(data => {
					alert(data.data?.message || (data.success ? 'Done.' : 'Action failed.'));
				})
				.catch(function(err) {
					alert(err && err.message ? err.message : 'Request failed.');
				})
				.finally(function() {
					if (btn) {
						self.setButtonBusy(btn, false);
					}
				});
		},

		quickDebugViewLog: function(triggerBtn) {
			const self = this;
			if (triggerBtn) {
				self.setButtonBusy(triggerBtn, true, self.i18n('Loading...', 'wp-powersuite') || 'Loading...');
			}
			const modal = document.getElementById('wpps-debug-log-modal');
			if (!modal) {
				if (triggerBtn) {
					self.setButtonBusy(triggerBtn, false);
				}
				return;
			}
			const panel = modal.querySelector('.wpps-debug-log-modal-panel');
			const body = modal.querySelector('.wpps-debug-log-modal-body');
			const footer = modal.querySelector('.wpps-debug-log-modal-footer');
			const closeBtn = modal.querySelector('.wpps-debug-log-modal-close');
			let previousFocus = document.activeElement;

			function closeModal() {
				modal.style.display = 'none';
				if (body) {
					body.setAttribute('aria-busy', 'false');
				}
				document.removeEventListener('keydown', trapFocus);
				if (previousFocus && typeof previousFocus.focus === 'function') {
					previousFocus.focus();
				}
			}

			function trapFocus(e) {
				if (e.key === 'Escape') {
					e.preventDefault();
					closeModal();
					return;
				}
				if (e.key !== 'Tab' || !panel) return;
				const focusable = panel.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
				if (!focusable.length) return;
				const first = focusable[0];
				const last = focusable[focusable.length - 1];
				if (e.shiftKey && document.activeElement === first) {
					e.preventDefault();
					last.focus();
				} else if (!e.shiftKey && document.activeElement === last) {
					e.preventDefault();
					first.focus();
				}
			}

			modal.style.display = 'flex';
			if (body) {
				body.innerHTML = '<p class="description" style="margin:0;">' + (self.i18n ? self.i18n('Loading...', 'wp-powersuite') : 'Loading...') + '</p>';
				body.setAttribute('aria-busy', 'true');
			}
			if (footer) footer.innerHTML = '';
			document.addEventListener('keydown', trapFocus);
			if (panel) {
				panel.focus();
			}

			const formData = new FormData();
			formData.append('action', 'wp_powersuite_debug_toggle');
			formData.append('operation', 'get_log');
			formData.append('nonce', window.wpPowerSuite.nonce);
			self.fetchJsonResponse(formData)
				.then(data => {
					if (body) body.setAttribute('aria-busy', 'false');
					if (data.success && data.data) {
						const content = data.data.content || '';
						const truncated = data.data.truncated;
						const totalSize = data.data.total_size || 0;
						const escaped = content.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
						if (body) body.innerHTML = '<pre tabindex="0">' + escaped + '</pre>';
						if (footer) {
							let footerText = '';
							if (truncated) footerText = (self.i18n ? self.i18n('Showing last 100 KB of %s total.', 'wp-powersuite') : 'Showing last 100 KB of %s total.').replace('%s', self.formatBytes(totalSize));
							else if (content) footerText = (self.i18n ? self.i18n('Size: %s', 'wp-powersuite') : 'Size: %s').replace('%s', self.formatBytes(totalSize));
							footer.innerHTML = footerText ? '<span class="description">' + footerText + '</span>' : '';
						}
					} else {
						if (body) body.innerHTML = '<p class="description" style="margin:0;color:#d63638;">' + (data.data && data.data.message ? String(data.data.message).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;') : 'Failed to load log.') + '</p>';
					}
				})
				.catch(function(err) {
					if (body) {
						body.setAttribute('aria-busy', 'false');
						body.innerHTML = '<p class="description" style="margin:0;color:#d63638;">' + (err && err.message ? String(err.message).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;') : 'Request failed.') + '</p>';
					}
				})
				.finally(function() {
					if (triggerBtn) {
						self.setButtonBusy(triggerBtn, false);
					}
				});

			if (closeBtn) closeBtn.onclick = closeModal;
			modal.onclick = function(e) {
				if (e.target === modal) closeModal();
			};
		},

		formatBytes: function(bytes) {
			if (bytes < 1024) return bytes + ' B';
			if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
			return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
		},

		/**
		 * Clear settings cache for a module (or all modules)
		 */
		clearSettingsCache: function(moduleId = null) {
			if (moduleId) {
				delete this.settingsCache[moduleId];
			} else {
				this.settingsCache = {};
			}
		},

		/**
		 * Initialize advanced settings form interactions
		 */
		initializeAdvancedSettingsForm: function(container) {
			const self = this;
			
			const updateHeartbeatIntervalUi = function () {
				const preset = container.querySelector('#wpps-heartbeat-interval-preset');
				const customWrap = container.querySelector('#wpps-heartbeat-custom-interval');
				const heartbeatInterval = container.querySelector('#wpps-heartbeat-throttle-interval');
				if (!preset || !customWrap || !heartbeatInterval) {
					return;
				}

				let throttleMode = false;
				container.querySelectorAll('input[type="radio"][name="mode"]').forEach(function (radio) {
					if (radio.checked && radio.value === 'throttle') {
						throttleMode = true;
					}
				});

				if (!throttleMode) {
					customWrap.style.display = 'none';
					return;
				}

				if (preset.value) {
					heartbeatInterval.value = preset.value;
					customWrap.style.display = 'none';
				} else {
					customWrap.style.display = '';
				}
			};

			// Function to update disabled states based on radio buttons
			const updateDisabledStates = function() {
				const modeRadios = container.querySelectorAll('input[type="radio"][name="mode"]');
				let selectedMode = '';
				modeRadios.forEach(radio => {
					if (radio.checked) {
						selectedMode = radio.value;
					}
				});
				
				if (selectedMode) {
					const modeRadioList = Array.from(modeRadios);
					const hasLegacyPostTypeMode = modeRadioList.some(radio => radio.value === 'disable_for_post_types');

					// Legacy modules that use disable_for_post_types mode value only.
					if (hasLegacyPostTypeMode) {
						const postTypeCheckboxes = container.querySelectorAll('input[type="checkbox"][name*="post_types"], input[type="checkbox"][name*="disabled_post_types"]');
						postTypeCheckboxes.forEach(cb => {
							cb.disabled = selectedMode !== 'disable_for_post_types';
						});
					}

					// Disable Gutenberg: post type picker active only in "specific" mode.
					const disableGutenbergForm = container.querySelector('form[data-module-id="disable-gutenberg"]');
					if (disableGutenbergForm) {
						const postTypeCheckboxes = container.querySelectorAll('input[name="disabled_post_types[]"]');
						const multiselectTrigger = container.querySelector('#wpps-disable-gutenberg-post-types-row .wpps-ct-multiselect__trigger');
						const postTypesRow = container.querySelector('#wpps-disable-gutenberg-post-types-row');
						const enableSpecific = selectedMode === 'specific';
						postTypeCheckboxes.forEach(cb => {
							cb.disabled = !enableSpecific;
						});
						if (multiselectTrigger) {
							multiselectTrigger.disabled = !enableSpecific;
							multiselectTrigger.classList.toggle('wpps-ct-multiselect__trigger--disabled', !enableSpecific);
						}
						if (postTypesRow) {
							postTypesRow.style.display = enableSpecific ? '' : 'none';
						}
					}
					
					const revisionLimitInputs = container.querySelectorAll('input[name="revision_limit"]');
					revisionLimitInputs.forEach(input => {
						input.disabled = selectedMode !== 'limit_revisions';
					});
					
					const throttleInputs = container.querySelectorAll('input[name="throttle_interval"]');
					throttleInputs.forEach(input => {
						input.disabled = selectedMode !== 'throttle';
					});

					const heartbeatPresets = container.querySelectorAll('#wpps-heartbeat-interval-preset');
					heartbeatPresets.forEach(select => {
						select.disabled = selectedMode !== 'throttle';
					});
					
					const allowInEditorInputs = container.querySelectorAll('input[name="allow_in_editor"]');
					allowInEditorInputs.forEach(input => {
						input.disabled = selectedMode !== 'disable_admin' && selectedMode !== 'disable_all';
					});
				}
				
				const enableRedirect = container.querySelector('input[type="checkbox"][name="enable_redirect"]');
				if (enableRedirect) {
					const redirectInputs = container.querySelectorAll('input[name="default_redirect"], input[name^="role_redirects"]');
					redirectInputs.forEach(input => {
						input.disabled = !enableRedirect.checked;
					});
				}
				
				const disablePasswordReset = container.querySelector('input[type="checkbox"][name="disable_password_reset"]');
				if (disablePasswordReset) {
					const removeLinkInputs = container.querySelectorAll('input[name="remove_lost_password_link"]');
					removeLinkInputs.forEach(input => {
						input.disabled = !disablePasswordReset.checked;
					});
				}
				
				const enableCustomMessages = container.querySelector('input[type="checkbox"][name="enable_custom_messages"]');
				if (enableCustomMessages) {
					const errorMessageInputs = container.querySelectorAll('input[name="generic_error_message"]');
					errorMessageInputs.forEach(input => {
						input.disabled = !enableCustomMessages.checked;
					});
				}
				
				const disableAllUpdates = container.querySelector('input[type="checkbox"][name="disable_all_updates"], input[type="checkbox"][name="disable_all_update_emails"]');
				if (disableAllUpdates) {
					const updateCheckboxes = container.querySelectorAll('input[name="disable_core_updates"], input[name="disable_plugin_updates"], input[name="disable_theme_updates"]');
					updateCheckboxes.forEach(input => {
						input.disabled = disableAllUpdates.checked;
					});
				}

				const disableAllDdw = container.querySelector('input[type="checkbox"][name="disable_all"]');
				if (disableAllDdw && container.querySelector('.wpps-ddw-individual')) {
					const ddwCheckboxes = container.querySelectorAll('.wpps-ddw-individual input[type="checkbox"]');
					ddwCheckboxes.forEach(input => {
						input.disabled = disableAllDdw.checked;
					});
				}
				
				const footerOptionRadios = container.querySelectorAll('input[type="radio"][name="footer_option"]');
				footerOptionRadios.forEach(radio => {
					if (radio.checked) {
						const customTextarea = container.querySelector('textarea[name="custom_text"]');
						if (customTextarea) {
							customTextarea.disabled = radio.value !== 'custom';
						}
					}
				});

				updateHeartbeatIntervalUi();
			}
			
			const radioButtons = container.querySelectorAll('input[type="radio"]');
			radioButtons.forEach(radio => {
				radio.addEventListener('change', function() {
					updateDisabledStates();
				});
			});
			
			const checkboxes = container.querySelectorAll('input[type="checkbox"]');
			checkboxes.forEach(checkbox => {
				checkbox.addEventListener('change', function() {
					updateDisabledStates();
				});
			});
			
			updateDisabledStates();

			const heartbeatPreset = container.querySelector('#wpps-heartbeat-interval-preset');
			if (heartbeatPreset) {
				heartbeatPreset.addEventListener('change', function () {
					updateHeartbeatIntervalUi();
				});
			}
		},

		/**
		 * Save advanced settings via AJAX
		 */
		saveAdvancedSettings: function() {
			const modal = document.getElementById('wp-powersuite-modal');
			const settings = modal.querySelector('.wp-powersuite-modal__settings');
			const saveBtn = modal.querySelector('.wp-powersuite-modal__save');
			const moduleId = modal.dataset.currentModuleId;

			if (!moduleId) return;
			
			if (moduleId === 'robots-txt-manager') {
				this.robotsTxtSave(settings, saveBtn);
				return;
			}
			if (moduleId === 'wp-config-editor') {
				this.wpConfigEditorSave(settings, saveBtn);
				return;
			}
			// Download Media Library: no save (button hidden)
			if (moduleId === 'download-media-library') return;
			// Regenerate Thumbnails: footer button triggers Start Regenerating instead of save
			if (moduleId === 'regenerate-thumbnails') {
				const startBtn = settings ? settings.querySelector('#wpps-rt-start') : null;
				if (startBtn && !startBtn.disabled) {
					startBtn.click();
				}
				return;
			}
			if (moduleId === 'change-login-url' && settings) {
				const slugInput = settings.querySelector('[data-setting-key="login_slug"]');
				if (slugInput && typeof slugInput.checkValidity === 'function' && !slugInput.checkValidity()) {
					slugInput.reportValidity();
					return;
				}
			}
			if (moduleId === 'custom-admin-css' && Array.isArray(this.customAdminCssEditors)) {
				this.savePowerSuiteCodeEditors(this.customAdminCssEditors);
				this.customAdminCssDirty = false;
			}
			if (moduleId === 'login-page-customizor' && this.customCssCodeEditor && this.customCssCodeEditor.codemirror) {
				this.customCssCodeEditor.codemirror.save();
			}
			if (moduleId === 'header-footer-code' && Array.isArray(this.headerFooterCodeEditors)) {
				this.savePowerSuiteCodeEditors(this.headerFooterCodeEditors);
				this.headerFooterCodeDirty = false;
			}
			if (moduleId === 'ads-txt-manager' && this.adsTxtEditor && this.adsTxtEditor.codemirror) {
				this.adsTxtEditor.codemirror.save();
			}
			// Allow modules to sync dynamic values into form inputs before collect
			if (settings && window.wpPowerSuite && window.wpPowerSuite.syncModuleSettingsBeforeSave && typeof window.wpPowerSuite.syncModuleSettingsBeforeSave[moduleId] === 'function') {
				window.wpPowerSuite.syncModuleSettingsBeforeSave[moduleId]();
			}

			const settingsForm = settings ? settings.querySelector('.wp-powersuite-module-settings-form') : null;
			if (settingsForm && window.wpPowerSuite && window.wpPowerSuite.confirmBeforeSave && typeof window.wpPowerSuite.confirmBeforeSave[moduleId] === 'function') {
				if (!window.wpPowerSuite.confirmBeforeSave[moduleId](settingsForm)) {
					return;
				}
			}

			const formData = new FormData();
			formData.append('action', 'wp_powersuite_save_module_settings');
			formData.append('module_id', moduleId);
			formData.append('nonce', window.wpPowerSuite.nonce);

			const inputs = settings.querySelectorAll('.wp-powersuite-advanced-setting');
			const settingsObj = {}; // Build settings object first
			
			// Track radio button groups to only save the checked one
			const radioGroups = {};
			
			inputs.forEach(input => {
				const key = input.dataset.settingKey;
				if (key) {
					if (input.type === 'radio') {
						if (!radioGroups[key]) {
							radioGroups[key] = [];
						}
						radioGroups[key].push(input);
					} else if (input.type === 'checkbox') {
						if (input.name && input.name.includes('[]')) {
							// This is an array input (like disabled_post_types[])
							if (!settingsObj[key]) {
								settingsObj[key] = [];
							}
							if (input.checked) {
								settingsObj[key].push(input.value);
							}
						} else if (key && key.indexOf('.') !== -1) {
							// Nested checkbox (like roles.administrator) - only include if checked
							if (input.checked) {
								settingsObj[key] = '1';
							}
						} else {
							// Regular checkbox (non-nested)
							settingsObj[key] = input.checked ? '1' : '0';
						}
					} else if (input.tagName === 'TEXTAREA') {
						const value = input.value || '';
						if (key.indexOf('whitelist_') !== -1 || 
						    key.indexOf('plugin_slugs') !== -1 ||
						    key.indexOf('css_classes') !== -1 ||
						    key.indexOf('text_match') !== -1) {
							// Split by newlines and filter empty
							const lines = value.split('\n')
								.map(line => line.trim())
								.filter(line => line.length > 0);
							settingsObj[key] = lines;
						} else {
							// Regular textarea value
							settingsObj[key] = value;
						}
					} else {
						// Regular input (text, number, etc.)
						settingsObj[key] = input.value || '';
					}
				}
			});
			
			Object.keys(radioGroups).forEach(key => {
				const radios = radioGroups[key];
				const checkedRadio = radios.find(radio => radio.checked);
				if (checkedRadio) {
					settingsObj[key] = checkedRadio.value || '';
				} else {
					// Unchecked radios omit the key so existing saved values are not wiped.
					delete settingsObj[key];
				}
			});

			// Checkbox groups with [] names must still post an empty array when none are checked.
			inputs.forEach(input => {
				if (input.type === 'checkbox' && input.name && input.name.includes('[]')) {
					const key = input.dataset.settingKey;
					if (key && !settingsObj.hasOwnProperty(key)) {
						settingsObj[key] = [];
					}
				}
			});

			// Shadow colors (all tabs): opacity is stored in the hex text input as #RRGGBBAA.
			Object.keys(settingsObj).forEach(key => {
				if (!key || !key.endsWith('_shadow_color')) return;
				const shadowHex = settings.querySelector('input[data-shadow-hex][data-setting-key="' + key + '"]');
				if (!shadowHex) return;
				const val = (shadowHex.value || '').trim();
				if (val) {
					settingsObj[key] = val.charAt(0) === '#' ? val : '#' + val;
				}
			});
			// Other color keys with picker: prefer type="color" value so picked colors save (text input may be out of sync).
			Object.keys(settingsObj).forEach(key => {
				if (!key || key.endsWith('_shadow_color')) return;
				const colorInput = settings.querySelector('input[type="color"][data-setting-key="' + key + '"]');
				if (colorInput && colorInput.value) {
					settingsObj[key] = colorInput.value.charAt(0) === '#' ? colorInput.value : '#' + colorInput.value;
				}
			});

			// Custom Image Sizes: build from list DOM (source of truth) after syncBeforeSave
			if (moduleId === 'custom-image-sizes' && settings) {
				const listEl = settings.querySelector('#wpps-cis-custom-list');
				const arr = [];
				if (listEl) {
					listEl.querySelectorAll('li:not(.wpps-cis-empty)').forEach(function(li) {
						const n = (li.dataset.name || '').trim();
						if (n) {
							arr.push({
								name: n,
								width: parseInt(li.dataset.width, 10) || 0,
								height: parseInt(li.dataset.height, 10) || 0,
								crop: li.dataset.crop === '1'
							});
						}
					});
				}
				const customSizesJson = JSON.stringify(arr);
				settingsObj.custom_sizes = customSizesJson;
				// Fallback: also send as top-level param in case nested FormData parsing fails
				formData.append('wpps_cis_custom_sizes', customSizesJson);
			}

			// e.g., { 'roles.administrator': '1', 'roles.editor': '0' } -> { roles: { administrator: '1', editor: '0' } }
			const nestedSettingsObj = {};
			Object.keys(settingsObj).forEach(key => {
				if (key.indexOf('.') !== -1) {
					const keys = key.split('.');
					let current = nestedSettingsObj;
					for (let i = 0; i < keys.length - 1; i++) {
						const k = keys[i];
						if (!current[k]) {
							current[k] = {};
						}
						current = current[k];
					}
					const lastKey = keys[keys.length - 1];
					current[lastKey] = settingsObj[key];
				} else {
					// Simple key, add directly
					nestedSettingsObj[key] = settingsObj[key];
				}
			});

			// PHP expects settings[key][subkey]Ã¢â‚¬Â¦ not settings[key[subkey]] (broken nesting breaks $_POST parsing).
			const appendToFormData = (obj, prefixParts) => {
				prefixParts = prefixParts || [];
				Object.keys(obj).forEach(key => {
					const value = obj[key];
					const parts = prefixParts.concat([ key ]);
					const fieldPrefix = 'settings[' + parts.join('][') + ']';
					if (Array.isArray(value)) {
						if (value.length === 0) {
							formData.append(fieldPrefix + '[]', '');
						} else {
							value.forEach(v => {
								formData.append(fieldPrefix + '[]', v);
							});
						}
					} else if (typeof value === 'object' && value !== null) {
						appendToFormData(value, parts);
					} else {
						formData.append(fieldPrefix, value !== undefined && value !== null ? value : '');
					}
				});
			};
			
			appendToFormData(nestedSettingsObj);

			saveBtn.disabled = true;
			saveBtn.textContent = 'Saving...';

			fetch(window.wpPowerSuite.ajaxUrl, {
				method: 'POST',
				body: formData
			})
			.then(response => response.text())
			.then(text => {
				let data;
				try {
					data = JSON.parse(text);
				} catch (e) {
					// Server returned HTML (e.g. PHP fatal error) instead of JSON
					const preview = text.trim().substring(0, 80).replace(/</g, '&lt;');
					throw new Error('Server returned an error instead of JSON. Check your PHP error log. Response: ' + preview + (text.length > 80 ? 'Ã¢â‚¬Â¦' : ''));
				}
				return data;
			})
			.then(data => {
				saveBtn.disabled = false;
				saveBtn.textContent = 'Save Settings';

				if (data.success) {
					this.clearSettingsCache(moduleId);
					document.dispatchEvent(new CustomEvent('wp-powersuite-settings-saved', { detail: { moduleId: moduleId } }));
					const notice = this.createNoticeElement('notice notice-success is-dismissible', data.data?.message || 'Settings saved successfully.');
					settings.insertBefore(notice, settings.firstChild);

					if (!data.data?.no_changes) {
						const modalRefreshBtn = modal.querySelector('.wp-powersuite-modal__refresh');
						if (modalRefreshBtn) {
							modalRefreshBtn.style.display = 'inline-flex';
						}
						
						this.showRefreshButton();
						
						// Auto-dismiss notice after 3 seconds
						setTimeout(() => {
							notice.remove();
						}, 3000);
					} else {
						// Auto-dismiss after 3 seconds for "no changes" message
						setTimeout(() => {
							notice.remove();
						}, 3000);
					}
				} else {
					const notice = this.createNoticeElement('notice notice-error is-dismissible', data.data?.message || 'Failed to save settings.');
					settings.insertBefore(notice, settings.firstChild);
					
					// Auto-dismiss after 3 seconds
					setTimeout(() => {
						notice.remove();
					}, 3000);
				}
			})
			.catch(error => {
				saveBtn.disabled = false;
				saveBtn.textContent = 'Save Settings';

				const notice = this.createNoticeElement('notice notice-error is-dismissible', 'Error saving settings: ' + (error.message || ''));
				settings.insertBefore(notice, settings.firstChild);
			});
		},

		/**
		 * All save buttons for a custom full-page module.
		 */
		getCustomPageSaveButtons: function(moduleId) {
			if (!moduleId) {
				return [];
			}
			return Array.from(document.querySelectorAll('.wp-powersuite-custom-page-save[data-module-id="' + moduleId + '"]'));
		},

		/**
		 * Settings wrapper used for inline save notices (matches modal settings panel behavior).
		 */
		getCustomPageSettingsContainer: function(form) {
			if (!form) {
				return null;
			}
			return form.closest('.wp-powersuite-settings')
				|| form.closest('.wp-powersuite-module-settings')
				|| form.parentNode;
		},

		/**
		 * Toggle busy state on every save button for the module.
		 */
		setCustomPageSaveBusy: function(moduleId, busy) {
			const saveLabel = this.i18n('Save Changes', 'wp-powersuite') || 'Save Changes';
			const savingLabel = this.i18n('Saving...', 'wp-powersuite') || 'Saving...';
			this.getCustomPageSaveButtons(moduleId).forEach(function(btn) {
				if (!btn.dataset.saveDefaultLabel) {
					btn.dataset.saveDefaultLabel = (btn.textContent || saveLabel).trim();
				}
				const isHeroBtn = btn.classList.contains('wp-powersuite-custom-page-save--hero')
					|| (btn.closest && btn.closest('.wpps-page-hero__meta'));
				btn.disabled = !!busy;
				btn.classList.toggle('is-busy', !!busy);
				btn.setAttribute('aria-busy', busy ? 'true' : 'false');
				if (busy) {
					if (isHeroBtn) {
						btn.textContent = savingLabel;
					} else {
						btn.innerHTML = '<span class="spinner is-active"></span><span>' + savingLabel + '</span>';
					}
				} else {
					btn.textContent = btn.dataset.saveDefaultLabel || saveLabel;
				}
			});
		},

		/**
		 * Insert save feedback for custom full-page modules (hero save button may sit outside the form).
		 */
		insertCustomPageSaveNotice: function(form, notice) {
			const lpcNotice = document.querySelector('.wpps-lpc-notice');
			const settingsWrap = lpcNotice || this.getCustomPageSettingsContainer(form);
			if (!settingsWrap) {
				return;
			}
			const existing = settingsWrap.querySelector('[data-wpps-save-notice]');
			if (existing) {
				existing.remove();
			}
			notice.setAttribute('data-wpps-save-notice', 'true');
			if (lpcNotice) {
				lpcNotice.innerHTML = '';
				lpcNotice.appendChild(notice);
			} else {
				settingsWrap.insertBefore(notice, settingsWrap.firstChild);
			}
			if (typeof settingsWrap.scrollIntoView === 'function') {
				settingsWrap.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
			}
		},

		/**
		 * Save settings from custom page form (e.g. Coming Soon & Maintenance)
		 */
		saveCustomPageSettings: function(saveBtn) {
			const self = this;
			const moduleId = saveBtn.dataset.moduleId;
			let form = saveBtn.closest('form.wp-powersuite-module-settings-form');
			if (!form && moduleId) {
				form = document.querySelector('form.wp-powersuite-module-settings-form[data-module-id="' + moduleId + '"]');
			}
			if (!moduleId || !form || !window.wpPowerSuite) return;
			if (form.dataset.wppsSaving === '1' || saveBtn.disabled) return;
			const settings = form;
			if (window.wpPowerSuite.syncModuleSettingsBeforeSave && typeof window.wpPowerSuite.syncModuleSettingsBeforeSave[moduleId] === 'function') {
				window.wpPowerSuite.syncModuleSettingsBeforeSave[moduleId]();
			}
			if (moduleId === 'login-page-customizor' && this.customCssCodeEditor && this.customCssCodeEditor.codemirror) {
				this.customCssCodeEditor.codemirror.save();
			}
			const formData = new FormData();
			formData.append('action', 'wp_powersuite_save_module_settings');
			formData.append('module_id', moduleId);
			formData.append('nonce', window.wpPowerSuite.nonce);
			const inputs = settings.querySelectorAll('.wp-powersuite-advanced-setting');
			const settingsObj = {};
			const radioGroups = {};
			inputs.forEach(function(input) {
				const key = input.dataset.settingKey;
				if (!key) return;
				if (input.type === 'radio') {
					if (!radioGroups[key]) radioGroups[key] = [];
					radioGroups[key].push(input);
				} else if (input.type === 'checkbox') {
					if (input.name && input.name.includes('[]')) {
						if (!settingsObj[key]) settingsObj[key] = [];
						if (input.checked) settingsObj[key].push(input.value);
					} else if (key.indexOf('.') !== -1) {
						if (input.checked) settingsObj[key] = '1';
					} else {
						settingsObj[key] = input.checked ? '1' : '0';
					}
				} else if (input.tagName === 'TEXTAREA') {
					const value = input.value || '';
					if (key.indexOf('whitelist_') !== -1 || key.indexOf('plugin_slugs') !== -1 || key.indexOf('css_classes') !== -1 || key.indexOf('text_match') !== -1) {
						settingsObj[key] = value.split('\n').map(function(l) { return l.trim(); }).filter(function(l) { return l.length > 0; });
					} else {
						settingsObj[key] = value;
					}
				} else {
					settingsObj[key] = input.value || '';
				}
			});
			Object.keys(radioGroups).forEach(function(key) {
				const checked = radioGroups[key].find(function(r) { return r.checked; });
				if (checked) settingsObj[key] = checked.value || '';
				else delete settingsObj[key];
			});
			inputs.forEach(function(input) {
				if (input.type === 'checkbox' && input.name && input.name.includes('[]')) {
					const key = input.dataset.settingKey;
					if (key && !settingsObj.hasOwnProperty(key)) settingsObj[key] = [];
				}
			});
			Object.keys(settingsObj).forEach(function(key) {
				if (!key || !key.endsWith('_shadow_color')) return;
				const shadowHex = settings.querySelector('input[data-shadow-hex][data-setting-key="' + key + '"]');
				if (!shadowHex) return;
				const val = (shadowHex.value || '').trim();
				if (val) {
					settingsObj[key] = val.charAt(0) === '#' ? val : '#' + val;
				}
			});
			Object.keys(settingsObj).forEach(function(key) {
				if (!key || key.endsWith('_shadow_color')) return;
				const colorInput = settings.querySelector('input[type="color"][data-setting-key="' + key + '"]');
				if (colorInput && colorInput.value) {
					settingsObj[key] = colorInput.value.charAt(0) === '#' ? colorInput.value : '#' + colorInput.value;
				}
			});
			const nestedSettingsObj = {};
			Object.keys(settingsObj).forEach(function(key) {
				if (key.indexOf('.') !== -1) {
					const keys = key.split('.');
					let current = nestedSettingsObj;
					for (let i = 0; i < keys.length - 1; i++) {
						const k = keys[i];
						if (!current[k]) current[k] = {};
						current = current[k];
					}
					current[keys[keys.length - 1]] = settingsObj[key];
				} else {
					nestedSettingsObj[key] = settingsObj[key];
				}
			});
			const appendToFormData = function(obj, prefixParts) {
				prefixParts = prefixParts || [];
				Object.keys(obj).forEach(function(key) {
					const value = obj[key];
					const parts = prefixParts.concat([ key ]);
					const fieldPrefix = 'settings[' + parts.join('][') + ']';
					if (Array.isArray(value)) {
						if (value.length === 0) formData.append(fieldPrefix + '[]', '');
						else value.forEach(function(v) { formData.append(fieldPrefix + '[]', v); });
					} else if (typeof value === 'object' && value !== null) {
						appendToFormData(value, parts);
					} else {
						formData.append(fieldPrefix, value !== undefined && value !== null ? value : '');
					}
				});
			};
			appendToFormData(nestedSettingsObj);
			form.dataset.wppsSaving = '1';
			self.setCustomPageSaveBusy(moduleId, true);
			fetch(window.wpPowerSuite.ajaxUrl, { method: 'POST', body: formData })
				.then(function(r) { return r.text(); })
				.then(function(text) {
					let data;
					try {
						data = JSON.parse(text);
					} catch (e) {
						const preview = text.trim().substring(0, 80).replace(/</g, '&lt;');
						throw new Error('Server returned an error instead of JSON. Response: ' + preview + (text.length > 80 ? 'â€¦' : ''));
					}
					return data;
				})
				.then(function(data) {
					delete form.dataset.wppsSaving;
					self.setCustomPageSaveBusy(moduleId, false);
					if (data.success) {
						self.clearSettingsCache(moduleId);
						document.dispatchEvent(new CustomEvent('wp-powersuite-settings-saved', { detail: { moduleId: moduleId } }));
					}
					const defaultSuccess = self.i18n('Settings saved successfully.', 'wp-powersuite') || 'Settings saved successfully.';
					const defaultError = self.i18n('Failed to save settings.', 'wp-powersuite') || 'Failed to save settings.';
					const msg = data.data && data.data.message ? data.data.message : (data.success ? defaultSuccess : defaultError);
					const notice = self.createNoticeElement('notice notice-' + (data.success ? 'success' : 'error') + ' is-dismissible', msg);
					self.insertCustomPageSaveNotice(form, notice);
					setTimeout(function() { notice.remove(); }, data.success ? 3000 : 5000);
				})
				.catch(function(err) {
					delete form.dataset.wppsSaving;
					self.setCustomPageSaveBusy(moduleId, false);
					const notice = self.createNoticeElement(
						'notice notice-error is-dismissible',
						(self.i18n('Error saving settings:', 'wp-powersuite') || 'Error saving settings:') + ' ' + (err.message || self.i18n('Unknown error', 'wp-powersuite') || 'Unknown error')
					);
					self.insertCustomPageSaveNotice(form, notice);
					setTimeout(function() { notice.remove(); }, 5000);
				});
		},

		/**
		 * Update bulk actions visibility
		 */
		updateBulkActions: function() {
			const bulkActions = document.querySelector('.wp-powersuite-bulk-actions');
			if (bulkActions) {
				bulkActions.style.display = this.selectedModules.length > 0 ? 'flex' : 'none';
				const selectedCount = bulkActions.querySelector('.wp-powersuite-bulk-actions__selected strong');
				if (selectedCount) {
					selectedCount.textContent = this.selectedModules.length;
				}
			}
		},

		/**
		 * Update select all checkbox state
		 */
		updateSelectAll: function() {
			const selectAll = document.getElementById('wp-powersuite-select-all');
			if (selectAll) {
				const checkboxes = document.querySelectorAll('.wp-powersuite-module-checkbox');
				const checkedCount = Array.from(checkboxes).filter(cb => cb.checked).length;
				selectAll.checked = checkedCount === checkboxes.length && checkboxes.length > 0;
				selectAll.indeterminate = checkedCount > 0 && checkedCount < checkboxes.length;
			}
		},

		/**
		 * Clear selection
		 */
		clearSelection: function() {
			this.selectedModules = [];
			const checkboxes = document.querySelectorAll('.wp-powersuite-module-checkbox');
			checkboxes.forEach(cb => cb.checked = false);
			const selectAll = document.getElementById('wp-powersuite-select-all');
			if (selectAll) {
				selectAll.checked = false;
				selectAll.indeterminate = false;
			}
			this.updateBulkActions();
		},

		/**
		 * Bulk disable modules
		 */
		bulkDisableModules: function() {
			if (this.selectedModules.length === 0) return;

			const modulesToDisable = this.selectedModules.filter(moduleId => this.enabled.includes(moduleId));
			if (modulesToDisable.length === 0) {
				alert(this.i18n('No enabled modules selected to disable.', 'wp-powersuite') || 'No enabled modules selected to disable.');
				return;
			}

			this.bulkAborted = false;
			let completed = 0;
			let failed = 0;
			const total = modulesToDisable.length;

			this.showBulkLoader(true, this.i18n('Disabling modules...', 'wp-powersuite') || 'Disabling modules...', true);

			const processNext = () => {
				if (this.bulkAborted) {
					this.showBulkComplete(completed, failed, 'disable', true);
					return;
				}
				if (completed + failed >= total) {
					this.showBulkComplete(completed, failed, 'disable', false);
					return;
				}

				const currentIndex = completed + failed;
				const moduleId = modulesToDisable[currentIndex];

				this.toggleModule(moduleId, false, true).then(() => {
					completed++;
					const done = completed + failed;
					const progress = Math.round((done / total) * 100);
					this.updateBulkLoaderProgress(progress, (this.i18n('Disabling...', 'wp-powersuite') || 'Disabling...') + ' ' + done + '/' + total + ' ' + (this.i18n('complete', 'wp-powersuite') || 'complete'));
					processNext();
				}).catch(() => {
					failed++;
					const done = completed + failed;
					const progress = Math.round((done / total) * 100);
					this.updateBulkLoaderProgress(progress, (this.i18n('Disabling...', 'wp-powersuite') || 'Disabling...') + ' ' + done + '/' + total + ' ' + (this.i18n('complete', 'wp-powersuite') || 'complete'));
					processNext();
				});
			};

			requestAnimationFrame(() => processNext());
		},

		/**
		 * Show/hide bulk loader. showCancel: show Cancel button during progress.
		 */
		showBulkLoader: function(show, message = '', showCancel = false) {
			let loader = document.getElementById('wp-powersuite-bulk-loader');
			if (!loader) {
				loader = document.createElement('div');
				loader.id = 'wp-powersuite-bulk-loader';
				loader.className = 'wp-powersuite-bulk-loader';
				loader.setAttribute('role', 'dialog');
				loader.setAttribute('aria-modal', 'true');
				loader.setAttribute('aria-labelledby', 'wp-powersuite-bulk-loader-title');
				loader.innerHTML = `
					<div class="wp-powersuite-bulk-loader__content">
						<div class="wp-powersuite-bulk-loader__progress-area">
							<span class="spinner is-active wp-powersuite-bulk-loader__spinner"></span>
							<div class="wp-powersuite-bulk-loader__text">
								<p id="wp-powersuite-bulk-loader-title" class="wp-powersuite-bulk-loader__message">${message}</p>
								<div class="wp-powersuite-bulk-loader__progress">
									<div class="wp-powersuite-bulk-loader__progress-bar" style="width: 0%"></div>
								</div>
							</div>
						</div>
						<div class="wp-powersuite-bulk-loader__actions wp-powersuite-bulk-loader__actions--progress" style="display: none;">
							<button type="button" class="button wp-powersuite-bulk-loader-cancel">${this.i18n('Cancel', 'wp-powersuite') || 'Cancel'}</button>
						</div>
						<div class="wp-powersuite-bulk-loader__result-area" style="display: none;">
							<p class="wp-powersuite-bulk-loader__result-message"></p>
							<button type="button" class="button button-primary wp-powersuite-bulk-loader-done">${this.i18n('Done', 'wp-powersuite') || 'Done'}</button>
						</div>
					</div>
				`;
				document.body.appendChild(loader);

				loader.querySelector('.wp-powersuite-bulk-loader-cancel').addEventListener('click', () => {
					this.bulkAborted = true;
				});
				loader.querySelector('.wp-powersuite-bulk-loader-done').addEventListener('click', () => {
					loader.style.display = 'none';
					this.clearSelection();
					this.applyFilters();
					this.renderModules();
				});
			}

			if (show) {
				loader.style.display = 'flex';
				const progressArea = loader.querySelector('.wp-powersuite-bulk-loader__progress-area');
				const actionsProgress = loader.querySelector('.wp-powersuite-bulk-loader__actions--progress');
				const resultArea = loader.querySelector('.wp-powersuite-bulk-loader__result-area');
				if (progressArea) progressArea.style.display = 'flex';
				if (actionsProgress) actionsProgress.style.display = showCancel ? 'flex' : 'none';
				if (resultArea) resultArea.style.display = 'none';
				const spinner = loader.querySelector('.wp-powersuite-bulk-loader__spinner');
				if (spinner) spinner.style.display = 'inline-block';
				const progressBar = loader.querySelector('.wp-powersuite-bulk-loader__progress-bar');
				if (progressBar) progressBar.style.width = '0%';
				const msgEl = loader.querySelector('.wp-powersuite-bulk-loader__message');
				if (msgEl) msgEl.textContent = message;
				loader.offsetHeight;
			} else {
				loader.style.display = 'none';
			}
		},

		/**
		 * Show bulk completion message and Done button (replaces progress UI).
		 */
		showBulkComplete: function(successCount, failedCount, action, cancelled) {
			const loader = document.getElementById('wp-powersuite-bulk-loader');
			if (!loader) return;

			const progressArea = loader.querySelector('.wp-powersuite-bulk-loader__progress-area');
			const actionsProgress = loader.querySelector('.wp-powersuite-bulk-loader__actions--progress');
			const resultArea = loader.querySelector('.wp-powersuite-bulk-loader__result-area');
			const resultMessage = loader.querySelector('.wp-powersuite-bulk-loader__result-message');
			const progressBar = loader.querySelector('.wp-powersuite-bulk-loader__progress-bar');
			const spinner = loader.querySelector('.wp-powersuite-bulk-loader__spinner');

			if (progressArea) progressArea.style.display = 'none';
			if (actionsProgress) actionsProgress.style.display = 'none';
			if (resultArea) resultArea.style.display = 'block';
			if (spinner) spinner.style.display = 'none';
			if (progressBar) progressBar.style.width = '100%';

			const actionLabel = action === 'enable' ? (this.i18n('enabled', 'wp-powersuite') || 'enabled') : (this.i18n('disabled', 'wp-powersuite') || 'disabled');
			let msg;
			if (cancelled) {
				msg = (this.i18n('Cancelled.', 'wp-powersuite') || 'Cancelled.') + ' ' + successCount + ' ' + actionLabel + (successCount === 1 ? '' : '') + (failedCount > 0 ? '. ' + failedCount + ' ' + (this.i18n('failed', 'wp-powersuite') || 'failed') + '.' : '.');
			} else if (failedCount > 0) {
				msg = successCount + ' ' + (this.i18n('module(s)', 'wp-powersuite') || 'module(s)') + ' ' + actionLabel + '. ' + failedCount + ' ' + (this.i18n('failed', 'wp-powersuite') || 'failed') + '.';
			} else {
				msg = successCount + ' ' + (this.i18n('module(s)', 'wp-powersuite') || 'module(s)') + ' ' + actionLabel + ' ' + (this.i18n('successfully.', 'wp-powersuite') || 'successfully.');
			}
			if (resultMessage) resultMessage.textContent = msg;
			this.applyFilters();
			this.renderModules();
		},

		/**
		 * Update bulk loader progress (message and bar).
		 */
		updateBulkLoaderProgress: function(progress, message = '') {
			const loader = document.getElementById('wp-powersuite-bulk-loader');
			if (!loader) return;
			if (loader.style.display !== 'flex') {
				loader.style.display = 'flex';
			}
			const progressBar = loader.querySelector('.wp-powersuite-bulk-loader__progress-bar');
			if (progressBar) {
				progressBar.offsetHeight;
				progressBar.style.width = progress + '%';
			}
			const msgEl = loader.querySelector('.wp-powersuite-bulk-loader__message');
			if (msgEl && message) msgEl.textContent = message;
		},

		/**
		 * Initialize sortable for drag-and-drop module ordering
		 */
		initSortable: function() {
			const container = this.getModulesListEl();
			if (!container || typeof jQuery === 'undefined' || !jQuery.fn.sortable) {
				return;
			}

			const $container = jQuery(container);
			const effectiveViewMode = this.getEffectiveViewMode();
			const selector = effectiveViewMode === 'grid' ? '.wp-powersuite-module--grid' : '.wp-powersuite-module--list';

			// Destroy existing sortable if any
			if ($container.data('ui-sortable')) {
				$container.sortable('destroy');
			}

			// Only make sortable if no explicit sort is active
			if (this.sortBy) {
				return;
			}

			const self = this;
			$container.sortable({
				items: selector,
				handle: effectiveViewMode === 'grid' ? '.wp-powersuite-module__header' : '.wp-powersuite-module--list',
				cursor: 'move',
				opacity: 0.8,
				tolerance: 'pointer',
				axis: false,
				placeholder: 'wp-powersuite-module-placeholder',
				update: function() {
					self.saveModuleOrder();
				}
			});
		},

		/**
		 * Save module order via AJAX
		 */
		saveModuleOrder: function() {
			const container = this.getModulesListEl();
			if (!container) {
				return;
			}

			const effectiveViewMode = this.getEffectiveViewMode();
			const selector = effectiveViewMode === 'grid' ? '.wp-powersuite-module--grid' : '.wp-powersuite-module--list';
			const modules = container.querySelectorAll(selector);
			const order = [];

			modules.forEach((module) => {
				const moduleId = module.dataset.moduleId;
				if (moduleId) {
					order.push(moduleId);
				}
			});

			if (order.length === 0) {
				return;
			}

			const self = this;
			jQuery.ajax({
				url: window.wpPowerSuite.ajaxUrl,
				type: 'POST',
				data: {
					action: window.wpPowerSuite.action.saveModuleOrder,
					nonce: window.wpPowerSuite.nonce,
					module_order: order
				},
				success: function(response) {
					if (response.success) {
						self.moduleOrder = order;
					} else {
						console.error('WP PowerSuite: Failed to save module order', response);
					}
				},
				error: function(xhr, status, error) {
					console.error('WP PowerSuite: Failed to save module order', status, error);
				}
			});
		}
	};

	function onReady() {
		WPPSAdmin.init();
		document.querySelectorAll('.wp-powersuite-custom-page-save').forEach(function(btn) {
			btn.addEventListener('click', function() {
				WPPSAdmin.saveCustomPageSettings(this);
			});
		});
		var loginCustomizorForm = document.querySelector('.wpps-login-customizor-page form.wp-powersuite-module-settings-form[data-module-id="login-page-customizor"]');
		if (loginCustomizorForm) {
			WPPSAdmin.initCustomAdminCssEditor(loginCustomizorForm);
		}
	}

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

// Captcha "Test connection" handler (roadmap #13).
//
// The admin UI contains a <div class="wpps-captcha-test-widget"> placeholder next
// to each "Run test" button. When the button is clicked we:
//   1. Read the Site Key from the settings form (the one the admin just typed).
//   2. Load the provider JS (Turnstile or reCAPTCHA) on demand — never at page load.
//   3. Render a real widget inside the placeholder. The widget callback hands us
//      a freshly-minted token which we relay to the server-side siteverify endpoint.
//   4. Show OK / Error with the provider error codes (invalid secret, domain
//      mismatch, timeout, etc.) next to the button.
//
// Keeping all of this gated behind the button click means the admin settings list
// page, and any non-captcha settings screen, still pays zero runtime cost.
(function () {
	var STATUS_OK_COLOR = '#005a00';
	var STATUS_ERR_COLOR = '#b32d2e';
	var TURNSTILE_SCRIPT = 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit';
	var RECAPTCHA_SCRIPT_V2 = 'https://www.google.com/recaptcha/api.js?render=explicit';
	var RECAPTCHA_SCRIPT_V3 = 'https://www.google.com/recaptcha/api.js?render='; // sitekey appended per-request
	var loaderPromises = {};

	function loadScriptOnce(src, markerFn) {
		if (loaderPromises[src]) {
			return loaderPromises[src];
		}
		loaderPromises[src] = new Promise(function (resolve, reject) {
			if (markerFn && markerFn()) {
				resolve();
				return;
			}
			var s = document.createElement('script');
			s.src = src;
			s.async = true;
			s.defer = true;
			s.onload = function () { resolve(); };
			s.onerror = function () { reject(new Error('Failed to load: ' + src)); };
			document.head.appendChild(s);
		});
		return loaderPromises[src];
	}

	function waitFor(predicate, timeoutMs) {
		return new Promise(function (resolve, reject) {
			var startedAt = Date.now();
			(function poll() {
				if (predicate()) { resolve(); return; }
				if (Date.now() - startedAt > timeoutMs) { reject(new Error('timeout')); return; }
				setTimeout(poll, 100);
			})();
		});
	}

	function setStatus(statusEl, message, ok) {
		if (!statusEl) { return; }
		statusEl.textContent = message;
		statusEl.style.color = ok ? STATUS_OK_COLOR : STATUS_ERR_COLOR;
	}

	function postVerify(moduleId, nonce, token) {
		if (!window.wpPowerSuite || !window.wpPowerSuite.ajaxUrl) {
			return Promise.reject(new Error('ajax URL not available'));
		}
		var fd = new FormData();
		fd.append('action', 'wp_powersuite_captcha_test_connection');
		fd.append('module_id', moduleId);
		fd.append('nonce', nonce);
		fd.append('token', token);
		return fetch(window.wpPowerSuite.ajaxUrl, { method: 'POST', body: fd }).then(function (r) { return r.json(); });
	}

	function getSiteKeyFromForm(moduleId, placeholder) {
		var field = placeholder && placeholder.getAttribute('data-sitekey-field');
		if (field) {
			var el = document.getElementById(field);
			if (el && el.value) { return el.value.trim(); }
		}
		var fallback = document.getElementById(moduleId + '_site_key');
		return fallback && fallback.value ? fallback.value.trim() : '';
	}

	function getSettingFromPlaceholder(placeholder, dataKey, fallback) {
		if (!placeholder) {
			return fallback;
		}
		var fieldId = placeholder.getAttribute('data-' + dataKey + '-field');
		if (!fieldId) {
			return fallback;
		}
		var el = document.getElementById(fieldId);
		if (!el || el.value === undefined) {
			return fallback;
		}
		var val = String(el.value).trim();
		return val !== '' ? val : fallback;
	}

	function isWidgetFullWidth(placeholder) {
		var fieldId = placeholder && placeholder.getAttribute('data-full-width-field');
		if (!fieldId) {
			return true;
		}
		var el = document.getElementById(fieldId);
		if (!el || el.type !== 'checkbox') {
			return true;
		}
		return el.checked;
	}

	function formatVerifyStatus(ok, data) {
		var parts = [];
		if (data && data.message) {
			parts.push((ok ? 'OK: ' : 'Error: ') + data.message);
		}
		if (data && data.codes && data.codes.length) {
			parts.push('[' + data.codes.join(', ') + ']');
		}
		return parts.join(' ') || (ok ? 'OK' : 'Failed');
	}

	function runTurnstileTest(btn, placeholder, statusEl, moduleId, nonce) {
		var sitekey = getSiteKeyFromForm(moduleId, placeholder);
		if (!sitekey) {
			setStatus(statusEl, 'Error: Please fill in the Site Key first.', false);
			return;
		}
		// Each "Run test" run is one-shot: never call turnstile.reset() after
		// siteverify — reset re-issues a challenge and fires callback again (infinite loop).
		placeholder.innerHTML = '';
		placeholder.removeAttribute('data-wpps-test-done');
		placeholder.className = 'wpps-captcha-test-widget wpps-captcha-cushion';
		if (isWidgetFullWidth(placeholder)) {
			placeholder.classList.add('wpps-captcha-core-form-fit');
		}
		var theme = getSettingFromPlaceholder(placeholder, 'theme', 'auto');
		var appearance = getSettingFromPlaceholder(placeholder, 'appearance', 'interaction-only');
		var widgetSize = isWidgetFullWidth(placeholder) ? 'flexible' : 'normal';
		setStatus(statusEl, 'Loading Turnstile...', true);
		loadScriptOnce(TURNSTILE_SCRIPT, function () { return typeof window.turnstile !== 'undefined'; })
			.then(function () { return waitFor(function () { return typeof window.turnstile !== 'undefined' && typeof window.turnstile.render === 'function'; }, 10000); })
			.then(function () {
				setStatus(statusEl, 'Awaiting Turnstile challenge...', true);
				window.turnstile.render(placeholder, {
					sitekey: sitekey,
					theme: theme,
					size: widgetSize,
					appearance: appearance,
					action: 'wpps_test_connection',
					callback: function (token) {
						if (placeholder.getAttribute('data-wpps-test-done') === '1') { return; }
						placeholder.setAttribute('data-wpps-test-done', '1');
						setStatus(statusEl, 'Verifying token with Cloudflare...', true);
						postVerify(moduleId, nonce, token)
							.then(function (res) {
								btn.disabled = false;
								var data = (res && res.data) || res || {};
								var ok = !!(res && res.success && data && data.success);
								setStatus(statusEl, formatVerifyStatus(ok, data), ok);
							})
							.catch(function () {
								btn.disabled = false;
								setStatus(statusEl, 'Error: Request failed (network or server error).', false);
							});
					},
					'error-callback': function (err) {
						btn.disabled = false;
						setStatus(statusEl, 'Error: Turnstile error: ' + (err || 'unknown'), false);
					},
					'timeout-callback': function () {
						btn.disabled = false;
						setStatus(statusEl, 'Error: Turnstile challenge timed out.', false);
					}
				});
			})
			.catch(function (err) {
				btn.disabled = false;
				setStatus(statusEl, 'Error: Could not load Turnstile script: ' + err.message, false);
			});
	}

	function runRecaptchaV2Test(btn, placeholder, statusEl, moduleId, nonce, sitekey) {
		loadScriptOnce(RECAPTCHA_SCRIPT_V2, function () { return typeof window.grecaptcha !== 'undefined' && typeof window.grecaptcha.render === 'function'; })
			.then(function () { return waitFor(function () { return typeof window.grecaptcha !== 'undefined' && typeof window.grecaptcha.render === 'function'; }, 10000); })
			.then(function () {
				placeholder.innerHTML = '';
				placeholder.className = 'wpps-captcha-test-widget wpps-captcha-cushion';
				var theme = getSettingFromPlaceholder(placeholder, 'theme', 'light');
				setStatus(statusEl, 'Please complete the reCAPTCHA challenge...', true);
				window.grecaptcha.render(placeholder, {
					sitekey: sitekey,
					theme: theme,
					size: 'normal',
					callback: function (token) {
						setStatus(statusEl, 'Verifying token with Google...', true);
						postVerify(moduleId, nonce, token)
							.then(function (res) {
								btn.disabled = false;
								var data = (res && res.data) || res || {};
								var ok = !!(res && res.success && data && data.success);
								setStatus(statusEl, formatVerifyStatus(ok, data), ok);
								if (window.grecaptcha && window.grecaptcha.reset) { window.grecaptcha.reset(); }
							})
							.catch(function () {
								btn.disabled = false;
								setStatus(statusEl, 'Error: Request failed (network or server error).', false);
							});
					}
				});
			})
			.catch(function (err) {
				btn.disabled = false;
				setStatus(statusEl, 'Error: Could not load reCAPTCHA script: ' + err.message, false);
			});
	}

	function runRecaptchaV3Test(btn, placeholder, statusEl, moduleId, nonce, sitekey) {
		var src = RECAPTCHA_SCRIPT_V3 + encodeURIComponent(sitekey);
		loadScriptOnce(src, function () { return typeof window.grecaptcha !== 'undefined' && typeof window.grecaptcha.execute === 'function'; })
			.then(function () { return waitFor(function () { return typeof window.grecaptcha !== 'undefined' && typeof window.grecaptcha.execute === 'function'; }, 10000); })
			.then(function () {
				setStatus(statusEl, 'Requesting reCAPTCHA v3 token...', true);
				window.grecaptcha.ready(function () {
					window.grecaptcha.execute(sitekey, { action: 'wpps_test_connection' })
						.then(function (token) {
							setStatus(statusEl, 'Verifying token with Google...', true);
							return postVerify(moduleId, nonce, token);
						})
						.then(function (res) {
							btn.disabled = false;
							var data = (res && res.data) || res || {};
							var ok = !!(res && res.success && data && data.success);
							setStatus(statusEl, formatVerifyStatus(ok, data), ok);
						})
						.catch(function (err) {
							btn.disabled = false;
							setStatus(statusEl, 'Error: reCAPTCHA v3 error: ' + (err && err.message ? err.message : 'unknown'), false);
						});
				});
			})
			.catch(function (err) {
				btn.disabled = false;
				setStatus(statusEl, 'Error: Could not load reCAPTCHA v3 script: ' + err.message, false);
			});
	}

	function runRecaptchaTest(btn, placeholder, statusEl, moduleId, nonce) {
		var sitekey = getSiteKeyFromForm(moduleId, placeholder);
		if (!sitekey) {
			setStatus(statusEl, 'Error: Please fill in the Site Key first.', false);
			return;
		}
		var versionField = placeholder.getAttribute('data-version-field');
		var versionEl = versionField ? document.getElementById(versionField) : null;
		var version = versionEl && versionEl.value === 'v3' ? 'v3' : 'v2';
		if (version === 'v3') {
			runRecaptchaV3Test(btn, placeholder, statusEl, moduleId, nonce, sitekey);
		} else {
			runRecaptchaV2Test(btn, placeholder, statusEl, moduleId, nonce, sitekey);
		}
	}


	document.addEventListener('click', function (e) {
		var btn = e.target && e.target.closest ? e.target.closest('.wpps-captcha-test-connection') : null;
		if (!btn) { return; }
		e.preventDefault();
		var moduleId = btn.getAttribute('data-module-id') || '';
		var provider = btn.getAttribute('data-provider') || (moduleId === 'google-recaptcha' ? 'recaptcha' : 'turnstile');
		var nonce = btn.getAttribute('data-nonce') || '';
		// Classic form-table markup uses <tr>; Turnstile modal uses div fields inside accordions.
		var scope = btn.closest('tr') || btn.closest('.wpps-ts-field__control') || btn.closest('td');
		var placeholder = scope ? scope.querySelector('.wpps-captcha-test-widget') : null;
		var statusEl = scope ? scope.querySelector('p.wpps-captcha-test-status[role="status"], p.description[role="status"]') : null;
		if (!placeholder) {
			setStatus(statusEl, 'Error: Widget container missing.', false);
			return;
		}
		btn.disabled = true;
		if (provider === 'recaptcha') {
			runRecaptchaTest(btn, placeholder, statusEl, moduleId, nonce);
		} else {
			runTurnstileTest(btn, placeholder, statusEl, moduleId, nonce);
		}
	});
})();
