PageSourceSearch

https://www.eoppep.gr/wp-content/plugins/open-accessibility/assets/js/open-accessibility-public.js?ver=1.4.0

js eoppep.gr collected 2026-09-24 18:07:26 UTC 76,616 bytes, 2,000 lines download raw bytes

1/**
2 * Frontend JavaScript for the Open Accessibility plugin
3 *
4 * Handles the accessibility widget functionality
5 */
6
7(function($) {
8    'use strict';
9
10    // Store state in local storage - site-wide key
11    const storageKey = 'open-accessibility-settings';
12    const DEFAULT_ACCESSIBILITY_STATE = {
13        active: false,
14        contrast: '',
15        grayscale: false,
16        textSize: 0,
17        selectedFont: 'default',
18        linksUnderline: false,
19        hideImages: false,
20        readingGuide: false,
21        focusOutline: false,
22        lineHeightLevel: 0,
23        textAlign: '',
24        pauseAnimations: false,
25        letterSpacingLevel: 0,
26        wordSpacingLevel: 0
27    };
28    let accessibilityState = Object.assign({}, DEFAULT_ACCESSIBILITY_STATE);
29
30    const MAX_SPACING_LEVEL = 3;
31    const MAX_TEXT_SIZE = 5;
32    const VALID_CONTRAST_MODES = ['', 'high', 'negative', 'light', 'dark'];
33    const VALID_FONT_VALUES = ['default', 'atkinson', 'opendyslexic'];
34    const VALID_TEXT_ALIGN_VALUES = ['', 'left', 'center', 'right'];
35
36    let isShortcodeEmbed = false;
37    const DEFAULT_TARGET_CONFIG = {
38        roots: [
39            'main',
40            'article',
41            '[role="main"]',
42            '.entry-content',
43            '.post-content',
44            '.page-content',
45            '.wp-block-post-content',
46            '.site-content',
47            '.site-main',
48            '#content',
49            '#primary'
50        ],
51        groups: {
52            readable_text: [
53                'p',
54                'li',
55                'blockquote',
56                'dd',
57                'dt',
58                'figcaption',
59                'caption',
60                'td',
61                'th',
62                'label'
63            ],
64            headings: [
65                'h1',
66                'h2',
67                'h3',
68                'h4',
69                'h5',
70                'h6'
71            ],
72            links: [
73                'a[href]'
74            ],
75            media: [
76                'img',
77                'picture',
78                'video',
79                'audio',
80                'iframe',
81                'embed',
82                'object',
83                'svg',
84                'canvas'
85            ],
86            interactive: [
87                'a[href]',
88                'button',
89                'input',
90                'select',
91                'textarea',
92                'summary',
93                '[tabindex]:not([tabindex="-1"])'
94            ]
95        },
96        layout_containers: [
97            '[data-oa-relax-layout]'
98        ],
99        excluded: [
100            '.open-accessibility-widget-wrapper',
101            '.open-accessibility-reading-guide',
102            '.open-accessibility-skip-to-content-link',
103            '.open-accessibility-skip-to-content-backdrop',
104            '.open-accessibility-ignore',
105            '[data-oa-ignore]',
106            'aside',
107            '[role="complementary"]',
108            '.sidebar',
109            '.sidebar-primary',
110            '.sidebar-secondary',
111            '.widget',
112            '.widget-area',
113            'nav',
114            '.nav',
115            '.menu',
116            '.navigation',
117            '.pagination',
118            '.breadcrumbs',
119            '.breadcrumb',
120            'code',
121            'pre',
122            'kbd',
123            'samp',
124            '.screen-reader-text'
125        ]
126    };
127    const TEXT_SIZE_MULTIPLIERS = [1, 1.1, 1.2, 1.3, 1.4, 1.5];
128    const LINE_HEIGHT_MULTIPLIERS = [0, 1.6, 1.8, 2.0];
129    const LETTER_SPACING_STEPS = [0, 0.12, 0.18, 0.24];
130    const WORD_SPACING_STEPS = [0, 0.16, 0.24, 0.32];
131
132    let targetResolver = null;
133
134    function clampLevel(value, maxLevel) {
135        const parsed = Number.parseInt(value, 10);
136
137        if (!Number.isFinite(parsed)) {
138            return 0;
139        }
140
141        return Math.min(Math.max(parsed, 0), maxLevel);
142    }
143
144    function normalizeChoice(value, validValues, fallback) {
145        return validValues.includes(value) ? value : fallback;
146    }
147
148    function normalizeAccessibilityState(state) {
149        const source = state && typeof state === 'object' ? state : {};
150
151        return {
152            active: Boolean(source.active),
153            contrast: normalizeChoice(source.contrast, VALID_CONTRAST_MODES, ''),
154            grayscale: Boolean(source.grayscale),
155            textSize: clampLevel(source.textSize, MAX_TEXT_SIZE),
156            selectedFont: normalizeChoice(source.selectedFont, VALID_FONT_VALUES, 'default'),
157            linksUnderline: Boolean(source.linksUnderline),
158            hideImages: Boolean(source.hideImages),
159            readingGuide: Boolean(source.readingGuide),
160            focusOutline: Boolean(source.focusOutline),
161            lineHeightLevel: clampLevel(source.lineHeightLevel, MAX_SPACING_LEVEL),
162            textAlign: normalizeChoice(source.textAlign, VALID_TEXT_ALIGN_VALUES, ''),
163            pauseAnimations: Boolean(source.pauseAnimations),
164            letterSpacingLevel: clampLevel(source.letterSpacingLevel, MAX_SPACING_LEVEL),
165            wordSpacingLevel: clampLevel(source.wordSpacingLevel, MAX_SPACING_LEVEL)
166        };
167    }
168
169    function isDebugStorageEnabled() {
170        try {
171            return typeof localStorage !== 'undefined' &&
172                localStorage.getItem('openAccessibilityDebug') === '1';
173        } catch (error) {
174            return false;
175        }
176    }
177
178    const debugEnabled = Boolean(
179        typeof open_accessibility_data !== 'undefined' &&
180        open_accessibility_data &&
181        open_accessibility_data.options &&
182        open_accessibility_data.options.debug
183    ) || isDebugStorageEnabled();
184
185    function normalizeSelectorList(value, fallback) {
186        const fallbackSelectors = Array.isArray(fallback) ? fallback : [];
187
188        if (!Array.isArray(value)) {
189            return fallbackSelectors.slice();
190        }
191
192        const selectors = value
193            .filter((selector) => typeof selector === 'string' && selector.trim().length > 0)
194            .map((selector) => selector.trim());
195
196        return Array.from(new Set(selectors));
197    }
198
199    function normalizeTargetConfig(config) {
200        const source = config && typeof config === 'object' ? config : {};
201        const hasOwn = Object.prototype.hasOwnProperty;
202        const hasGroups = hasOwn.call(source, 'groups') && source.groups && typeof source.groups === 'object';
203        const sourceGroups = hasGroups ? source.groups : DEFAULT_TARGET_CONFIG.groups;
204        const normalizedGroups = {};
205
206        Object.keys(sourceGroups).forEach((groupName) => {
207            normalizedGroups[groupName] = normalizeSelectorList(
208                sourceGroups[groupName],
209                hasGroups ? [] : DEFAULT_TARGET_CONFIG.groups[groupName]
210            );
211        });
212
213        const roots = normalizeSelectorList(source.roots, DEFAULT_TARGET_CONFIG.roots);
214        const hasExplicitEmptyRoots = hasOwn.call(source, 'roots') &&
215            Array.isArray(source.roots) &&
216            roots.length === 0;
217
218        return {
219            roots,
220            groups: normalizedGroups,
221            layoutContainers: normalizeSelectorList(source.layout_containers, DEFAULT_TARGET_CONFIG.layout_containers),
222            excluded: normalizeSelectorList(source.excluded, DEFAULT_TARGET_CONFIG.excluded),
223            useBodyFallback: !hasExplicitEmptyRoots
224        };
225    }
226
227    const targetConfig = normalizeTargetConfig(
228        typeof open_accessibility_data !== 'undefined' &&
229        open_accessibility_data &&
230        open_accessibility_data.options &&
231        open_accessibility_data.options.target_config
232            ? open_accessibility_data.options.target_config
233            : {}
234    );
235
236    function exposeOpenAccessibilityApi() {
237        const api = window.OpenAccessibility || {};
238
239        api.refresh = function() {
240            targetResolver.refresh();
241            applyState();
242        };
243        api.getState = function() {
244            return $.extend(true, {}, accessibilityState);
245        };
246        api.setState = function(partialState) {
247            if (!partialState || typeof partialState !== 'object') {
248                return;
249            }
250
251            accessibilityState = normalizeAccessibilityState($.extend({}, accessibilityState, partialState));
252            saveState();
253            applyState();
254        };
255        api.getTargets = function(groupName) {
256            return targetResolver.getTargets(groupName);
257        };
258        api.debug = function() {
259            return {
260                state: $.extend(true, {}, accessibilityState),
261                targets: targetResolver.getSummary()
262            };
263        };
264
265        window.OpenAccessibility = api;
266    }
267
268    function dispatchReadyEvent() {
269        dispatchOpenAccessibilityEvent('openAccessibility:ready', window.OpenAccessibility.debug());
270    }
271
272    // Initialize
273    function initAccessibility() {
274        // Load saved state
275        loadState();
276
277        // Detect if widget is embedded via shortcode (inline positioning)
278        isShortcodeEmbed = $('.open-accessibility-widget-wrapper').closest('.open-accessibility-shortcode').length > 0;
279
280        targetResolver = createTargetResolver(targetConfig);
281        exposeOpenAccessibilityApi();
282
283        // Create reading guide element
284        if ($('.open-accessibility-reading-guide').length === 0) {
285            $('body').append('<div class="open-accessibility-reading-guide"></div>');
286        }
287
288        // Apply saved state
289        applyState();
290        applyLinkTargetPolicy();
291        initSkipLink();
292        dispatchReadyEvent();
293
294        // Button click handler
295        $('.open-accessibility-toggle-button').on('click', toggleAccessibilityPanel);
296
297        // Close button
298        $('.open-accessibility-close').on('click', closeAccessibilityPanel);
299
300        // Hide widget button
301        $('.open-accessibility-hide-widget').on('click', hideAccessibilityWidget);
302
303        // Action buttons
304        $('.open-accessibility-action-button').on('click', function(e) {
305            handleActionButton.call(this, e);
306        });
307
308        // Reading guide mouse movement
309        $(document).on('mousemove', handleReadingGuide);
310
311        // Close panel when clicking outside
312        $(document).on('click', function(e) {
313            const $panel = $('.open-accessibility-widget-panel');
314            const $button = $('.open-accessibility-toggle-button');
315            if (
316                $panel.hasClass('oa-panel-is-active') &&
317                !$(e.target).closest($panel).length &&
318                !$(e.target).closest($button).length
319            ) {
320                closeAccessibilityPanel();
321            }
322        });
323
324        // Keyboard shortcuts
325        $(document).on('keydown', function(e) {
326            // ESC key closes the panel
327            if (e.key === 'Escape' && $('.open-accessibility-widget-panel').hasClass('oa-panel-is-active')) {
328                closeAccessibilityPanel();
329                $('.open-accessibility-toggle-button').trigger('focus');
330            }
331        });
332        
333        // Ensure widget wrapper stays in viewport on scroll
334        $(window).on('scroll', function() {
335            if (isShortcodeEmbed) {
336                // Re-resolve placement so an open panel stays in view while
337                // the embedded toggle moves with the page
338                if ($('.open-accessibility-widget-panel').hasClass('oa-panel-is-active')) {
339                    positionShortcodePanel();
340                }
341                return;
342            }
343
344            const $widgetWrapper = $('.open-accessibility-widget-wrapper');
345            const $panel = $('.open-accessibility-widget-panel');
346            const isMobile = window.innerWidth < 768;
347
348            if (!($panel.hasClass('oa-panel-is-active') && isMobile)) {
349                if ($widgetWrapper.hasClass('position-left') || $widgetWrapper.hasClass('position-right')) {
350                    $widgetWrapper.css({
351                        'top': '50vh',
352                        'transform': 'translateY(-50%)'
353                    });
354                } else {
355                    $widgetWrapper.css({'transform': '', 'top': ''}); // Reset for corner positions
356                }
357            }
358        });
359
360        // Handle window resize
361        $(window).on('resize', function() {
362            if (isShortcodeEmbed) {
363                if ($('.open-accessibility-widget-panel').hasClass('oa-panel-is-active')) {
364                    positionShortcodePanel();
365                }
366                return;
367            }
368
369            const $panel = $('.open-accessibility-widget-panel');
370            if ($panel.hasClass('oa-panel-is-active')) {
371                const $wrapper = $('.open-accessibility-widget-wrapper');
372                const isMobile = window.innerWidth < 768;
373
374                if (isMobile) {
375                    if ($wrapper.hasClass('position-left') || $wrapper.hasClass('position-right')) {
376                        const wrapperHeight = $wrapper.outerHeight();
377                        $wrapper.css('top', `calc(50vh - ${wrapperHeight / 2}px)`);
378                    }
379                    $wrapper.css('transform', 'none');
380                    $panel.css({
381                        'position': 'fixed', 'display': 'block', 'top': '0px', 'left': '0px',
382                        'width': '100vw', 'height': '100vh', 'max-height': '100vh',
383                        'transform': 'none', 'z-index': '1000001'
384                    });
385                } else { // Became desktop
386                    $wrapper.css({'transform': '', 'top': ''});
387                    $panel.css({
388                        'position': '', 'display': '', 'top': '', 'left': '',
389                        'width': '', 'height': '', 'max-height': '', 'transform': '', 'z-index': ''
390                    });
391                }
392            }
393        });
394    }
395
396    function mergeUniqueElements() {
397        const seen = new Set();
398        const merged = [];
399
400        Array.from(arguments).forEach((elements) => {
401            elements.forEach((element) => {
402                if (!seen.has(element)) {
403                    seen.add(element);
404                    merged.push(element);
405                }
406            });
407        });
408
409        return merged;
410    }
411
412    function logDebug() {
413        if (!debugEnabled || !window.console || !window.console.log) {
414            return;
415        }
416
417        window.console.log.apply(window.console, arguments);
418    }
419
420    function dispatchOpenAccessibilityEvent(name, detail) {
421        document.dispatchEvent(new CustomEvent(name, {
422            detail: detail || {}
423        }));
424    }
425
426    function getLifecycleEventDetail() {
427        return {
428            state: $.extend(true, {}, accessibilityState),
429            targets: targetResolver ? targetResolver.getSummary() : {}
430        };
431    }
432
433    function createTargetResolver(config) {
434        const invalidSelectors = [];
435        let roots = [];
436        let targets = {};
437        let layoutContainers = [];
438        let excludedCount = 0;
439
440        function queryAll(selector, root, context) {
441            if (!selector) {
442                return [];
443            }
444
445            try {
446                return Array.from((root || document).querySelectorAll(selector));
447            } catch (error) {
448                invalidSelectors.push({ selector, context, message: error.message });
449                return [];
450            }
451        }
452
453        function queryAllSelectors(selectors, root, context) {
454            const collected = [];
455
456            selectors.forEach((selector) => {
457                collected.push.apply(collected, queryAll(selector, root, context));
458            });
459
460            return collected;
461        }
462
463        function matches(element, selector, context) {
464            if (!selector || !(element instanceof Element)) {
465                return false;
466            }
467
468            try {
469                return element.matches(selector);
470            } catch (error) {
471                invalidSelectors.push({ selector, context, message: error.message });
472                return false;
473            }
474        }
475
476        function matchesAny(element, selectors, context) {
477            return selectors.some((selector) => matches(element, selector, context));
478        }
479
480        function closest(element, selector, context) {
481            if (!selector || !(element instanceof Element)) {
482                return null;
483            }
484
485            try {
486                return element.closest(selector);
487            } catch (error) {
488                invalidSelectors.push({ selector, context, message: error.message });
489                return null;
490            }
491        }
492
493        function closestAny(element, selectors, context) {
494            let match = null;
495
496            selectors.some((selector) => {
497                match = closest(element, selector, context);
498                return Boolean(match);
499            });
500
501            return match;
502        }
503
504        function isExcluded(element, options) {
505            const settings = options || {};
506
507            if (!(element instanceof Element)) {
508                return true;
509            }
510
511            if (!document.body.contains(element)) {
512                return true;
513            }
514
515            if (closest(element, '.open-accessibility-widget-wrapper, .open-accessibility-reading-guide, .open-accessibility-skip-to-content-link, .open-accessibility-skip-to-content-backdrop, .open-accessibility-ignore, [data-oa-ignore]', 'built-in exclusions')) {
516                return true;
517            }
518
519            if (matchesAny(element, config.excluded, 'excluded') || closestAny(element, config.excluded, 'excluded')) {
520                excludedCount++;
521                return true;
522            }
523
524            if (settings.preserveLayout && closest(element, '[data-oa-preserve-layout]', 'preserve layout')) {
525                excludedCount++;
526                return true;
527            }
528
529            return false;
530        }
531
532        function uniqueElements(elements, options) {
533            const seen = new Set();
534            const unique = [];
535
536            elements.forEach((element) => {
537                if (seen.has(element) || isExcluded(element, options)) {
538                    return;
539                }
540
541                seen.add(element);
542                unique.push(element);
543            });
544
545            return unique;
546        }
547
548        function collectRoots() {
549            const configuredRoots = queryAllSelectors(config.roots, document, 'roots')
550                .concat(queryAll('[data-oa-root]', document, 'data roots'));
551
552            roots = uniqueElements(configuredRoots);
553
554            if (!roots.length && config.useBodyFallback) {
555                roots = [document.body];
556            }
557        }
558
559        function collectGroup(groupName) {
560            const selectors = config.groups[groupName] || [];
561            const dataSelector = `[data-oa-target~="${groupName}"]`;
562            const collected = [];
563
564            roots.forEach((root) => {
565                if (matchesAny(root, selectors, groupName)) {
566                    collected.push(root);
567                }
568
569                collected.push.apply(collected, queryAllSelectors(selectors, root, groupName));
570                collected.push.apply(collected, queryAll(dataSelector, root, `${groupName} data target`));
571            });
572
573            targets[groupName] = uniqueElements(collected);
574        }
575
576        function collectLayoutContainers() {
577            const selectors = config.layoutContainers.concat(['[data-oa-relax-layout]']);
578            const collected = [];
579
580            roots.forEach((root) => {
581                if (matchesAny(root, selectors, 'layout containers')) {
582                    collected.push(root);
583                }
584
585                collected.push.apply(collected, queryAllSelectors(selectors, root, 'layout containers'));
586            });
587
588            layoutContainers = uniqueElements(collected, { preserveLayout: true });
589        }
590
591        function refresh() {
592            invalidSelectors.length = 0;
593            excludedCount = 0;
594            targets = {};
595
596            collectRoots();
597            Object.keys(config.groups).forEach(collectGroup);
598            collectLayoutContainers();
599
600            dispatchOpenAccessibilityEvent('openAccessibility:targetsRefreshed', getSummary());
601            logDebug('Open Accessibility targets', getSummary());
602        }
603
604        function getTargets(groupName) {
605            if (groupName === 'layout_containers') {
606                return layoutContainers.slice();
607            }
608
609            return targets[groupName] ? targets[groupName].slice() : [];
610        }
611
612        function getSummary() {
613            const groupCounts = {};
614
615            Object.keys(targets).forEach((groupName) => {
616                groupCounts[groupName] = targets[groupName].length;
617            });
618
619            return {
620                roots: roots.length,
621                groups: groupCounts,
622                layoutContainers: layoutContainers.length,
623                invalidSelectors: invalidSelectors.slice(),
624                excludedCount
625            };
626        }
627
628        refresh();
629
630        return {
631            refresh,
632            getTargets,
633            getSummary
634        };
635    }
636
637    function getTypographyTargets() {
638        const contentTargets = targetResolver ? targetResolver.getTargets('readable_text') : [];
639        const headingTargets = targetResolver ? targetResolver.getTargets('headings') : [];
640        const readableTargets = mergeUniqueElements(contentTargets, headingTargets);
641
642        return {
643            contentTargets,
644            readableTargets,
645            allTargets: mergeUniqueElements(contentTargets, readableTargets)
646        };
647    }
648
649    function parsePixelValue(value, fallback) {
650        if (typeof value !== 'string' || value === '' || value === 'normal') {
651            return fallback;
652        }
653
654        const parsed = Number.parseFloat(value);
655        return Number.isFinite(parsed) ? parsed : fallback;
656    }
657
658    function getComputedLineHeightPx(style, fontSizePx) {
659        return parsePixelValue(style.lineHeight, fontSizePx * 1.2);
660    }
661
662    function getComputedSpacingPx(value) {
663        return parsePixelValue(value, 0);
664    }
665
666    function roundPx(value) {
667        return Math.round(value * 1000) / 1000;
668    }
669
670    function captureOriginalInlineStyles(element) {
671        if (element.dataset.oaTypographyInlineCaptured === '1') {
672            return;
673        }
674
675        element.dataset.oaTypographyInlineCaptured = '1';
676        element.dataset.oaOriginalFontSize = element.style.fontSize || '';
677        element.dataset.oaOriginalLineHeight = element.style.lineHeight || '';
678        element.dataset.oaOriginalLetterSpacing = element.style.letterSpacing || '';
679        element.dataset.oaOriginalWordSpacing = element.style.wordSpacing || '';
680        element.dataset.oaOriginalTextAlign = element.style.textAlign || '';
681    }
682
683    function refreshTypographyBaseline(element) {
684        captureOriginalInlineStyles(element);
685
686        const style = window.getComputedStyle(element);
687        const fontSizePx = parsePixelValue(style.fontSize, 16);
688
689        element.dataset.oaTypographyManaged = '1';
690        element.dataset.oaBaseFontSize = String(fontSizePx);
691        element.dataset.oaBaseLineHeight = String(getComputedLineHeightPx(style, fontSizePx));
692        element.dataset.oaBaseLetterSpacing = String(getComputedSpacingPx(style.letterSpacing));
693        element.dataset.oaBaseWordSpacing = String(getComputedSpacingPx(style.wordSpacing));
694    }
695
696    function restoreTypographyProperty(element, propertyName, originalValueKey) {
697        if (element.dataset[originalValueKey]) {
698            element.style[propertyName] = element.dataset[originalValueKey];
699        } else {
700            element.style[propertyName] = '';
701        }
702    }
703
704    function captureOriginalStyle(element, datasetKey, propertyName) {
705        if (element.dataset[`${datasetKey}Captured`] === '1') {
706            return;
707        }
708
709        element.dataset[`${datasetKey}Captured`] = '1';
710        element.dataset[datasetKey] = element.style[propertyName] || '';
711    }
712
713    function restoreOriginalStyle(element, datasetKey, propertyName) {
714        if (element.dataset[datasetKey]) {
715            element.style[propertyName] = element.dataset[datasetKey];
716        } else {
717            element.style[propertyName] = '';
718        }
719    }
720
721    function clearManagedStyles(selector, restoreCallback) {
722        document.querySelectorAll(selector).forEach((element) => {
723            restoreCallback(element);
724        });
725    }
726
727    function clearDynamicTypographyStyles() {
728        document.querySelectorAll('[data-oa-typography-managed="1"]').forEach((element) => {
729            restoreTypographyProperty(element, 'fontSize', 'oaOriginalFontSize');
730            restoreTypographyProperty(element, 'lineHeight', 'oaOriginalLineHeight');
731            restoreTypographyProperty(element, 'letterSpacing', 'oaOriginalLetterSpacing');
732            restoreTypographyProperty(element, 'wordSpacing', 'oaOriginalWordSpacing');
733            restoreTypographyProperty(element, 'textAlign', 'oaOriginalTextAlign');
734        });
735    }
736
737    function hasTextAffectingControls() {
738        const selectedFont = accessibilityState.selectedFont || 'default';
739
740        return accessibilityState.textSize > 0 ||
741            accessibilityState.lineHeightLevel > 0 ||
742            accessibilityState.letterSpacingLevel > 0 ||
743            accessibilityState.wordSpacingLevel > 0 ||
744            selectedFont !== 'default';
745    }
746
747    function captureOriginalLayoutStyles(element) {
748        if (element.dataset.oaLayoutReliefCaptured === '1') {
749            return;
750        }
751
752        element.dataset.oaLayoutReliefCaptured = '1';
753        element.dataset.oaOriginalHeight = element.style.height || '';
754        element.dataset.oaOriginalMaxHeight = element.style.maxHeight || '';
755        element.dataset.oaOriginalOverflow = element.style.overflow || '';
756        element.dataset.oaOriginalOverflowX = element.style.overflowX || '';
757        element.dataset.oaOriginalOverflowY = element.style.overflowY || '';
758        element.dataset.oaOriginalDisplay = element.style.display || '';
759        element.dataset.oaOriginalWebkitLineClamp = element.style.webkitLineClamp || '';
760        element.dataset.oaOriginalLineClamp = element.style.lineClamp || '';
761    }
762
763    function restoreLayoutProperty(element, propertyName, originalValueKey) {
764        if (element.dataset[originalValueKey]) {
765            element.style[propertyName] = element.dataset[originalValueKey];
766        } else {
767            element.style[propertyName] = '';
768        }
769    }
770
771    function restoreLayoutRelief() {
772        document.querySelectorAll('[data-oa-layout-relief-managed="1"]').forEach((element) => {
773            restoreLayoutProperty(element, 'height', 'oaOriginalHeight');
774            restoreLayoutProperty(element, 'maxHeight', 'oaOriginalMaxHeight');
775            restoreLayoutProperty(element, 'overflow', 'oaOriginalOverflow');
776            restoreLayoutProperty(element, 'overflowX', 'oaOriginalOverflowX');
777            restoreLayoutProperty(element, 'overflowY', 'oaOriginalOverflowY');
778            restoreLayoutProperty(element, 'display', 'oaOriginalDisplay');
779            restoreLayoutProperty(element, 'webkitLineClamp', 'oaOriginalWebkitLineClamp');
780            restoreLayoutProperty(element, 'lineClamp', 'oaOriginalLineClamp');
781            delete element.dataset.oaLayoutReliefManaged;
782            delete element.dataset.oaLayoutReliefCaptured;
783            delete element.dataset.oaOriginalHeight;
784            delete element.dataset.oaOriginalMaxHeight;
785            delete element.dataset.oaOriginalOverflow;
786            delete element.dataset.oaOriginalOverflowX;
787            delete element.dataset.oaOriginalOverflowY;
788            delete element.dataset.oaOriginalDisplay;
789            delete element.dataset.oaOriginalWebkitLineClamp;
790            delete element.dataset.oaOriginalLineClamp;
791        });
792    }
793
794    function applyLayoutRelief() {
795        targetResolver.getTargets('layout_containers').forEach((element) => {
796            const computedStyle = window.getComputedStyle(element);
797
798            captureOriginalLayoutStyles(element);
799            element.dataset.oaLayoutReliefManaged = '1';
800            element.style.height = 'auto';
801            element.style.maxHeight = 'none';
802            element.style.overflow = 'visible';
803            element.style.overflowX = 'visible';
804            element.style.overflowY = 'visible';
805            element.style.webkitLineClamp = 'unset';
806            element.style.lineClamp = 'unset';
807
808            if (computedStyle.display === '-webkit-box') {
809                element.style.display = 'block';
810            }
811        });
812    }
813
814    function syncLayoutRelief() {
815        restoreLayoutRelief();
816
817        if (hasTextAffectingControls()) {
818            applyLayoutRelief();
819        }
820    }
821
822    function getReadableFontTargets() {
823        if (!targetResolver) {
824            return [];
825        }
826
827        return mergeUniqueElements(
828            targetResolver.getTargets('readable_text'),
829            targetResolver.getTargets('headings')
830        );
831    }
832
833    function getReadableFontFamily(fontValue) {
834        if (fontValue === 'atkinson') {
835            return "'Atkinson Hyperlegible', sans-serif";
836        }
837
838        if (fontValue === 'opendyslexic') {
839            return "'OpenDyslexic', sans-serif";
840        }
841
842        return '';
843    }
844
845    function restoreReadableFontTarget(element) {
846        restoreOriginalStyle(element, 'oaReadableFontOriginal', 'fontFamily');
847        delete element.dataset.oaReadableFontManaged;
848        delete element.dataset.oaReadableFontOriginal;
849        delete element.dataset.oaReadableFontOriginalCaptured;
850    }
851
852    function applyReadableFontTargets(fontValue) {
853        const fontFamily = getReadableFontFamily(fontValue);
854
855        clearManagedStyles('[data-oa-readable-font-managed="1"]', restoreReadableFontTarget);
856
857        if (!fontFamily) {
858            return;
859        }
860
861        getReadableFontTargets().forEach((element) => {
862            captureOriginalStyle(element, 'oaReadableFontOriginal', 'fontFamily');
863            element.dataset.oaReadableFontManaged = '1';
864            element.style.fontFamily = fontFamily;
865        });
866    }
867
868    function restoreLinksUnderlineTarget(element) {
869        restoreOriginalStyle(element, 'oaLinksUnderlineOriginal', 'textDecoration');
870        delete element.dataset.oaLinksUnderlineManaged;
871        delete element.dataset.oaLinksUnderlineOriginal;
872        delete element.dataset.oaLinksUnderlineOriginalCaptured;
873    }
874
875    function applyLinksUnderlineTargets() {
876        clearManagedStyles('[data-oa-links-underline-managed="1"]', restoreLinksUnderlineTarget);
877
878        if (!accessibilityState.linksUnderline || !targetResolver) {
879            return;
880        }
881
882        targetResolver.getTargets('links').forEach((element) => {
883            captureOriginalStyle(element, 'oaLinksUnderlineOriginal', 'textDecoration');
884            element.dataset.oaLinksUnderlineManaged = '1';
885            element.style.textDecoration = 'underline';
886        });
887    }
888
889    function restoreHideImagesTarget(element) {
890        restoreOriginalStyle(element, 'oaHideImagesOriginal', 'visibility');
891        delete element.dataset.oaHideImagesManaged;
892        delete element.dataset.oaHideImagesOriginal;
893        delete element.dataset.oaHideImagesOriginalCaptured;
894    }
895
896    function applyHideImagesTargets() {
897        clearManagedStyles('[data-oa-hide-images-managed="1"]', restoreHideImagesTarget);
898
899        if (!accessibilityState.hideImages || !targetResolver) {
900            return;
901        }
902
903        targetResolver.getTargets('media').forEach((element) => {
904            if (!['IMG', 'PICTURE', 'SVG'].includes(element.tagName)) {
905                return;
906            }
907
908            captureOriginalStyle(element, 'oaHideImagesOriginal', 'visibility');
909            element.dataset.oaHideImagesManaged = '1';
910            element.style.visibility = 'hidden';
911        });
912    }
913
914    function restoreGrayscaleTarget(element) {
915        restoreOriginalStyle(element, 'oaGrayscaleOriginal', 'filter');
916        delete element.dataset.oaGrayscaleManaged;
917        delete element.dataset.oaGrayscaleOriginal;
918        delete element.dataset.oaGrayscaleOriginalCaptured;
919    }
920
921    function getGrayscaleTargets() {
922        if (!targetResolver) {
923            return [];
924        }
925
926        return mergeUniqueElements(
927            targetResolver.getTargets('readable_text'),
928            targetResolver.getTargets('headings'),
929            targetResolver.getTargets('links'),
930            targetResolver.getTargets('media')
931        );
932    }
933
934    function applyGrayscaleTargets() {
935        const $button = $('.open-accessibility-toggle-button');
936        const $panel = $('.open-accessibility-widget-panel');
937
938        clearManagedStyles('[data-oa-grayscale-managed="1"]', restoreGrayscaleTarget);
939        $button.toggleClass('widget-grayscale', accessibilityState.grayscale);
940        $panel.toggleClass('widget-grayscale', accessibilityState.grayscale);
941
942        if (!accessibilityState.grayscale) {
943            return;
944        }
945
946        getGrayscaleTargets().forEach((element) => {
947            captureOriginalStyle(element, 'oaGrayscaleOriginal', 'f
947ilter');
948            element.dataset.oaGrayscaleManaged = '1';
949            element.style.filter = 'grayscale(100%)';
950        });
951    }
952
953    function removeLegacyTypographyClasses() {
954        $('body').removeClass('open-accessibility-text-align-left open-accessibility-text-align-center open-accessibility-text-align-right');
955
956        for (let i = 1; i <= MAX_TEXT_SIZE; i++) {
957            $('body').removeClass(`open-accessibility-text-size-${i}`);
958        }
959
960        for (let i = 1; i <= MAX_SPACING_LEVEL; i++) {
961            $('body').removeClass(`open-accessibility-letter-spacing-${i}`);
962            $('body').removeClass(`open-accessibility-word-spacing-${i}`);
963            $('body').removeClass(`open-accessibility-line-height-${i}`);
964        }
965    }
966
967    function applyDynamicTypographyAdjustments() {
968        removeLegacyTypographyClasses();
969        clearDynamicTypographyStyles();
970
971        const typographyElements = getTypographyTargets();
972        if (!typographyElements.allTargets.length) {
973            syncLayoutRelief();
974            return;
975        }
976
977        typographyElements.allTargets.forEach((element) => {
978            refreshTypographyBaseline(element);
979        });
980
981        const textSizeMultiplier = TEXT_SIZE_MULTIPLIERS[accessibilityState.textSize] || 1;
982        const lineHeightMultiplier = LINE_HEIGHT_MULTIPLIERS[accessibilityState.lineHeightLevel] || 0;
983        const letterSpacingStep = LETTER_SPACING_STEPS[accessibilityState.letterSpacingLevel] || 0;
984        const wordSpacingStep = WORD_SPACING_STEPS[accessibilityState.wordSpacingLevel] || 0;
985        const resizedTargets = new Set(typographyElements.readableTargets);
986
987        typographyElements.readableTargets.forEach((element) => {
988            if (accessibilityState.textSize <= 0) {
989                return;
990            }
991
992            const baseFontSize = parsePixelValue(element.dataset.oaBaseFontSize, 16);
993            element.style.fontSize = `${roundPx(baseFontSize * textSizeMultiplier)}px`;
994        });
995
996        typographyElements.contentTargets.forEach((element) => {
997            const baseFontSize = parsePixelValue(element.dataset.oaBaseFontSize, 16);
998            const effectiveFontSize =
999                resizedTargets.has(element) && accessibilityState.textSize > 0
1000                    ? baseFontSize * textSizeMultiplier
1001                    : baseFontSize;
1002
1003            if (accessibilityState.lineHeightLevel > 0) {
1004                const baseLineHeight = parsePixelValue(element.dataset.oaBaseLineHeight, effectiveFontSize * 1.2);
1005                element.style.lineHeight = `${roundPx(Math.max(baseLineHeight, effectiveFontSize * lineHeightMultiplier))}px`;
1006            }
1007
1008            if (accessibilityState.letterSpacingLevel > 0) {
1009                const baseLetterSpacing = parsePixelValue(element.dataset.oaBaseLetterSpacing, 0);
1010                element.style.letterSpacing = `${roundPx(baseLetterSpacing + effectiveFontSize * letterSpacingStep)}px`;
1011            }
1012
1013            if (accessibilityState.wordSpacingLevel > 0) {
1014                const baseWordSpacing = parsePixelValue(element.dataset.oaBaseWordSpacing, 0);
1015                element.style.wordSpacing = `${roundPx(baseWordSpacing + effectiveFontSize * wordSpacingStep)}px`;
1016            }
1017        });
1018
1019        if (accessibilityState.textAlign) {
1020            typographyElements.readableTargets.forEach((element) => {
1021                element.style.textAlign = accessibilityState.textAlign;
1022            });
1023        }
1024
1025        syncLayoutRelief();
1026    }
1027
1028    // Resolve which way a shortcode-embedded panel opens so it stays inside
1029    // the viewport. Explicit direction/align shortcode attributes are
1030    // honored; 'auto' picks the side with the most available space.
1031    function positionShortcodePanel() {
1032        if (!isShortcodeEmbed) {
1033            return;
1034        }
1035
1036        const $root = $('.open-accessibility-widget-wrapper').closest('.open-accessibility-shortcode');
1037        const $panel = $root.find('.open-accessibility-widget-panel');
1038        const $button = $root.find('.open-accessibility-toggle-button');
1039
1040        if (!$root.length || !$panel.length || !$button.length) {
1041            return;
1042        }
1043
1044        // Small screens get the full-screen panel from CSS
1045        if (window.matchMedia('(max-width: 600px)').matches) {
1046            $panel.css('max-height', '');
1047            return;
1048        }
1049
1050        const preferredDirection = $root.attr('data-oa-direction') || 'auto';
1051        const preferredAlign = $root.attr('data-oa-align') || 'auto';
1052        const rect = $button[0].getBoundingClientRect();
1053        const panelGap = 15;        // matches the CSS panel offset
1054        const viewportMargin = 10;  // breathing room from the viewport edge
1055        const spaceAbove = rect.top - panelGap - viewportMargin;
1056        const spaceBelow = window.innerHeight - rect.bottom - panelGap - viewportMargin;
1057        const panelHeight = Math.min($panel.prop('scrollHeight') || 0, window.innerHeight * 0.8);
1058
1059        let direction = preferredDirection;
1060        if (direction !== 'up' && direction !== 'down') {
1061            direction = (spaceBelow >= panelHeight || spaceBelow >= spaceAbove) ? 'down' : 'up';
1062        }
1063
1064        let align = preferredAlign;
1065        if (align !== 'left' && align !== 'right') {
1066            const panelWidth = $panel.outerWidth() || 350;
1067            const spaceRight = window.innerWidth - rect.left - viewportMargin;
1068            const spaceLeft = rect.right - viewportMargin;
1069            align = (spaceRight >= panelWidth || spaceRight >= spaceLeft) ? 'left' : 'right';
1070        }
1071
1072        $root.toggleClass('oa-direction-up', direction === 'up')
1073            .toggleClass('oa-direction-down', direction === 'down')
1074            .toggleClass('oa-align-left', align === 'left')
1075            .toggleClass('oa-align-right', align === 'right');
1076
1077        // Cap the panel to the space on its open side; content scrolls
1078        const availableSpace = direction === 'down' ? spaceBelow : spaceAbove;
1079        $panel.css('max-height', Math.max(Math.round(availableSpace), 200) + 'px');
1080    }
1081
1082    // Toggle widget panel
1083    function toggleAccessibilityPanel() {
1084        const $panel = $('.open-accessibility-widget-panel');
1085        const $toggle = $('.open-accessibility-toggle-button');
1086        const $wrapper = $('.open-accessibility-widget-wrapper');
1087        const isMobile = window.innerWidth < 768;
1088
1089        $panel.toggleClass('oa-panel-is-active');
1090        const isActive = $panel.hasClass('oa-panel-is-active');
1091        $panel.attr('aria-hidden', isActive ? 'false' : 'true');
1092        $toggle.attr('aria-expanded', isActive ? 'true' : 'false');
1093
1094        // Shortcode embed: resolve placement against the viewport on open
1095        if (isShortcodeEmbed) {
1096            if (isActive) {
1097                positionShortcodePanel();
1098            }
1099            return;
1100        }
1101
1102        if (isActive) { // Panel is OPENING
1103            if (isMobile) {
1104                if ($wrapper.hasClass('position-left') || $wrapper.hasClass('position-right')) {
1105                    const wrapperHeight = $wrapper.outerHeight();
1106                    $wrapper.css('top', `calc(50vh - ${wrapperHeight / 2}px)`);
1107                }
1108                $wrapper.css('transform', 'none');
1109                $panel.css({
1110                    'position': 'fixed', 'display': 'block', 'top': '0px', 'left': '0px',
1111                    'width': '100vw', 'height': '100vh', 'max-height': '100vh',
1112                    'transform': 'none', 'z-index': '1000001'
1113                });
1114            } else { // Desktop OPENING
1115                $wrapper.css({'transform': '', 'top': ''});
1116                $panel.css({
1117                    'position': '', 'display': '', 'top': '', 'left': '',
1118                    'width': '', 'height': '', 'max-height': '', 'transform': '', 'z-index': ''
1119                });
1120            }
1121        } else { // Panel is CLOSING (via toggle button click)
1122            if (isMobile) {
1123                $panel.css({ // Re-assert full-screen styles for fade-out
1124                    'position': 'fixed', 'display': 'block', 'top': '0px', 'left': '0px',
1125                    'width': '100vw', 'height': '100vh', 'max-height': '100vh',
1126                    'transform': 'none', 'z-index': '1000001' 
1127                });
1128                setTimeout(function() {
1129                    $panel.css({ // Hide and reset panel
1130                        'display': 'none', 'position': '', 'top': '', 'left': '', 'width': '', 
1131                        'height': '', 'max-height': '', 'transform': '', 'z-index': ''
1132                    });
1133                    // Reset wrapper transform/top AFTER panel is hidden
1134                    $wrapper.css({'transform': '', 'top': ''}); 
1135                }, 350); 
1136            } else { // Desktop CLOSING
1137                $wrapper.css({'transform': '', 'top': ''}); // Reset wrapper immediately for desktop
1138                $panel.css({
1139                    'position': '', 'display': '', 'top': '','left': '',
1140                    'width': '', 'height': '', 'max-height': '', 'transform': '', 'z-index': ''
1141                });
1142            }
1143        }
1144    }
1145
1146    // Close widget panel (called by close button, ESC, click-outside)
1147    function closeAccessibilityPanel() {
1148        const $panel = $('.open-accessibility-widget-panel');
1149        const $toggle = $('.open-accessibility-toggle-button');
1150        const $wrapper = $('.open-accessibility-widget-wrapper');
1151        const isMobile = window.innerWidth < 768;
1152
1153        $panel.removeClass('oa-panel-is-active');
1154        $panel.attr('aria-hidden', 'true');
1155        $toggle.attr('aria-expanded', 'false');
1156
1157        // Shortcode embed: simple hide, no positioning logic
1158        if (isShortcodeEmbed) {
1159            return;
1160        }
1161
1162        if (isMobile) {
1163            $panel.css({ // Re-assert full-screen styles for fade-out
1164                'position': 'fixed', 'display': 'block', 'top': '0px', 'left': '0px',
1165                'width': '100vw', 'height': '100vh', 'max-height': '100vh',
1166                'transform': 'none', 'z-index': '1000001' 
1167            });
1168            setTimeout(function() {
1169                $panel.css({ // Hide and reset panel
1170                    'display': 'none', 'position': '', 'top': '', 'left': '', 'width': '', 
1171                    'height': '', 'max-height': '', 'transform': '', 'z-index': ''   
1172                });
1173                // Reset wrapper transform/top AFTER panel is hidden
1174                $wrapper.css({'transform': '', 'top': ''}); 
1175            }, 350); 
1176        } else { // Desktop
1177            $wrapper.css({'transform': '', 'top': ''}); // Reset wrapper immediately for desktop
1178            $panel.css({
1179                'position': '', 'display': '', 'top': '','left': '',
1180                'width': '', 'height': '', 'max-height': '', 'transform': '', 'z-index': ''
1181            });
1182        }
1183    }
1184
1185    // Hide widget (user choice)
1186    function hideAccessibilityWidget() {
1187        $('.open-accessibility-toggle-button, .open-accessibility-widget-panel').css('display', 'none');
1188
1189        // Set a cookie to remember this choice temporarily
1190        setCookie('open_accessibility_hidden', '1', 1); // Hide for 1 day
1191
1192        closeAccessibilityPanel();
1193    }
1194
1195    // Handle action button clicks
1196    function handleActionButton(e) {
1197        e.stopPropagation();
1198
1199        const $btn = $(this);
1200        const action = $btn.data('action');
1201        const value = $btn.data('value');
1202
1203        switch (action) {
1204            case 'contrast':
1205                handleContrast(value);
1206                break;
1207
1208            case 'grayscale':
1209                toggleGrayscale();
1210                break;
1211
1212            case 'text-size':
1213                adjustTextSize(value);
1214                break;
1215
1216            case 'set-font':
1217                setFont(value);
1218                break;
1219
1220            case 'links-underline':
1221                toggleLinksUnderline();
1222                break;
1223
1224            case 'hide-images':
1225                toggleHideImages();
1226                break;
1227
1228            case 'reading-guide':
1229                toggleReadingGuide();
1230                break;
1231
1232            case 'focus-outline':
1233                toggleFocusOutline();
1234                break;
1235
1236            case 'line-height':
1237                adjustLineHeight(value);
1238                break;
1239
1240            case 'text-align':
1241                setTextAlign(value);
1242                break;
1243
1244            case 'pause-animations':
1245                togglePauseAnimations();
1246                break;
1247
1248            case 'letter-spacing':
1249                adjustLetterSpacing(value);
1250                break;
1251
1252            case 'word-spacing':
1253                adjustWordSpacing(value);
1254                break;
1255        }
1256
1257        // Update state
1258        syncActionButtonStates();
1259        saveState();
1260
1261        // Logged after the sync so toggles report the state they landed in.
1262        logFeatureUsage($btn, action, value);
1263    }
1264
1265    function setButtonPressed(action, value, pressed) {
1266        const $button = $('.open-accessibility-action-button[aria-pressed]').filter(function() {
1267            const $actionButton = $(this);
1268
1269            return $actionButton.attr('data-action') === action &&
1270                $actionButton.attr('data-value') === value;
1271        });
1272        $button.toggleClass('active', pressed);
1273        $button.attr('aria-pressed', pressed ? 'true' : 'false');
1274    }
1275
1276    function syncActionButtonStates() {
1277        $('.open-accessibility-action-button[aria-pressed]')
1278            .removeClass('active')
1279            .attr('aria-pressed', 'false');
1280
1281        if (accessibilityState.contrast) {
1282            setButtonPressed('contrast', accessibilityState.contrast, true);
1283        }
1284
1285        setButtonPressed('grayscale', 'toggle', accessibilityState.grayscale);
1286        setButtonPressed('set-font', accessibilityState.selectedFont || 'default', true);
1287        setButtonPressed('links-underline', 'toggle', accessibilityState.linksUnderline);
1288        setButtonPressed('hide-images', 'toggle', accessibilityState.hideImages);
1289        setButtonPressed('reading-guide', 'toggle', accessibilityState.readingGuide);
1290        setButtonPressed('focus-outline', 'toggle', accessibilityState.focusOutline);
1291
1292        if (accessibilityState.textAlign) {
1293            setButtonPressed('text-align', accessibilityState.textAlign, true);
1294        }
1295
1296        setButtonPressed('pause-animations', 'toggle', accessibilityState.pauseAnimations);
1297    }
1298
1299    // Handle contrast modes
1300    function handleContrast(mode) {
1301        mode = normalizeChoice(mode, VALID_CONTRAST_MODES, '');
1302
1303        // Remove existing contrast classes
1304        $('body').removeClass('open-accessibility-high-contrast open-accessibility-negative-contrast open-accessibility-light-background open-accessibility-dark-background');
1305        
1306        // Clear active state on all contrast buttons
1307        $('.open-accessibility-action-button[data-action="contrast"]').removeClass('active');
1308
1309        // If the same mode is clicked again, deactivate it
1310        if (accessibilityState.contrast === mode) {
1311            accessibilityState.contrast = '';
1312            return;
1313        }
1314
1315        // Apply new contrast mode
1316        switch (mode) {
1317            case 'high':
1318                $('body').addClass('open-accessibility-high-contrast');
1319                $('.open-accessibility-action-button[data-action="contrast"][data-value="high"]').addClass('active');
1320                break;
1321
1322            case 'negative':
1323                $('body').addClass('open-accessibility-negative-contrast');
1324                $('.open-accessibility-action-button[data-action="contrast"][data-value="negative"]').addClass('active');
1325                break;
1326
1327            case 'light':
1328                $('body').addClass('open-accessibility-light-background');
1329                $('.open-accessibility-action-button[data-action="contrast"][data-value="light"]').addClass('active');
1330                break;
1331
1332            case 'dark':
1333                $('body').addClass('open-accessibility-dark-background');
1334                $('.open-accessibility-action-button[data-action="contrast"][data-value="dark"]').addClass('active');
1335                break;
1336        }
1337
1338        accessibilityState.contrast = mode;
1339    }
1340
1341    // Apply contrast from saved state (without toggle logic)
1342    function applyContrast(mode) {
1343        // Remove existing contrast classes
1344        $('body').removeClass('open-accessibility-high-contrast open-accessibility-negative-contrast open-accessibility-light-background open-accessibility-dark-background');
1345        
1346        // Clear active state on all contrast buttons
1347        $('.open-accessibility-action-button[data-action="contrast"]').removeClass('active');
1348
1349        // Apply the contrast mode and activate the correct button
1350        switch (mode) {
1351            case 'high':
1352                $('body').addClass('open-accessibility-high-contrast');
1353                $('.open-accessibility-action-button[data-action="contrast"][data-value="high"]').addClass('active');
1354                break;
1355
1356            case 'negative':
1357                $('body').addClass('open-accessibility-negative-contrast');
1358                $('.open-accessibility-action-button[data-action="contrast"][data-value="negative"]').addClass('active');
1359                break;
1360
1361            case 'light':
1362                $('body').addClass('open-accessibility-light-background');
1363                $('.open-accessibility-action-button[data-action="contrast"][data-value="light"]').addClass('active');
1364                break;
1365
1366            case 'dark':
1367                $('body').addClass('open-accessibility-dark-background');
1368                $('.open-accessibility-action-button[data-action="contrast"][data-value="dark"]').addClass('active');
1369                break;
1370        }
1371    }
1372
1373    // Toggle grayscale
1374    function toggleGrayscale() {
1375        accessibilityState.grayscale = !accessibilityState.grayscale;
1376        applyGrayscaleTargets();
1377        syncActionButtonStates();
1378    }
1379
1380    // Adjust text size
1381    function adjustTextSize(direction) {
1382        if (direction === 'increase') {
1383            accessibilityState.textSize = Math.min(accessibilityState.textSize + 1, MAX_TEXT_SIZE);
1384        } else if (direction === 'decrease') {
1385            accessibilityState.textSize = Math.max(accessibilityState.textSize - 1, 0);
1386        }
1387
1388        applyDynamicTypographyAdjustments();
1389
1390        // Update button states and indicator
1391        updateSpacingButtonStates('text-size', accessibilityState.textSize);
1392        updateIndicator('text-size', accessibilityState.textSize);
1393    }
1394
1395    // Set selected font
1396    function setFont(fontValue) {
1397        fontValue = normalizeChoice(fontValue, VALID_FONT_VALUES, 'default');
1398
1399        // Remove previous legacy font classes
1400        $('body').removeClass('open-accessibility-font-atkinson open-accessibility-font-opendyslexic');
1401        // Remove active class from all font buttons
1402        $('.open-accessibility-action-button[data-action="set-font"]').removeClass('active');
1403
1404        // If the same font is clicked again, revert to default
1405        if (accessibilityState.selectedFont === fontValue) {
1406            accessibilityState.selectedFont = 'default';
1407            $('.open-accessibility-action-button[data-action="set-font"][data-value="default"]').addClass('active'); // Activate default button
1408        } else {
1409            accessibilityState.selectedFont = fontValue;
1410             // Add active class to the clicked button
1411            setButtonPressed('set-font', fontValue, true);
1412        }
1413
1414        // Ensure default button is active if state is default
1415        if (accessibilityState.selectedFont === 'default') {
1416             $('.open-accessibility-action-button[data-action="set-font"][data-value="default"]').addClass('active');
1417        }
1418
1419        applyDynamicTypographyAdjustments();
1420        applyReadableFontTargets(accessibilityState.selectedFont || 'default');
1421    }
1422
1423    // Apply font from saved state (without toggle logic)
1424    function applyFont(fontValue) {
1425        // Remove previous legacy font classes
1426        $('body').removeClass('open-accessibility-font-atkinson open-accessibility-font-opendyslexic');
1427        // Remove active class from all font buttons
1428        $('.open-accessibility-action-button[data-action="set-font"]').removeClass('active');
1429
1430        // Activate the correct button; targeted inline styles apply the font.
1431        if (fontValue === 'atkinson') {
1432            $('.open-accessibility-action-button[data-action="set-font"][data-value="atkinson"]').addClass('active');
1433        } else if (fontValue === 'opendyslexic') {
1434            $('.open-accessibility-action-button[data-action="set-font"][data-value="opendyslexic"]').addClass('active');
1435        } else {
1436            // Default font - just activate the default button
1437            $('.open-accessibility-action-button[data-action="set-font"][data-value="default"]').addClass('active');
1438        }
1439
1440        applyReadableFontTargets(accessibilityState.selectedFont || 'default');
1441    }
1442
1443    // Toggle links underline
1444    function toggleLinksUnderline() {
1445        accessibilityState.linksUnderline = !accessibilityState.linksUnderline;
1446        $('body').removeClass('open-accessibility-links-underline');
1447        applyLinksUnderlineTargets();
1448        syncActionButtonStates();
1449    }
1450
1451    // Toggle hide images
1452    function toggleHideImages() {
1453        accessibilityState.hideImages = !accessibilityState.hideImages;
1454        $('body').removeClass('open-accessibility-hide-images');
1455        applyHideImagesTargets();
1456        syncActionButtonStates();
1457    }
1458
1459    // Toggle reading guide
1460    function toggleReadingGuide() {
1461        accessibilityState.readingGuide = !accessibilityState.readingGuide;
1462        $('.open-accessibility-action-button[data-action="reading-guide"]').toggleClass('active', accessibilityState.readingGuide);
1463        if (accessibilityState.readingGuide) {
1464            $('.open-accessibility-reading-guide').show();
1465        } else {
1466            $('.open-accessibility-reading-guide').hide();
1467        }
1468        saveState();
1469    }
1470
1471    // Handle reading guide mouse movement
1472    function handleReadingGuide(e) {
1473        if (accessibilityState.readingGuide) {
1474            $('.open-accessibility-reading-guide').css('top', e.clientY - 15);
1475        }
1476    }
1477
1478    // Toggle focus outline
1479    function toggleFocusOutline() {
1480        accessibilityState.focusOutline = !accessibilityState.focusOutline;
1481        $('body').toggleClass('open-accessibility-focus-outline', accessibilityState.focusOutline);
1482        $('.open-accessibility-action-button[data-action="focus-outline"]').toggleClass('active', accessibilityState.focusOutline);
1483    }
1484
1485
1486    // Set text align
1487    function setTextAlign(align) {
1488        align = normalizeChoice(align, VALID_TEXT_ALIGN_VALUES, '');
1489
1490        if (!align) {
1491            return;
1492        }
1493
1494        // Clear active state on all text align buttons
1495        $('.open-accessibility-action-button[data-action="text-align"]').removeClass('active');
1496
1497        // If the same alignment is clicked again, deactivate it
1498        if (accessibilityState.textAlign === align) {
1499            accessibilityState.textAlign = '';
1500            applyDynamicTypographyAdjustments();
1501            return;
1502        }
1503
1504        accessibilityState.textAlign = align;
1505        setButtonPressed('text-align', align, true);
1506        applyDynamicTypographyAdjustments();
1507    }
1508
1509    // Toggle pause animations
1510    function togglePauseAnimations() {
1511        accessibilityState.pauseAnimations = !accessibilityState.pauseAnimations;
1512        $('body').toggleClass('open-accessibility-pause-animations', accessibilityState.pauseAnimations);
1513        $('.open-accessibility-action-button[data-action="pause-animations"]').toggleClass('active', accessibilityState.pauseAnimations);
1514    }
1515
1516    // Adjust Letter Spacing Level
1517    function adjustLetterSpacing(direction) {
1518        const currentLevel = accessibilityState.letterSpacingLevel;
1519        let newLevel = currentLevel;
1520
1521        if (direction === 'increase') {
1522            newLevel = Math.min(currentLevel + 1, MAX_SPACING_LEVEL);
1523        } else if (direction === 'decrease') {
1524            newLevel = Math.max(currentLevel - 1, 0);
1525        }
1526
1527        if (newLevel !== currentLevel) {
1528            accessibilityState.letterSpacingLevel = newLevel;
1529            applyDynamicTypographyAdjustments();
1530            updateSpacingButtonStates('letter-spacing', newLevel);
1531            updateIndicator('letter-spacing', newLevel);
1532        }
1533    }
1534
1535    // Adjust Word Spacing Level
1536    function adjustWordSpacing(direction) {
1537        const currentLevel = accessibilityState.wordSpacingLevel;
1538        let newLevel = currentLevel;
1539
1540        if (direction === 'increase') {
1541            newLevel = Math.min(currentLevel + 1, MAX_SPACING_LEVEL);
1542        } else if (direction === 'decrease') {
1543            newLevel = Math.max(currentLevel - 1, 0);
1544        }
1545
1546        if (newLevel !== currentLevel) {
1547            accessibilityState.wordSpacingLevel = newLevel;
1548            applyDynamicTypographyAdjustments();
1549            updateSpacingButtonStates('word-spacing', newLevel);
1550            updateIndicator('word-spacing', newLevel);
1551        }
1552    }
1553
1554    // Adjust Line Height Level
1555    function adjustLineHeight(direction) {
1556        const currentLevel = accessibilityState.lineHeightLevel;
1557        let newLevel = currentLevel;
1558
1559        if (direction === 'increase') {
1560            newLevel = Math.min(currentLevel + 1, MAX_SPACING_LEVEL);
1561        } else if (direction === 'decrease') {
1562            newLevel = Math.max(currentLevel - 1, 0);
1563        }
1564
1565        if (newLevel !== currentLevel) {
1566            accessibilityState.lineHeightLevel = newLevel;
1567            applyDynamicTypographyAdjustments();
1568            updateSpacingButtonStates('line-height', newLevel);
1569            updateIndicator('line-height', newLevel);
1570        }
1571    }
1572
1573    // Helper to update Increase/Decrease button states
1574    function updateSpacingButtonStates(action, level) {
1575        const maxLevel = action === 'text-size' ? MAX_TEXT_SIZE : MAX_SPACING_LEVEL;
1576        $(`.open-accessibility-action-button[data-action="${action}"][data-value="decrease"]`).prop('disabled', level <= 0);
1577        $(`.open-accessibility-action-button[data-action="${action}"][data-value="increase"]`).prop('disabled', level >= maxLevel);
1578    }
1579
1580    // Update visual indicator (dots/dashes) for incremental controls
1581    function updateIndicator(action, currentLevel) {
1582        const $indicator = $(`.open-accessibility-indicator[data-action="${action}"]`);
1583        if ($indicator.length === 0) {
1584            return;
1585        }
1586
1587        const maxLevel = parseInt($indicator.data('max'), 10);
1588        const totalDots = maxLevel + 1; // 0 to maxLevel inclusive
1589        
1590        // Clear existing dots
1591        $indicator.empty();
1592        
1593        // Create dots for each level
1594        for (let i = 0; i <= maxLevel; i++) {
1595            const $dot = $('<span></span>')
1596                .addClass('open-accessibility-indicator-dot')
1597                .addClass(i <= currentLevel ? 'active' : '');
1598            $indicator.append($dot);
1599        }
1600        
1601        // Update aria-label
1602        $indicator.attr('aria-label', `Level ${currentLevel} of ${maxLevel}`);
1603        $indicator.append($('<span></span>')
1604            .addClass('screen-reader-text')
1605            .text(`Level ${currentLevel} of ${maxLevel}`));
1606    }
1607
1608    // Reset all settings to default
1609    $('.open-accessibility-reset-button').on('click', function() {
1610        // Remove all accessibility classes
1611        $('body').removeClass('open-accessibility-high-contrast open-accessibility-negative-contrast open-accessibility-light-background open-accessibility-dark-background');
1612        $('body').removeClass('open-accessibility-grayscale open-accessibility-links-underline');
1613        $('body').removeClass('open-accessibility-hide-images open-accessibility-reading-guide-active open-accessibility-focus-outline');
1614        $('body').removeClass('open-accessibility-pause-animations');
1615        $('body').removeClass('open-accessibility-font-atkinson open-accessibility-font-opendyslexic');
1616        removeLegacyTypographyClasses();
1617        clearDynamicTypographyStyles();
1618        restoreLayoutRelief();
1619
1620        // Reset all buttons active/disabled state
1621        $('.open-accessibility-action-button').removeClass('active').prop('disabled', false);
1622
1623        // Reset state
1624        accessibilityState = normalizeAccessibilityState($.extend({}, DEFAULT_ACCESSIBILITY_STATE, { active: true }));
1625
1626        // Update all indicators
1627        updateIndicator('text-size', 0);
1628        updateIndicator('letter-spacing', 0);
1629        updateIndicator('word-spacing', 0);
1630        updateIndicator('line-height', 0);
1631
1632        // Explicitly hide the reading guide on reset
1633        $('.open-accessibility-reading-guide').hide(); 
1634
1635        applyDynamicTypographyAdjustments();
1636        applyReadableFontTargets('default');
1637        applyGrayscaleTargets();
1638        applyLinksUnderlineTargets();
1639        applyHideImagesTargets();
1640        syncActionButtonStates();
1641
1642        // Save reset state
1643        saveState();
1644
1645        dispatchOpenAccessibilityEvent('openAccessibility:reset', getLifecycleEventDetail());
1646    });
1647
1648    // Save state to local storage
1649    function saveState() {
1650        accessibilityState = normalizeAccessibilityState(accessibilityState);
1651
1652        if (typeof localStorage !== 'undefined') {
1653            localStorage.setItem(storageKey, JSON.stringify(accessibilityState));
1654        }
1655    }
1656
1657    // Load state from local storage
1658    function loadState() {
1659        if (typeof localStorage !== 'undefined') {
1660            let savedState = localStorage.getItem(storageKey);
1661            
1662            // Migration: Look for old page-specific keys and consolidate them
1663            if (!savedState) {
1664                // Check for any page-specific keys that might exist
1665                for (let i = 0; i < localStorage.length; i++) {
1666                    const key = localStorage.key(i);
1667                    if (key && key.endsWith('_open_accessibility_settings')) {
1668                        const pageSpecificState = localStorage.getItem(key);
1669                        if (pageSpecificState) {
1670                            try {
1671                                const parsedState = JSON.parse(pageSpecificState);
1672                                // If this state shows accessibility was active, use it as our base
1673                                if (parsedState.active) {
1674                                    savedState = pageSpecificState;
1675                                    // Save it to the new unified key
1676                                    localStorage.setItem(storageKey, savedState);
1677                                    console.log('Open Accessibility: Migrated settings from page-specific storage');
1678                                }
1679                                // Clean up the old page-specific key
1680                                localStorage.removeItem(key);
1681                            } catch (e) {
1682                                console.error('Error parsing page-specific accessibility state', e);
1683                                localStorage.removeItem(key); // Clean up corrupted data
1684                            }
1685                        }
1686                    }
1687                }
1688            }
1689            
1690            if (savedState) {
1691                try {
1692                    const parsedState = JSON.parse(savedState);
1693                    // Migration: Convert old lineHeight boolean to lineHeightLevel
1694                    if (parsedState.hasOwnProperty('lineHeight') && typeof parsedState.lineHeight === 'boolean') {
1695                        parsedState.lineHeightLevel = parsedState.lineHeight ? 2 : 0; // Old toggle was equivalent to level 2
1696                        delete parsedState.lineHeight;
1697                    }
1698                    // Ensure lineHeightLevel exists
1699                    if (!parsedState.hasOwnProperty('lineHeightLevel')) {
1700                        parsedState.lineHeightLevel = 0;
1701                    }
1702                    accessibilityState = normalizeAccessibilityState(parsedState);
1703                } catch (e) {
1704                    console.error('Error parsing saved accessibility state', e);
1705                }
1706            }
1707        }
1708    }
1709
1710    // Apply current state to the UI
1711    function applyState() {
1712        accessibilityState = normalizeAccessibilityState(accessibilityState);
1713
1714        dispatchOpenAccessibilityEvent('openAccessibility:beforeApply', getLifecycleEventDetail());
1715
1716        // Apply contrast (directly without toggle logic)
1717        applyContrast(accessibilityState.contrast || '');
1718
1719        // Apply grayscale
1720        applyGrayscaleTargets();
1721
1722        // Apply selected font (directly without toggle logic)
1723        applyFont(accessibilityState.selectedFont || 'default');
1724
1725        // Apply links underline
1726        $('body').removeClass('open-accessibility-links-underline');
1727        applyLinksUnderlineTargets();
1728
1729        // Apply hide images
1730        $('body').removeClass('open-accessibility-hide-images');
1731        applyHideImagesTargets();
1732
1733        // Apply reading guide
1734        $('.open-accessibility-reading-guide').toggle(accessibilityState.readingGuide);
1735        $('.open-accessibility-action-button[data-action="reading-guide"]').toggleClass('active', accessibilityState.readingGuide);
1736
1737        // Apply focus outline
1738        $('body').toggleClass('open-accessibility-focus-outline', accessibilityState.focusOutline);
1739        $('.open-accessibility-action-button[data-action="focus-outline"]').toggleClass('active', accessibilityState.focusOutline);
1740
1741        // Apply pause animations
1742        $('body').toggleClass('open-accessibility-pause-animations', accessibilityState.pauseAnimations);
1743        $('.open-accessibility-action-button[data-action="pause-animations"]').toggleClass('active', accessibilityState.pauseAnimations);
1744
1745        applyDynamicTypographyAdjustments();
1746
1747        // Update button states and indicators for adaptive typography controls
1748        updateSpacingButtonStates('text-size', accessibilityState.textSize);
1749        updateIndicator('text-size', accessibilityState.textSize);
1750        updateSpacingButtonStates('line-height', accessibilityState.lineHeightLevel);
1751        updateIndicator('line-height', accessibilityState.lineHeightLevel);
1752        updateSpacingButtonStates('letter-spacing', accessibilityState.letterSpacingLevel);
1753        updateIndicator('letter-spacing', accessibilityState.letterSpacingLevel);
1754        updateSpacingButtonStates('word-spacing', accessibilityState.wordSpacingLevel);
1755        updateIndicator('word-spacing', accessibilityState.wordSpacingLevel);
1756
1757        $('.open-accessibility-action-button[data-action="text-align"]').removeClass('active');
1758        if (accessibilityState.textAlign) {
1759            setButtonPressed('text-align', accessibilityState.textAlign, true);
1760        }
1761
1762        syncActionButtonStates();
1763
1764        dispatchOpenAccessibilityEvent('openAccessibility:afterApply', getLifecycleEventDetail());
1765    }
1766
1767    // Helper function to set cookies
1768    // Ordered list of places "skip to content" should land when the configured
1769    // element ID is not on the page.
1770    //
1771    // Order matters: the first match wins, so the most reliably "start of the
1772    // main content" candidate belongs first. Block themes get
1773    // #wp--skip-link--target injected by WordPress core, which is why it leads.
1774    //
1775    // The list is supplied by PHP and can be reordered or extended with the
1776    // open_accessibility_skip_target_candidates filter. The literals here are
1777    // only a fallback for when the option fails to arrive.
1778    function getSkipTargetCandidates() {
1779        const configured = getFrontendOption('skip_target_candidates', null);
1780
1781        if (Array.isArray(configured) && configured.length) {
1782            return configured.filter((s) => typeof s === 'string' && s.trim().length);
1783        }
1784
1785        return [
1786            '#wp--skip-link--target',
1787            '#content',
1788            '#main',
1789            '#primary',
1790            'main',
1791            '[role="main"]',
1792            '.site-main',
1793            '.entry-content'
1794        ];
1795    }
1796
1797    // Resolve the element the skip link should actually move focus to.
1798    function resolveSkipTarget() {
1799        const configured = getFrontendOption('skip_to_element_id', 'content');
1800
1801        // An explicitly configured ID always wins when it is really on the page.
1802        if (configured) {
1803            const byId = document.getElementById(configured);
1804            if (byId) {
1805                return byId;
1806            }
1807        }
1808
1809        const candidates = getSkipTargetCandidates();
1810
1811        for (let i = 0; i < candidates.length; i++) {
1812            try {
1813                const el = document.querySelector(candidates[i]);
1814                if (el) {
1815                    return el;
1816                }
1817            } catch (e) {
1818                // Ignore an invalid selector and keep looking.
1819            }
1820        }
1821
1822        return null;
1823    }
1824
1825    // Point the skip link at something real, and actually move focus.
1826    //
1827    // Two separate problems are handled here. Block themes have no #content,
1828    // so the default href resolves to nothing. And even when the target does
1829    // exist, following an in-page anchor scrolls the viewport without moving
1830    // keyboard focus unless the target can hold it - the classic reason a skip
1831    // link appears to do nothing for screen reader users (WCAG 2.1 SC 2.4.1).
1832    function initSkipLink() {
1833        const $link = $('.open-accessibility-skip-to-content-link');
1834
1835        if (!$link.length) {
1836            return;
1837        }
1838
1839        const target = resolveSkipTarget();
1840
1841        if (!target) {
1842            logDebug('Open Accessibility: no skip-to-content target found on this page');
1843            return;
1844        }
1845
1846        // Give the target an ID if it has none, so the href stays meaningful.
1847        if (!target.id) {
1848            target.id = 'open-accessibility-content';
1849        }
1850
1851        $link.attr('href', '#' + target.id);
1852
1853        $link.on('click', function(e) {
1854            e.preventDefault();
1855
1856            // Headings, divs and <main> are not focusable by default. -1 keeps
1857            // them out of the tab order while still allowing programmatic focus.
1858            if (!target.hasAttribute('tabindex')) {
1859                target.setAttribute('tabindex', '-1');
1860            }
1861
1862            target.focus({ preventScroll: true });
1863            target.scrollIntoView();
1864
1865            $link.trigger('blur');
1866        });
1867    }
1868
1869    // Read a frontend option pushed through wp_localize_script.
1870    function getFrontendOption(key, fallback) {
1871        if (typeof open_accessibility_data === 'undefined' ||
1872            !open_accessibility_data ||
1873            !open_accessibility_data.options ||
1874            typeof open_accessibility_data.options[key] === 'undefined') {
1875            return fallback;
1876        }
1877
1878        return open_accessibility_data.options[key];
1879    }
1880
1881    // Open links in the same tab.
1882    //
1883    // An unannounced new tab breaks the back button and disorients screen
1884    // reader and magnifier users (WCAG 2.1 SC 3.2.5). Opt-in, because it
1885    // changes the behaviour of every link on the site. The widget's own links
1886    // and anything explicitly excluded are left alone.
1887    function applyLinkTargetPolicy() {
1888        if (!getFrontendOption('strip_link_targets', false)) {
1889            return;
1890        }
1891
1892        $('a[target="_blank"]').each(function() {
1893            const $link = $(this);
1894
1895            if ($link.closest('.open-accessibility-widget-wrapper, .open-accessibility-ignore, [data-oa-ignore]').length) {
1896                return;
1897            }
1898
1899            $link.removeAttr('target');
1900        });
1901    }
1902
1903    // Per-tab identifier so repeated toggles in one visit group together.
1904    // Deliberately sessionStorage, not localStorage: it dies with the tab and
1905    // never becomes a cross-visit identifier.
1906    function getSessionId() {
1907        const key = 'open-accessibility-session';
1908
1909        try {
1910            let id = sessionStorage.getItem(key);
1911
1912            if (!id) {
1913                id = 'oa-' + Math.random().toString(36).slice(2) + Date.now().toString(36);
1914                sessionStorage.setItem(key, id);
1915            }
1916
1917            return id;
1918        } catch (e) {
1919            // Private browsing or storage disabled: skip logging rather than throw.
1920            return '';
1921        }
1922    }
1923
1924    // Report a control interaction, when the site owner has opted in.
1925    //
1926    // Sends `feature_action` rather than `action`: admin-ajax reserves `action`
1927    // to route the request, so a field of that name never reaches the handler.
1928    function logFeatureUsage($button, feature, value) {
1929        if (!getFrontendOption('enable_analytics', false)) {
1930            return;
1931        }
1932
1933        if (typeof open_accessibility_data === 'undefined' ||
1934            !open_accessibility_data ||
1935            !open_accessibility_data.ajaxurl) {
1936            return;
1937        }
1938
1939        const sessionId = getSessionId();
1940
1941        if (!sessionId || !feature) {
1942            return;
1943        }
1944
1945        // Toggles report the state they landed in; value controls report the
1946        // value that was chosen.
1947        const isToggle = value === 'toggle';
1948        const pressed = $button && $button.attr('aria-pressed') === 'true';
1949
1950        $.post(open_accessibility_data.ajaxurl, {
1951            action: 'open_accessibility_log_usage',
1952            nonce: open_accessibility_data.nonce,
1953            session_id: sessionId,
1954            feature: feature,
1955            feature_action: isToggle ? 'toggle' : 'set',
1956            value: isToggle ? (pressed ? 'on' : 'off') : String(value == null ? '' : value)
1957        }).fail(function() {
1958            logDebug('Open Accessibility: usage logging request failed');
1959        });
1960    }
1961
1962    function setCookie(name, value, days) {
1963        let expires = '';
1964        if (days) {
1965            const date = new Date();
1966            date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
1967            expires = '; expires=' + date.toUTCString();
1968        }
1969        document.cookie = name + '=' + (value || '') + expires + '; path=/; SameSite=Lax';
1970    }
1971
1972    // Helper function to get cookies
1973    function getCookie(name) {
1974        const nameEQ = name + '=';
1975        const ca = document.cookie.split(';');
1976        for (let i = 0; i < ca.length; i++) {
1977            let c = ca[i];
1978            while (c.charAt(0) === ' ') c = c.substring(1, c.length);
1979            if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
1980        }
1981        return null;
1982    }
1983
1984    // Check if widget should be hidden based on user preference
1985    function checkWidgetVisibility() {
1986        const hidden = getCookie('open_accessibility_hidden');
1987        if (hidden === '1') {
1988            $('.open-accessibility-toggle-button, .open-accessibility-widget-panel').css('display', 'none');
1989        } else {
1990            $('.open-accessibility-toggle-button').css('display', '');
1991        }
1992    }
1993
1994    // Initialize on document ready
1995    $(document).ready(function() {
1996        initAccessibility();
1997        checkWidgetVisibility();
1998    });
1999
2000})(jQuery);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.