/**
 * WP PowerSuite — Accessibility Widget (frontend behavior).
 *
 * Vanilla JS, no dependencies. Visitor preferences persist in localStorage.
 *
 * Design:
 * - Page-level visual filters compose into one CSS custom property
 *   (--wpps-a11y-filter) applied to the root element (<html>). Per the Filter
 *   Effects spec, a filter on the document root does NOT create a containing
 *   block for fixed-position descendants, so sticky headers, cookie banners,
 *   and chat widgets keep working while a filter is active.
 * - The widget UI and all overlays are relocated under <html> (outside <body>)
 *   so body-level contrast classes never tint the widget.
 * - Stored preferences are validated against the admin's enabled-feature map
 *   (config.features); preferences for disabled tools are ignored.
 * - The panel is a non-modal dialog: focus is managed on open/close and Escape
 *   / outside-click close it, but focus is NOT trapped (avoids keyboard traps).
 */
(function () {
	'use strict';

	var config = window.wppsA11yWidget || {};
	var storageKey = config.storageKey || 'wpps_a11y_prefs';
	// wp_localize_script casts top-level scalars to strings ("" / "1"), so
	// accept both the boolean and its stringified forms. Default: on.
	function boolConfig(value) {
		return value === undefined || value === true || value === 1 || value === '1';
	}
	var savePreferences = boolConfig(config.savePreferences);
	var i18n = config.i18n || {};
	var profiles = config.profiles || {};

	/* Snake_case data-feature -> camelCase preference key. */
	var SNAKE_TO_CAMEL = {
		line_height: 'lineHeight',
		letter_spacing: 'letterSpacing',
		word_spacing: 'wordSpacing',
		readable_font: 'readableFont',
		dyslexia_font: 'dyslexiaFont',
		text_align_left: 'textAlignLeft',
		high_contrast: 'highContrast',
		light_contrast: 'lightContrast',
		dark_contrast: 'darkContrast',
		grayscale: 'grayscale',
		invert_colors: 'invertColors',
		low_saturation: 'lowSaturation',
		high_saturation: 'highSaturation',
		protanopia: 'protanopia',
		deuteranopia: 'deuteranopia',
		tritanopia: 'tritanopia',
		highlight_links: 'highlightLinks',
		highlight_headings: 'highlightHeadings',
		large_cursor: 'largeCursor',
		big_black_cursor: 'bigBlackCursor',
		reading_guide: 'readingGuide',
		reading_mask: 'readingMask',
		skip_to_content: 'skipToContent',
		focus_outlines: 'focusOutlines',
		underline_links: 'underlineLinks',
		tooltips: 'tooltips',
		page_structure: 'pageStructure',
		pause_animations: 'pauseAnimations',
		hide_images: 'hideImages',
		mute_media: 'muteMedia',
		simplify_layout: 'simplifyLayout'
	};

	/* camelCase pref -> body class (plain class toggles, no filters). */
	var CLASS_MAP = {
		readableFont: 'wpps-a11y-readable-font',
		dyslexiaFont: 'wpps-a11y-dyslexia-font',
		textAlignLeft: 'wpps-a11y-text-align-left',
		highContrast: 'wpps-a11y-high-contrast',
		lightContrast: 'wpps-a11y-light-contrast',
		darkContrast: 'wpps-a11y-dark-contrast',
		highlightLinks: 'wpps-a11y-highlight-links',
		highlightHeadings: 'wpps-a11y-highlight-headings',
		largeCursor: 'wpps-a11y-large-cursor',
		bigBlackCursor: 'wpps-a11y-big-black-cursor',
		focusOutlines: 'wpps-a11y-focus-outlines',
		underlineLinks: 'wpps-a11y-underline-links',
		pauseAnimations: 'wpps-a11y-pause-animations',
		hideImages: 'wpps-a11y-hide-images',
		simplifyLayout: 'wpps-a11y-simplify-layout'
	};

	/* camelCase pref -> CSS filter fragment (composed into one chain). */
	var FILTER_MAP = {
		grayscale: 'grayscale(1)',
		invertColors: 'invert(1) hue-rotate(180deg)',
		lowSaturation: 'saturate(0.5)',
		highSaturation: 'saturate(1.6)',
		protanopia: "url('#wpps-a11y-protanopia')",
		deuteranopia: "url('#wpps-a11y-deuteranopia')",
		tritanopia: "url('#wpps-a11y-tritanopia')"
	};

	/* Mutually-exclusive groups. */
	var CONTRAST_KEYS = ['highContrast', 'lightContrast', 'darkContrast'];
	var COLOR_KEYS = ['grayscale', 'invertColors', 'lowSaturation', 'highSaturation', 'protanopia', 'deuteranopia', 'tritanopia'];
	var CURSOR_KEYS = ['largeCursor', 'bigBlackCursor'];

	/*
	 * Multi-level (stepped) controls. Each card with data-levels cycles
	 * 0..max and toggles a numbered body/root class (e.g. wpps-a11y-line-height-2).
	 * "root: true" applies to <html> so it can affect rem-based layouts.
	 */
	var LEVEL_FEATURES = {
		text_size: { pref: 'textScale', cls: 'wpps-a11y-text-scale-', root: true, max: 5 },
		line_height: { pref: 'lineHeightLevel', cls: 'wpps-a11y-line-height-', root: false, max: 3 },
		letter_spacing: { pref: 'letterSpacingLevel', cls: 'wpps-a11y-letter-spacing-', root: false, max: 3 },
		word_spacing: { pref: 'wordSpacingLevel', cls: 'wpps-a11y-word-spacing-', root: false, max: 3 }
	};

	var prefs = loadPrefs();
	var readingGuideEl = null;
	var readingGuideRaf = 0;
	var readingMaskEl = null;
	var readingMaskRaf = 0;
	var tooltipEl = null;
	var skipLinkEl = null;
	var liveRegion = null;
	var lastFocused = null;
	var mediaObserver = null;
	var mutedMedia = [];
	var roots = [];

	/* ---------------------------------------------------------------------
	 * Preference storage
	 * ------------------------------------------------------------------- */

	function loadPrefs() {
		if (!savePreferences) {
			return { textScale: 0 };
		}
		try {
			var raw = localStorage.getItem(storageKey);
			if (!raw) {
				return { textScale: 0 };
			}
			var parsed = JSON.parse(raw);
			if (!parsed || typeof parsed !== 'object') {
				return { textScale: 0 };
			}
			return sanitizePrefs(parsed);
		} catch (e) {
			return { textScale: 0 };
		}
	}

	/* True unless the admin (or license tier) turned the tool off. */
	function featureEnabled(snakeKey) {
		var features = config.features;
		if (!features || typeof features !== 'object' || !(snakeKey in features)) {
			return true;
		}
		return !!features[snakeKey];
	}

	/*
	 * Whitelist + type-check stored preferences. Unknown keys, invalid values,
	 * and preferences for tools the admin disabled are dropped.
	 */
	function sanitizePrefs(raw) {
		var out = { textScale: 0 };

		if (raw.widgetOversized) {
			out.widgetOversized = true;
		}
		if (typeof raw.activeProfile === 'string' && profiles[raw.activeProfile]) {
			out.activeProfile = raw.activeProfile;
		}

		Object.keys(LEVEL_FEATURES).forEach(function (key) {
			if (!featureEnabled(key)) {
				return;
			}
			var cfg = LEVEL_FEATURES[key];
			var level = parseInt(raw[cfg.pref], 10) || 0;
			if (level > 0) {
				out[cfg.pref] = Math.min(level, cfg.max);
			}
		});

		Object.keys(SNAKE_TO_CAMEL).forEach(function (key) {
			if (LEVEL_FEATURES[key] || !featureEnabled(key)) {
				return;
			}
			if (raw[SNAKE_TO_CAMEL[key]]) {
				out[SNAKE_TO_CAMEL[key]] = true;
			}
		});

		return out;
	}

	function savePrefs() {
		if (!savePreferences) {
			return;
		}
		try {
			localStorage.setItem(storageKey, JSON.stringify(prefs));
		} catch (e) {
			/* Storage full or unavailable — fail silently. */
		}
	}

	function snakeToCamel(key) {
		return SNAKE_TO_CAMEL[key] || key;
	}

	/* ---------------------------------------------------------------------
	 * Applying preferences to the document
	 * ------------------------------------------------------------------- */

	function applyLevels() {
		var root = document.documentElement;
		var body = document.body;
		Object.keys(LEVEL_FEATURES).forEach(function (key) {
			var cfg = LEVEL_FEATURES[key];
			var el = cfg.root ? root : body;
			if (!el) {
				return;
			}
			for (var i = 1; i <= 5; i++) {
				el.classList.remove(cfg.cls + i);
			}
			if (key === 'text_size') {
				return; // Applied per element below (works on px-based themes too).
			}
			var level = parseInt(prefs[cfg.pref], 10) || 0;
			if (level > 0) {
				el.classList.add(cfg.cls + Math.min(level, cfg.max));
			}
		});
		var scale = parseInt(prefs.textScale, 10) || 0;
		applyTextScale(Math.min(Math.max(scale, 0), LEVEL_FEATURES.text_size.max));
	}

	/*
	 * Bigger Text. Scaling the root font-size only works on rem/em-based
	 * themes; page builders and many classic themes set px sizes that ignore
	 * it. Instead, each text element's *measured* size is multiplied and set
	 * inline. The original computed and inline values are cached on the
	 * element so every level derives from the true base size (no compounding)
	 * and level 0 restores the exact original styling.
	 */
	var TEXT_SCALE_STEP = 0.12;
	var TEXT_SCALE_SELECTOR = 'h1, h2, h3, h4, h5, h6, p, li, a, span, em, strong, small, label, legend, button, input, select, textarea, td, th, dt, dd, blockquote, figcaption, code, pre';

	function applyTextScale(level) {
		var els = document.querySelectorAll(TEXT_SCALE_SELECTOR);
		var factor = 1 + level * TEXT_SCALE_STEP;
		var sizes = [];
		var i;
		var el;

		// Pass 1: measure everything first, so scaling a parent never inflates
		// the measurement of an em/%-sized child (no compounding).
		for (i = 0; i < els.length; i++) {
			el = els[i];
			if (el.closest('.wpps-a11y-widget')) {
				sizes.push(0);
				continue;
			}
			if (level > 0 && el.dataset.wppsA11yFs === undefined) {
				sizes.push(parseFloat(window.getComputedStyle(el).fontSize) || 0);
			} else {
				sizes.push(-1); // Already cached, or restoring.
			}
		}

		// Pass 2: apply.
		for (i = 0; i < els.length; i++) {
			el = els[i];
			if (sizes[i] === 0) {
				continue; // Widget UI.
			}
			if (level > 0) {
				if (el.dataset.wppsA11yFs === undefined) {
					if (sizes[i] <= 0) {
						continue;
					}
					el.dataset.wppsA11yFs = String(sizes[i]);
					el.dataset.wppsA11yFsInline = el.style.fontSize || '';
				}
				el.style.fontSize = (parseFloat(el.dataset.wppsA11yFs) * factor).toFixed(2) + 'px';
			} else if (el.dataset.wppsA11yFs !== undefined) {
				el.style.fontSize = el.dataset.wppsA11yFsInline || '';
				delete el.dataset.wppsA11yFs;
				delete el.dataset.wppsA11yFsInline;
			}
		}
	}

	function applyClasses() {
		var body = document.body;
		if (!body) {
			return;
		}
		Object.keys(CLASS_MAP).forEach(function (key) {
			body.classList.toggle(CLASS_MAP[key], !!prefs[key]);
		});
	}

	var repaintRaf = 0;

	/*
	 * Chromium keeps stale raster tiles when a large CSS `filter` (e.g.
	 * invert/grayscale) is removed, leaving "ghost" patches that only
	 * surface as you hover/scroll. Briefly flipping opacity invalidates the
	 * compositor layer and forces a clean full repaint. Opacity (unlike
	 * transform/filter) does not create a containing block, so fixed elements
	 * are not displaced.
	 */
	function forceRepaint() {
		var root = document.documentElement;
		if (repaintRaf) {
			cancelAnimationFrame(repaintRaf);
		}
		root.style.opacity = '0.99999';
		repaintRaf = requestAnimationFrame(function () {
			repaintRaf = requestAnimationFrame(function () {
				document.documentElement.style.opacity = '';
				repaintRaf = 0;
			});
		});
	}

	/*
	 * Filters live on <html>: a filter on the document root element does not
	 * create a containing block for fixed-position descendants (Filter Effects
	 * spec exception), so sticky headers / cookie banners / chat launchers
	 * keep their viewport positioning while a color filter is active.
	 */
	function applyFilters() {
		var root = document.documentElement;
		var chain = [];
		Object.keys(FILTER_MAP).forEach(function (key) {
			if (prefs[key]) {
				chain.push(FILTER_MAP[key]);
			}
		});
		var had = !!root.style.getPropertyValue('--wpps-a11y-filter');
		if (chain.length) {
			root.style.setProperty('--wpps-a11y-filter', chain.join(' '));
		} else {
			root.style.removeProperty('--wpps-a11y-filter');
			if (had) {
				forceRepaint();
			}
		}
	}

	function applyAll() {
		applyLevels();
		applyClasses();
		applyFilters();
		updateReadingGuide(!!prefs.readingGuide);
		updateReadingMask(!!prefs.readingMask);
		updateTooltips(!!prefs.tooltips);
		updateSkipLink(!!prefs.skipToContent);
		updateMuteMedia(!!prefs.muteMedia);
		updateStructurePanels(!!prefs.pageStructure);
		applyOversize();
		syncCards();
		syncProfileButtons();
	}

	function applyOversize() {
		roots.forEach(function (root) {
			root.classList.toggle('is-oversized', !!prefs.widgetOversized);
			var toggle = root.querySelector('[data-action="toggle-oversize"]');
			if (toggle) {
				toggle.setAttribute('aria-checked', prefs.widgetOversized ? 'true' : 'false');
			}
		});
	}

	/* Open the panel toward the larger half of the viewport so it is not clipped. */
	function updatePanelDirection(root) {
		var rect = root.getBoundingClientRect();
		var center = rect.top + rect.height / 2;
		root.classList.toggle('is-open-down', center < window.innerHeight / 2);
		root.classList.toggle('is-open-up', center >= window.innerHeight / 2);
	}

	/* ---------------------------------------------------------------------
	 * Reading guide
	 * ------------------------------------------------------------------- */

	function updateReadingGuide(enabled) {
		if (!enabled) {
			if (readingGuideEl && readingGuideEl.parentNode) {
				readingGuideEl.parentNode.removeChild(readingGuideEl);
			}
			readingGuideEl = null;
			document.removeEventListener('mousemove', onReadingGuideMove);
			document.removeEventListener('touchmove', onReadingGuideTouch);
			return;
		}
		if (readingGuideEl) {
			return;
		}
		readingGuideEl = document.createElement('div');
		readingGuideEl.className = 'wpps-a11y-reading-guide';
		readingGuideEl.setAttribute('aria-hidden', 'true');
		document.documentElement.appendChild(readingGuideEl);
		document.addEventListener('mousemove', onReadingGuideMove, { passive: true });
		document.addEventListener('touchmove', onReadingGuideTouch, { passive: true });
	}

	function onReadingGuideMove(e) {
		queueReadingGuide(e.clientY);
	}

	function onReadingGuideTouch(e) {
		if (e.touches && e.touches.length) {
			queueReadingGuide(e.touches[0].clientY);
		}
	}

	function queueReadingGuide(y) {
		if (!readingGuideEl || readingGuideRaf) {
			return;
		}
		readingGuideRaf = window.requestAnimationFrame(function () {
			readingGuideRaf = 0;
			if (readingGuideEl) {
				readingGuideEl.style.top = y + 'px';
			}
		});
	}

	/* ---------------------------------------------------------------------
	 * Reading mask (dims the page except a band around the pointer)
	 * ------------------------------------------------------------------- */

	function updateReadingMask(enabled) {
		if (!enabled) {
			if (readingMaskEl && readingMaskEl.parentNode) {
				readingMaskEl.parentNode.removeChild(readingMaskEl);
			}
			readingMaskEl = null;
			document.removeEventListener('mousemove', onReadingMaskMove);
			document.removeEventListener('touchmove', onReadingMaskTouch);
			return;
		}
		if (readingMaskEl) {
			return;
		}
		readingMaskEl = document.createElement('div');
		readingMaskEl.className = 'wpps-a11y-reading-mask';
		readingMaskEl.setAttribute('aria-hidden', 'true');
		readingMaskEl.style.top = Math.round(window.innerHeight / 2) + 'px';
		document.documentElement.appendChild(readingMaskEl);
		document.addEventListener('mousemove', onReadingMaskMove, { passive: true });
		document.addEventListener('touchmove', onReadingMaskTouch, { passive: true });
	}

	function onReadingMaskMove(e) {
		queueReadingMask(e.clientY);
	}

	function onReadingMaskTouch(e) {
		if (e.touches && e.touches.length) {
			queueReadingMask(e.touches[0].clientY);
		}
	}

	function queueReadingMask(y) {
		if (!readingMaskEl || readingMaskRaf) {
			return;
		}
		readingMaskRaf = window.requestAnimationFrame(function () {
			readingMaskRaf = 0;
			if (readingMaskEl) {
				readingMaskEl.style.top = y + 'px';
			}
		});
	}

	/* ---------------------------------------------------------------------
	 * Tooltips (surface title / aria-label text on hover & focus)
	 * ------------------------------------------------------------------- */

	function getTooltipText(el) {
		var node = el.closest('[title], [aria-label]');
		if (!node || node.closest('.wpps-a11y-widget')) {
			return '';
		}
		var text = node.getAttribute('aria-label') || node.getAttribute('title') || '';
		return text.replace(/\s+/g, ' ').trim();
	}

	function showTooltip(target) {
		var text = getTooltipText(target);
		if (!text) {
			hideTooltip();
			return;
		}
		if (!tooltipEl) {
			tooltipEl = document.createElement('div');
			tooltipEl.className = 'wpps-a11y-tooltip';
			tooltipEl.setAttribute('role', 'tooltip');
			document.documentElement.appendChild(tooltipEl);
		}
		var node = target.closest('[title], [aria-label]') || target;
		var rect = node.getBoundingClientRect();
		tooltipEl.textContent = text;
		tooltipEl.style.display = 'block';
		var top = rect.top - tooltipEl.offsetHeight - 8;
		if (top < 4) {
			top = rect.bottom + 8;
		}
		tooltipEl.style.top = Math.max(4, top) + 'px';
		tooltipEl.style.left = Math.max(4, rect.left) + 'px';
	}

	function hideTooltip() {
		if (tooltipEl) {
			tooltipEl.style.display = 'none';
		}
	}

	function onTooltipOver(e) {
		showTooltip(e.target);
	}

	function onTooltipOut() {
		hideTooltip();
	}

	function updateTooltips(enabled) {
		if (enabled) {
			document.addEventListener('mouseover', onTooltipOver, { passive: true });
			document.addEventListener('focusin', onTooltipOver, { passive: true });
			document.addEventListener('mouseout', onTooltipOut, { passive: true });
			document.addEventListener('focusout', onTooltipOut, { passive: true });
			return;
		}
		document.removeEventListener('mouseover', onTooltipOver);
		document.removeEventListener('focusin', onTooltipOver);
		document.removeEventListener('mouseout', onTooltipOut);
		document.removeEventListener('focusout', onTooltipOut);
		hideTooltip();
	}

	/* ---------------------------------------------------------------------
	 * Skip link
	 * ------------------------------------------------------------------- */

	function findMainTarget() {
		var selectors = ['main', '[role="main"]', '#main', '#content', '#primary', '.site-content', '.content-area'];
		for (var i = 0; i < selectors.length; i++) {
			var el = document.querySelector(selectors[i]);
			if (el && !el.closest('.wpps-a11y-widget')) {
				return el;
			}
		}
		return document.body;
	}

	function updateSkipLink(enabled) {
		if (!enabled) {
			if (skipLinkEl && skipLinkEl.parentNode) {
				skipLinkEl.parentNode.removeChild(skipLinkEl);
			}
			skipLinkEl = null;
			return;
		}
		if (skipLinkEl) {
			return;
		}
		skipLinkEl = document.createElement('a');
		skipLinkEl.className = 'wpps-a11y-skip-link';
		skipLinkEl.href = '#';
		skipLinkEl.textContent = i18n.skipLinkText || 'Skip to main content';
		skipLinkEl.addEventListener('click', function (e) {
			e.preventDefault();
			var target = findMainTarget();
			if (!target) {
				return;
			}
			if (!target.id) {
				target.id = 'wpps-a11y-skip-target';
			}
			target.setAttribute('tabindex', '-1');
			target.focus({ preventScroll: false });
		});
		document.documentElement.appendChild(skipLinkEl);
	}

	/* ---------------------------------------------------------------------
	 * Mute media (handles dynamically-added players via observer)
	 * ------------------------------------------------------------------- */

	function muteExistingMedia() {
		document.querySelectorAll('video, audio').forEach(function (el) {
			if (el.closest('.wpps-a11y-widget')) {
				return;
			}
			if (!el.dataset.wppsA11yMuted) {
				el.dataset.wppsA11yMuted = el.muted ? '1' : '0';
			}
			el.muted = true;
		});
	}

	function updateMuteMedia(enabled) {
		if (enabled) {
			muteExistingMedia();
			if (!mediaObserver && window.MutationObserver) {
				mediaObserver = new MutationObserver(debounce(function () {
					if (prefs.muteMedia) {
						muteExistingMedia();
					}
				}, 200));
				mediaObserver.observe(document.body, { childList: true, subtree: true });
			}
			return;
		}
		if (mediaObserver) {
			mediaObserver.disconnect();
			mediaObserver = null;
		}
		document.querySelectorAll('video, audio').forEach(function (el) {
			if (el.dataset.wppsA11yMuted !== undefined) {
				el.muted = el.dataset.wppsA11yMuted === '1';
				delete el.dataset.wppsA11yMuted;
			}
		});
	}

	function debounce(fn, wait) {
		var t;
		return function () {
			window.clearTimeout(t);
			t = window.setTimeout(fn, wait);
		};
	}

	/* ---------------------------------------------------------------------
	 * Page structure (headings outline)
	 * ------------------------------------------------------------------- */

	function collectHeadings() {
		var headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6');
		var items = [];
		headings.forEach(function (heading) {
			if (heading.closest('.wpps-a11y-widget')) {
				return;
			}
			var text = (heading.textContent || '').replace(/\s+/g, ' ').trim();
			if (!text) {
				return;
			}
			items.push({ level: parseInt(heading.tagName.charAt(1), 10) || 1, text: text, el: heading });
		});
		return items;
	}

	function renderStructurePanel(container) {
		var items = collectHeadings();
		container.innerHTML = '';
		if (!items.length) {
			container.textContent = i18n.noHeadings || 'No headings found on this page.';
			return;
		}
		var list = document.createElement('ul');
		items.forEach(function (item) {
			var li = document.createElement('li');
			var btn = document.createElement('button');
			btn.type = 'button';
			btn.className = 'wpps-a11y-structure-link';
			btn.style.paddingInlineStart = ((item.level - 1) * 12 + 6) + 'px';
			btn.textContent = 'H' + item.level + ': ' + item.text;
			btn.addEventListener('click', function () {
				if (item.el && typeof item.el.scrollIntoView === 'function') {
					item.el.scrollIntoView({ behavior: 'smooth', block: 'start' });
					item.el.setAttribute('tabindex', '-1');
					item.el.focus({ preventScroll: true });
				}
			});
			li.appendChild(btn);
			list.appendChild(li);
		});
		container.appendChild(list);
	}

	function updateStructurePanels(enabled) {
		roots.forEach(function (root) {
			var container = root.querySelector('.wpps-a11y-widget__panel-body');
			var panel = root.querySelector('.wpps-a11y-structure-panel');
			if (enabled && container) {
				if (!panel) {
					panel = document.createElement('div');
					panel.className = 'wpps-a11y-structure-panel';
					panel.setAttribute('role', 'navigation');
					panel.setAttribute('aria-label', i18n.pageStructure || 'Page structure');
					container.appendChild(panel);
				}
				renderStructurePanel(panel);
			} else if (panel && panel.parentNode) {
				panel.parentNode.removeChild(panel);
			}
		});
	}

	/* ---------------------------------------------------------------------
	 * State changes
	 * ------------------------------------------------------------------- */

	function clearExclusiveGroup(activeKey, groupKeys) {
		groupKeys.forEach(function (k) {
			if (k !== activeKey) {
				prefs[k] = false;
			}
		});
	}

	function setToggle(featureKey, enabled) {
		var camel = snakeToCamel(featureKey);
		if (enabled) {
			if (CONTRAST_KEYS.indexOf(camel) !== -1) { clearExclusiveGroup(camel, CONTRAST_KEYS); }
			if (COLOR_KEYS.indexOf(camel) !== -1) { clearExclusiveGroup(camel, COLOR_KEYS); }
			if (CURSOR_KEYS.indexOf(camel) !== -1) { clearExclusiveGroup(camel, CURSOR_KEYS); }
		}
		prefs[camel] = !!enabled;
		prefs.activeProfile = '';
		applyAll();
		savePrefs();
		announce(enabled ? (i18n.statusOn || 'On') : (i18n.statusOff || 'Off'));
	}

	function cycleLevel(featureKey, maxLevel) {
		var cfg = LEVEL_FEATURES[featureKey];
		if (!cfg) {
			return;
		}
		var max = Math.min(maxLevel || 3, cfg.max);
		var current = parseInt(prefs[cfg.pref], 10) || 0;
		prefs[cfg.pref] = current >= max ? 0 : current + 1;
		prefs.activeProfile = '';
		applyLevels();
		syncCards();
		savePrefs();
		var level = prefs[cfg.pref] || 0;
		if (level > 0) {
			var template = i18n.levelStatus || 'Level %1$s of %2$s';
			announce(template.replace('%1$s', String(level)).replace('%2$s', String(max)));
		} else {
			announce(i18n.levelOff || i18n.statusOff || 'Off');
		}
	}

	function toggleOversize() {
		prefs.widgetOversized = !prefs.widgetOversized;
		applyOversize();
		savePrefs();
	}

	function resetAll() {
		var hadMute = !!prefs.muteMedia;
		prefs = { textScale: 0 };
		if (hadMute) {
			updateMuteMedia(false);
		}
		if (window.speechSynthesis) {
			window.speechSynthesis.cancel();
		}
		applyAll();
		savePrefs();
		announce(i18n.preferencesReset || 'Preferences reset.');
	}

	function applyProfile(profileKey) {
		if (!window.wppsA11yPremium || typeof window.wppsA11yPremium.applyProfile !== 'function') {
			return;
		}
		var next = window.wppsA11yPremium.applyProfile(profileKey, {
			prefs: prefs,
			resetAll: resetAll,
			sanitizePrefs: sanitizePrefs,
			applyAll: applyAll,
			savePrefs: savePrefs,
			announce: announce,
			i18n: i18n
		});
		if (next && typeof next === 'object') {
			prefs = next;
		}
	}

	function isRendered(el) {
		var rects = el.getClientRects();
		return rects.length > 0 && rects[0].width > 0 && rects[0].height > 0;
	}

	function navigateLinks(direction) {
		if (window.wppsA11yPremium && typeof window.wppsA11yPremium.navigateLinks === 'function') {
			window.wppsA11yPremium.navigateLinks(direction);
		}
	}

	function readSelectionAloud() {
		if (window.wppsA11yPremium && typeof window.wppsA11yPremium.readSelectionAloud === 'function') {
			window.wppsA11yPremium.readSelectionAloud(announce, i18n);
		}
	}

	/* ---------------------------------------------------------------------
	 * Button state sync & announcements
	 * ------------------------------------------------------------------- */

	function syncCards() {
		document.querySelectorAll('.wpps-a11y-card[data-feature]').forEach(function (card) {
			var feature = card.getAttribute('data-feature');
			var cfg = LEVEL_FEATURES[feature];
			if (cfg) {
				var level = parseInt(prefs[cfg.pref], 10) || 0;
				card.setAttribute('aria-pressed', level > 0 ? 'true' : 'false');
				var dots = card.querySelectorAll('.wpps-a11y-card__dot');
				var max = dots.length;
				dots.forEach(function (dot, i) {
					dot.classList.toggle('is-on', i < Math.min(level, max));
				});
			} else {
				var camel = snakeToCamel(feature);
				card.setAttribute('aria-pressed', prefs[camel] ? 'true' : 'false');
			}
		});
	}

	function syncProfileButtons() {
		document.querySelectorAll('.wpps-a11y-profile').forEach(function (btn) {
			btn.setAttribute('aria-pressed', prefs.activeProfile === btn.getAttribute('data-profile') ? 'true' : 'false');
		});
	}

	function announce(message) {
		if (!message) {
			return;
		}
		if (!liveRegion) {
			liveRegion = document.createElement('div');
			liveRegion.className = 'wpps-a11y-sr-only';
			liveRegion.setAttribute('aria-live', 'polite');
			liveRegion.setAttribute('aria-atomic', 'true');
			document.documentElement.appendChild(liveRegion);
		}
		liveRegion.textContent = '';
		window.setTimeout(function () { liveRegion.textContent = message; }, 30);
	}

	/* ---------------------------------------------------------------------
	 * Panel open/close (non-modal: focus managed, not trapped)
	 * ------------------------------------------------------------------- */

	function getFirstFocusable(panel) {
		var focusables = panel.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
		for (var i = 0; i < focusables.length; i++) {
			if (!focusables[i].disabled && isRendered(focusables[i])) {
				return focusables[i];
			}
		}
		return null;
	}

	function openPanel(root) {
		var trigger = root.querySelector('.wpps-a11y-widget__trigger');
		var panel = root.querySelector('.wpps-a11y-widget__panel');
		if (!trigger || !panel) {
			return;
		}
		closeAllPanels(root);
		lastFocused = document.activeElement;
		panel.hidden = false;
		panel.classList.add('is-animating-in');
		window.requestAnimationFrame(function () {
			panel.classList.remove('is-animating-in');
		});
		trigger.setAttribute('aria-expanded', 'true');
		// Open toward the larger half of the viewport so the panel is never
		// clipped, regardless of where the button sits on this screen size.
		updatePanelDirection(root);
		if (prefs.pageStructure) {
			updateStructurePanels(true);
		}
		var first = getFirstFocusable(panel);
		if (first) {
			first.focus();
		}
	}

	function closePanel(root, returnFocus) {
		var trigger = root.querySelector('.wpps-a11y-widget__trigger');
		var panel = root.querySelector('.wpps-a11y-widget__panel');
		if (!trigger || !panel || panel.hidden) {
			return;
		}
		panel.hidden = true;
		trigger.setAttribute('aria-expanded', 'false');
		var modal = root.querySelector('.wpps-a11y-widget__statement-modal');
		if (modal) {
			modal.hidden = true;
		}
		if (returnFocus) {
			if (lastFocused && typeof lastFocused.focus === 'function' && document.contains(lastFocused)) {
				lastFocused.focus();
			} else {
				trigger.focus();
			}
		}
	}

	function closeAllPanels(except) {
		roots.forEach(function (root) {
			if (root !== except) {
				closePanel(root, false);
			}
		});
	}

	function isPanelOpen(root) {
		var panel = root.querySelector('.wpps-a11y-widget__panel');
		return panel && !panel.hidden;
	}

	function toggleStatement(root, open) {
		var modal = root.querySelector('.wpps-a11y-widget__statement-modal');
		if (!modal) {
			return;
		}
		if (open) {
			modal.hidden = false;
			var close = modal.querySelector('[data-action="close-statement"]');
			if (close) {
				close.focus();
			}
		} else {
			modal.hidden = true;
			var opener = root.querySelector('[data-action="open-statement"]');
			if (opener) {
				opener.focus();
			}
		}
	}

	function isStatementOpen(root) {
		var modal = root.querySelector('.wpps-a11y-widget__statement-modal');
		return modal && !modal.hidden;
	}

	/* ---------------------------------------------------------------------
	 * Binding
	 * ------------------------------------------------------------------- */

	function bindWidget(root) {
		var trigger = root.querySelector('.wpps-a11y-widget__trigger');
		var panel = root.querySelector('.wpps-a11y-widget__panel');
		var closeBtn = root.querySelector('.wpps-a11y-widget__close');
		var resetBtn = root.querySelector('.wpps-a11y-widget__reset-all');
		if (!trigger || !panel) {
			return;
		}

		trigger.addEventListener('click', function () {
			if (isPanelOpen(root)) {
				closePanel(root, true);
			} else {
				openPanel(root);
			}
		});

		if (closeBtn) {
			closeBtn.addEventListener('click', function () { closePanel(root, true); });
		}
		if (resetBtn) {
			resetBtn.addEventListener('click', function () { resetAll(); });
		}

		panel.addEventListener('click', function (e) {
			var profileBtn = e.target.closest('.wpps-a11y-profile');
			if (profileBtn) {
				applyProfile(profileBtn.getAttribute('data-profile'));
				return;
			}

			var oversize = e.target.closest('[data-action="toggle-oversize"]');
			if (oversize) {
				toggleOversize();
				return;
			}

			if (e.target.closest('[data-action="open-statement"]')) {
				toggleStatement(root, true);
				return;
			}
			if (e.target.closest('[data-action="close-statement"]')) {
				toggleStatement(root, false);
				return;
			}

			var card = e.target.closest('.wpps-a11y-card');
			if (!card) {
				return;
			}

			var feature = card.getAttribute('data-feature');
			if (LEVEL_FEATURES[feature]) {
				cycleLevel(feature, parseInt(card.getAttribute('data-levels'), 10) || 3);
				return;
			}
			if (feature) {
				setToggle(feature, !prefs[snakeToCamel(feature)]);
				return;
			}

			switch (card.getAttribute('data-action')) {
				case 'next-link': navigateLinks(1); break;
				case 'read-selection': readSelectionAloud(); break;
				default: break;
			}
		});

		// Escape closes the panel; listener scoped to the widget root (no
		// document-level duplicates across multiple widget instances).
		root.addEventListener('keydown', function (e) {
			if (e.key !== 'Escape') {
				return;
			}
			if (isStatementOpen(root)) {
				e.preventDefault();
				toggleStatement(root, false);
				return;
			}
			if (isPanelOpen(root)) {
				e.preventDefault();
				closePanel(root, true);
			}
		});
	}

	function relocateRoot(root) {
		// Move the widget outside <body> so page-level filters never tint it.
		if (root.parentNode !== document.documentElement) {
			document.documentElement.appendChild(root);
		}
	}

	function ensureSvgFilters() {
		if (window.wppsA11yPremium && typeof window.wppsA11yPremium.ensureSvgFilters === 'function') {
			window.wppsA11yPremium.ensureSvgFilters();
		}
	}

	/* ---------------------------------------------------------------------
	 * Init
	 * ------------------------------------------------------------------- */

	function init() {
		ensureSvgFilters();
		roots = Array.prototype.slice.call(document.querySelectorAll('[data-wpps-a11y-root]'));
		roots.forEach(function (root) {
			relocateRoot(root);
			bindWidget(root);
		});

		applyAll();

		// Single global outside-click handler closes any open panel.
		document.addEventListener('click', function (e) {
			roots.forEach(function (root) {
				if (isPanelOpen(root) && !root.contains(e.target)) {
					closePanel(root, false);
				}
			});
		});

		// Keep the open panel directed toward available space on resize/rotate.
		var resizeTimer = 0;
		window.addEventListener('resize', function () {
			window.clearTimeout(resizeTimer);
			resizeTimer = window.setTimeout(function () {
				roots.forEach(function (root) {
					if (isPanelOpen(root)) {
						updatePanelDirection(root);
					}
				});
			}, 150);
		});
	}

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