PageSourceSearch

https://hops.hr/lib/accessibility/a11y.js

js hops.hr collected 2026-10-03 19:13:41 UTC 58,837 bytes, 1,625 lines download raw bytes

1(function () {
2  "use strict";
3
4  // Configuration for the accessibility widget
5  const WIDGET_CONFIG = {
6    storageKey: "a11y-widget-settings",
7    defaultLang: "hr",
8    translations: {
9      de: {
10        title: "Barrierefreiheit-Einstellungen",
11        readingMask: "Lesemaske",
12        highContrast: "Hoher Kontrast",
13        fontSize: "Schriftgröße",
14        lineHeight: "Zeilenhöhe",
15        letterSpacing: "Buchstabenabstand",
16        contentScale: "Inhaltsskalierung",
17        dyslexiaFont: "Lesbare Schriftart",
18        textAlign: "Rechts Ausrichten",
19        textAlignCenter: "Im Zentrum",
20        textAlignLeft: "Linksbündig",
21        highlightLinks: "Links unterstreichen",
22        hideImages: "Bilder ausblenden",
23        highlightHeadings: "Titel hervorheben",
24        textMagnifier: "Cursor Lesehilfe",
25        reset: "Zurücksetzen",
26        poweredBy: "Powered by",
27        darkMode: "Dunkler Modus",
28        focusOutline: "Fokus-Rahmen anzeigen",
29        reduceMotion: "Animationen reduzieren",
30        cursorSize: "Cursor vergrößern",
31        stopAutoplay: "Autoplay stoppen",
32        invertColors: "Farben invertieren",
33        columnWidth: "Spaltenbreite",
34      },
35      en: {
36        title: "Accessibility Settings",
37        readingMask: "Reading Mask",
38        highContrast: "High Contrast",
39        fontSize: "Font Size",
40        lineHeight: "Line Height",
41        letterSpacing: "Letter Spacing",
42        contentScale: "Content Scale",
43        dyslexiaFont: "Readable Font",
44        textAlign: "Align Right",
45        textAlignCenter: "Center",
46        textAlignLeft: "Align Left",
47        highlightLinks: "Underline Links",
48        hideImages: "Hide Images",
49        highlightHeadings: "Highlight Titles",
50        textMagnifier: "Cursor Reading Aid",
51        reset: "Reset",
52        poweredBy: "Powered by",
53        darkMode: "Dark Mode",
54        focusOutline: "Show Focus Outline",
55        reduceMotion: "Reduce Motion",
56        cursorSize: "Large Cursor",
57        stopAutoplay: "Stop Autoplay",
58        invertColors: "Invert Colors",
59        columnWidth: "Column Width",
60      },
61      hr: {
62        title: "Postavke pristupačnosti",
63        readingMask: "Maska za čitanje",
64        highContrast: "Visoki kontrast",
65        fontSize: "Veličina fonta",
66        lineHeight: "Visina retka",
67        letterSpacing: "Razmak slova",
68        contentScale: "Skaliranje sadržaja",
69        dyslexiaFont: "Čitljiv font",
70        textAlign: "Poravnaj desno",
71        textAlignCenter: "Centriraj",
72        textAlignLeft: "Poravnaj lijevo",
73        highlightLinks: "Podcrtaj poveznice",
74        hideImages: "Sakrij slike",
75        highlightHeadings: "Istakni naslove",
76        textMagnifier: "Pomoć za čitanje pokazivačem",
77        reset: "Resetiraj",
78        poweredBy: "Pokreće",
79        darkMode: "Tamni način",
80        focusOutline: "Prikaži obrub fokusa",
81        reduceMotion: "Smanji animacije",
82        cursorSize: "Veliki kursor",
83        stopAutoplay: "Zaustavi automatsku reprodukciju",
84        invertColors: "Invertiraj boje",
85        columnWidth: "Å irina stupca"
86      }
87    },
88  };
89
90  // Check if FontAwesome is already available
91  function hasFontAwesome() {
92    // First check if FontAwesome CSS is already loaded
93    if (document.querySelector('link[rel="stylesheet"][href*="fontawesome"]')) {
94      return true;
95    }
96
97    // Fallback check by testing the font family
98    const probe = document.createElement("i");
99    probe.className = "fas";
100    probe.style.display = "none";
101    document.body.appendChild(probe);
102
103    const fontFamily = getComputedStyle(probe).fontFamily || "";
104    probe.remove();
105
106    return /Font Awesome/i.test(fontFamily);
107  }
108
109  class AccessibilityWidget {
110    constructor() {
111      this.settings = this.loadSettings();
112      this.currentLang = this.settings.language || WIDGET_CONFIG.defaultLang;
113      this.translations = WIDGET_CONFIG.translations[this.currentLang];
114      this.isOpen = false;
115      this.observers = [];
116      this.init();
117    }
118
119    init() {
120      this.loadExternalResources();
121      this.injectStyles();
122      this.createWidget();
123      this.attachEventListeners();
124      this.setupSPAListeners();
125      this.applySettings();
126    }
127
128    // Set up listeners for Single Page Application behavior
129    setupSPAListeners() {
130      // Watch for DOM changes using MutationObserver
131      const observer = new MutationObserver((mutations) => {
132        let shouldReapply = false;
133
134        mutations.forEach((mutation) => {
135          // Check for new nodes being added
136          if (mutation.type === "childList" && mutation.addedNodes.length > 0) {
137            for (let node of mutation.addedNodes) {
138              if (node.nodeType === Node.ELEMENT_NODE) {
139                // Look for Flarum-specific elements
140                if (
141                  node.matches &&
142                  (node.matches(".Post-body, .DiscussionListItem, .UserCard, .CommentPost") ||
143                    node.querySelector(".Post-body, .DiscussionListItem, .UserCard, .CommentPost"))
144                ) {
145                  shouldReapply = true;
146                  break;
147                }
148              }
149            }
150          }
151        });
152
153        if (shouldReapply) {
154          // Give Flarum time to finish rendering before applying styles
155          setTimeout(() => {
156            this.applySettings();
157          }, 100);
158        }
159      });
160
161      // Monitor the entire document for changes
162      observer.observe(document.body, {
163        childList: true,
164        subtree: true,
165      });
166
167      this.observers.push(observer);
168
169      // Intercept browser navigation for SPA
170      const originalPushState = history.pushState;
171      const originalReplaceState = history.replaceState;
172
173      history.pushState = function (...args) {
174        originalPushState.apply(history, args);
175        setTimeout(() => {
176          if (window.accessibilityWidget) {
177            window.accessibilityWidget.applySettings();
178          }
179        }, 200);
180      };
181
182      history.replaceState = function (...args) {
183        originalReplaceState.apply(history, args);
184        setTimeout(() => {
185          if (window.accessibilityWidget) {
186            window.accessibilityWidget.applySettings();
187          }
188        }, 200);
189      };
190
191      // Handle back/forward navigation
192      window.addEventListener("popstate", () => {
193        setTimeout(() => {
194          this.applySettings();
195        }, 200);
196      });
197
198      // Handle hash changes
199      window.addEventListener("hashchange", () => {
200        setTimeout(() => {
201          this.applySettings();
202        }, 200);
203      });
204
205      // Fallback timer for very dynamic content
206      this.reapplyInterval = setInterval(() => {
207        this.applySettings();
208      }, 2000); // Every 2 seconds
209    }
210
211    loadExternalResources() {
212      const basePath = (document.currentScript?.src || "").split("/").slice(0, -1).join("/") + "/";
213
214      // Load FontAwesome only if it's not already available
215      if (!hasFontAwesome()) {
216        const link = document.createElement("link");
217        link.id = "fontawesome-local";
218        link.rel = "stylesheet";
219        link.href = basePath + "css/all.min.css";
220        document.head.appendChild(link);
221      }
222
223      // Load Lexend font for dyslexia support
224      if (!document.getElementById("lexend-embed")) {
225        const css = `
226        @font-face{
227          font-family:'Lexend';
228          font-style:normal;
229          font-weight:400;
230          font-display:swap;
231          src:url('${basePath}webfonts/lexend-400.woff2') format('woff2');
232        }
233        @font-face{
234          font-family:'Lexend';
235          font-style:normal;
236          font-weight:700;
237          font-display:swap;
238          src:url('${basePath}webfonts/lexend-700.woff2') format('woff2');
239        }`;
240        const style = document.createElement("style");
241        style.id = "lexend-embed";
242        style.textContent = css;
243        document.head.appendChild(style);
244      }
245    }
246
247    injectStyles() {
248      if (document.getElementById("a11y-widget-styles")) return;
249
250      const style = document.createElement("style");
251      style.id = "a11y-widget-styles";
252      style.textContent = `
253                /* Widget Container */
254                #a11y-widget-container {
255                    position: fixed;
256                    bottom: 20px;
257                    right: 20px;
258                    z-index: 999999;
259                    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
260                }
261                
262                /* Toggle Button */
263                #a11y-toggle-btn {
264                    width: 56px;
265                    height: 56px;
266                    border-radius: 50%;
267                    background: #2563eb;
268                    color: white;
269                    border: none;
270                    cursor: pointer;
271                    box-shadow: 0 4px 12px rgba(0,0,0,0.15);
272                    display: flex;
273                    align-items: center;
274                    justify-content: center;
275                    transition: all 0.3s ease;
276                    font-size: 24px;
277                }
278                
279                #a11y-toggle-btn:hover {
280                    transform: translateY(-2px);
281                    box-shadow: 0 6px 20px rgba(0,0,0,0.2);
282                }
283                
284                /* Panel */
285                #a11y-panel {
286                    position: absolute;
287                    bottom: 76px;
288                    right: 0;
289                    width: 380px;
290                    max-width: calc(100vw - 40px);
291                    max-height: calc(100vh - 120px);
292                    background: white;
293                    border-radius: 16px;
294                    box-shadow: 0 10px 40px rgba(0,0,0,0.1);
295                    display: none;
296                    overflow: hidden;
297                }
298                
299                @media (max-height: 700px) {
300                    #a11y-panel {
301                        bottom: 10px;
302                        max-height: calc(100vh - 80px);
303                    }
304                }
305                
306                @media (max-width: 420px) {
307                    #a11y-panel {
308                        width: calc(100vw - 40px);
309                        right: 50%;
310                        transform: translateX(50%);
311                    }
312                }
313                
314                #a11y-panel.active {
315                    display: flex;
316                    flex-direction: column;
317                    animation: slideIn 0.3s ease;
318                }
319                
320                @keyframes slideIn {
321                    from { opacity: 0; transform: translateY(20px); }
322                    to { opacity: 1; transform: translateY(0); }
323                }
324                
325                /* Header */
326                .a11y-header {
327                    background: #f8fafc;
328                    padding: 20px;
329                    border-bottom: 1px solid #e5e7eb;
330                    display: flex;
331                    justify-content: space-between;
332                    align-items: center;
333                }
334                
335                .a11y-header h3 {
336                    margin: 0;
337                    font-size: 18px;
338                    font-weight: 600;
339                    color: #1f2937;
340                }
341                
342                .a11y-header-actions {
343                    display: flex;
344                    gap: 8px;
345                    align-items: center;
346                }
347                
348                .a11y-lang-toggle {
349                    display: flex;
350                    background: #e5e7eb;
351                    border-radius: 8px;
352                    padding: 2px;
353                }
354                
355                .a11y-lang-btn {
356                    padding: 6px 12px;
357                    border: none;
358                    background: transparent;
359                    cursor: pointer;
360                    border-radius: 6px;
361                    font-size: 12px;
362                    font-weight: 500;
363                    color: #6b7280;
364                    transition: all 0.2s;
365                }
366                
367                .a11y-lang-btn.active {
368                    background: white;
369                    color: #1f2937;
370                    box-shadow: 0 1px 3px rgba(0,0,0,0.1);
371                }
372                
373                .a11y-close-btn {
374                    background: none;
375                    border: none;
376                    color: #6b7280;
377                    font-size: 20px;
378                    cursor: pointer;
379                    padding: 4px;
380                    width: 32px;
381                    height: 32px;
382                    border-radius: 6px;
383                    transition: all 0.2s;
384                }
385                
386                .a11y-close-btn:hover {
387                    background: #e5e7eb;
388                    color: #1f2937;
389                }
390                
391                /* Content */
392                .a11y-content {
393                    padding: 20px;
394                    overflow-y: auto;
395                    max-height: calc(100vh - 380px);
396                }
397                
398                @media (max-height: 700px) {
399                    .a11y-content {
400                        padding: 15px;
401                        max-height: calc(100vh - 340px);
402                    }
403                }
404                
405                /* Grid */
406                .a11y-grid {
407                    display: grid;
408                    grid-template-columns: repeat(2, 1fr);
409                    gap: 12px;
410                    margin-bottom: 20px;
411                }
412                
413                /* Cards */
414                .a11y-card {
415                    background: #f8fafc;
416                    border: 1px solid #e5e7eb;
417                    border-radius: 12px;
418                    padding: 16px;
419                    text-align: center;
420                    cursor: pointer;
421                    transition: all 0.2s;
422                }
423                
424                .a11y-card:hover {
425                    border-color: #2563eb;
426                    box-shadow: 0 2px 8px rgba(37, 99, 235, 0.1);
427                }
428                
429                .a11y-card.active {
430                    background: #dbeafe;
431                    border-color: #2563eb;
432                }
433                
434                .a11y-card-icon {
435                    font-size: 24px;
436                    margin-bottom: 8px;
437                    color: #1f2937;
438                }
439                
440                .a11y-card.active .a11y-card-icon {
441                    color: #2563eb;
442                }
443                
444                .a11y-card-label {
445                    font-size: 13px;
446                    color: #1f2937;
447                    font-weight: 500;
448                }
449                
450                /* Sliders */
451                .a11y-slider-group {
452                    background: #f8fafc;
453                    border-radius: 12px;
454                    padding: 16px;
455                    margin-bottom: 12px;
456                }
457                
458                .a11y-slider-header {
459                    display: flex;
460                    justify-content: space-between;
461                    align-items: center;
462                    margin-bottom: 12px;
463                }
464                
465                .a11y-slider-label {
466                    font-size: 14px;
467                    font-weight: 500;
468                    color: #1f2937;
469                }
470                
471                .a11y-slider-value {
472                    font-size: 14px;
473                    color: #6b7280;
474                    font-weight: 500;
475                }
476                
477                .a11y-slider-controls {
478                    display: flex;
479                    align-items: center;
480                    gap: 12px;
481                }
482                
483                .a11y-slider-btn {
484                    width: 32px;
485                    height: 32px;
486                    border-radius: 50%;
487                    background: white;
488                    border: 1px solid #e5e7eb;
489                    display: flex;
490                    align-items: center;
491                    justify-content: center;
492                    cursor: pointer;
493                    transition: all 0.2s;
494                    color: #1f2937;
495                }
496                
497                .a11y-slider-btn:hover {
498                    background: #f3f4f6;
499                    border-color: #d1d5db;
500                }
501                
502                .a11y-slider {
503                    flex: 1;
504                    height: 4px;
505                    -webkit-appearance: none;
506                    appearance: none;
507                    background: #e5e7eb;
508                    border-radius: 2px;
509                    outline: none;
510                    cursor: pointer;
511                }
512                
513                .a11y-slider::-webkit-slider-thumb {
514                    -webkit-appearance: none;
515                    appearance: none;
516                    width: 20px;
517                    height: 20px;
518                    background: #2563eb;
519                    border-radius: 50%;
520                    cursor: pointer;
521                    box-shadow: 0 1px 4px rgba(0,0,0,0.2);
522                    transition: all 0.2s;
523                }
524                
525                .a11y-slider::-webkit-slider-thumb:hover {
526                    transform: scale(1.2);
527                }
528                
529                .a11y-slider::-moz-range-thumb {
530                    width: 20px;
531                    height: 20px;
532                    background: #2563eb;
533                    border-radius: 50%;
534                    cursor: pointer;
535                    border: none;
536                    box-shadow: 0 1px 4px rgba(0,0,0,0.2);
537                }
538                
539                /* Footer */
540                .a11y-footer {
541                    padding: 16px 20px;
542                    border-top: 1px solid #e5e7eb;
543                    background: #f8fafc;
544                }
545                
546                .a11y-btn {
547                    width: 100%;
548                    padding: 12px;
549                    border: none;
550                    border-radius: 8px;
551                    cursor: pointer;
552                    font-size: 14px;
553                    font-weight: 500;
554                    transition: all 0.2s;
555                    display: flex;
556                    align-items: center;
557                    justify-content: center;
558                    gap: 8px;
559                    margin-bottom: 20px;
560                }
561                
562                .a11y-btn-reset {
563                    background: #ef4444;
564                    color: white;
565                }
566                
567                .a11y-btn-reset:hover {
568                    background: #dc2626;
569                }
570                
571                .a11y-reset-all {
572                    padding: 0 20px 20px 20px;
573                    background: #f8fafc;
574                }
575                
576                .a11y-reset-all button {
577                    width: 100%;
578                    padding: 10px;
579                    background: #f3f4f6;
580                    border: 1px solid #e5e7eb;
581                    border-radius: 8px;
582                    color: #6b7280;
583                    font-size: 13px;
584                    cursor: pointer;
585                    transition: all 0.2s;
586                    display: flex;
587                    align-items: center;
588                    justify-content: center;
589                    gap: 6px;
590                }
591                
592                .a11y-reset-all button:hover {
593                    background: #fee2e2;
594                    border-color: #fca5a5;
595                    color: #dc2626;
596                }
597                
598                .a11y-powered-by {
599                    padding: 15px 20px;
600                    background: #f1f5f9;
601                    text-align: center;
602                    border-top: 1px solid #e5e7eb;
603                    font-size: 12px;
604                    color: #64748b;
605                    border-radius: 0 0 16px 16px;
606                }
607                
608                .a11y-powered-by a {
609                    color: #2563eb;
610                    text-decoration: none;
611                    font-weight: 500;
612                }
613                
614                .a11y-powered-by a:hover {
615                    text-decoration: underline;
616                }
617
618                /* Enhanced Flarum selectors for better coverage */
619                .a11y-flarum-text {
620                    font-size: inherit !important;
621                }
622                
623                /* Accessibility Features */
624                body.a11y-dyslexia *:not([class*="fa"]) {
625                    font-family: 'Lexend', Arial, sans-serif !important;
626                }
627                
628                body.a11y-highlight-links a {
629                    text-decoration: underline !important;
630                    text-decoration-thickness: 2px !important;
631                    text-underline-offset: 2px !important;
632                }
633                
634                body.a11y-hide-images img {
635                    opacity: 0 !important;
636                    visibility: hidden !important;
637                }
638                
639                body.a11y-highlight-headings h1::before,
640                body.a11y-highlight-headings h2::before,
641                body.a11y-highlight-headings h3::before,
642                body.a11y-highlight-headings h4::before,
643                body.a11y-highlight-headings h5::before,
644                body.a11y-highlight-headings h6::before {
645                    content: '' !important;
646                    position: absolute !important;
647                    left: 0 !important;
648                    top: 0 !important;
649                    bottom: 0 !important;
650                    width: 4px !important;
651                    background: #2563eb !important;
652                }
653                
654                body.a11y-highlight-headings h1,
655                body.a11y-highlight-headings h2,
656                body.a11y-highlight-headings h3,
657                body.a11y-highlight-headings h4,
658                body.a11y-highlight-headings h5,
659                body.a11y-highlight-headings h6 {
660                    position: relative !important;
661                    padding-left: 20px !important;
662                }
663                
664                /* High Contrast */
665                body.a11y-high-contrast {
666                    background: #000 !important;
667                    color: #fff !important;
668                }
669                
670                body.a11y-high-contrast * {
671                    background-color: #000 !important;
672                    color: #fff !important;
673                    border-color: #fff !important;
674                }
675                
676                body.a11y-high-contrast a {
677                    color: #ffff00 !important;
678                }
679                
680                body.a11y-high-contrast button {
681                    background: #fff !important;
682                    color: #000 !important;
683                }
684                
685                /* Dark Mode */
686                body.a11y-dark-mode {
687                    background: #1a1a1a !important;
688                    color: #e0e0e0 !important;
689                }
690                
691                body.a11y-dark-mode * {
692                    background-color: #1a1a1a !important;
693                    color: #e0e0e0 !important;
694                    border-color: #444 !important;
695                }
696                
697                body.a11y-dark-mode a {
698                    color: #66b3ff !important;
699                }
700                
701                /* Focus Outline */
702                body.a11y-focus-outline *:focus {
703                    outline: 6px solid #ff0000 !important;
704                    outline-offset: 6px !important;
705                }
706                
707                /* Reduce Motion */
708                body.a11y-reduce-motion * {
709                    animation: none !important;
710                    transition: none !important;
711                }
712                
713                /* Large Cursor */
714                body.a11y-cursor-size * {
715                    cursor: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><path fill="black" stroke="white" stroke-width="2" d="M8 8 L28 20 L20 24 L24 36 L20 38 L16 26 L8 30 Z"/></svg>') 0 0, auto !important;
716                }
717                
718                /* Stop Autoplay */
719                body.a11y-stop-autoplay video,
720                body.a11y-stop-autoplay audio {
721                    autoplay: false !important;
722                    pause: true !important;
723                }
724                
725                /* Invert Colors */
726                html.a11y-invert::before{
727                    content:"";
728                    position:fixed;
729                    inset:0;
730                    background:#fff;
731                    mix-blend-mode:difference;
732                    pointer-events:none;
733                    z-index:2147483647;
734                }
735                
736                html.a11y-invert img,
737                html.a11y-invert video{
738                    mix-blend-mode:normal;
739                }
740                
741                /* Reading Mask */
742                #a11y-reading-mask {
743                    position: fixed;
744                    left: 0;
745                    right: 0;
746                    height: 120px;
747                    pointer-events: none;
748                    z-index: 999998;
749                    display: none;
750                }
751                
752                #a11y-reading-mask::before,
753                #a11y-reading-mask::after {
754                    content: '';
755                    position: absolute;
756                    left: 0;
757                    right: 0;
758                    background: rgba(0,0,0,0.3);
759                }
760                
761                #a11y-reading-mask::before {
762                    top: -100vh;
763                    height: 100vh;
764                }
765                
766                #a11y-reading-mask::after {
767                    bottom: -100vh;
768                    height: 100vh;
769                }
770                
771                body.a11y-reading-mask #a11y-reading-mask {
772                    display: block;
773                }
774                
775                /* Text Magnifier */
776                .a11y-magnifier {
777                    position: fixed;
778                    width: 400px;
779                    max-width: 90vw;
780                    padding: 20px;
781                    background: rgba(255, 255, 255, 0.98);
782                    border: 2px solid #2563eb;
783                    border-radius: 8px;
784                    box-shadow: 0 8px 32px rgba(0,0,0,0.2);
785                    pointer-events: none;
786                    z-index: 999997;
787                    display: none;
788                    font-size: 150%;
789                    line-height: 1.6;
790                    word-wrap: break-word;
791                    color: #000;
792                }
793                
794                body.a11y-text-magnifier .a11y-magnifier {
795                    display: block;
796                }
797                
798                .a11y-magnifier::before {
799                    content: '';
800                    position: absolute;
801                    top: 50%;
802                    left: 50%;
803                    transform: translate(-50%, -50%);
804                    width: 2px;
805                    height: 20px;
806                    background: rgba(37, 99, 235, 0.3);
807                    pointer-events: none;
808                }
809                
810                /* Responsive styles */
811                @media (max-width: 420px) {
812                    .a11y-grid {
813                        grid-template-columns: 1fr;
814                        gap: 8px;
815                    }
816                    
817                    .a11y-slider-group {
818                        padding: 12px;
819                    }
820                    
821                    .a11y-content {
822                        padding: 15px;
823                    }
824                }
825            `;
826      document.head.appendChild(style);
827    }
828
829    createWidget() {
830      // Prevent creating duplicate widgets
831      if (document.getElementById("a11y-widget-container")) return;
832
833      const container = document.createElement("div");
834      container.id = "a11y-widget-container";
835
836      // Create the main toggle button
837      const toggleBtn = document.createElement("button");
838      toggleBtn.id = "a11y-toggle-btn";
839      toggleBtn.setAttribute("aria-label", this.translations.title);
840      toggleBtn.innerHTML = '<i class="fas fa-universal-access"></i>';
841
842      // Create the settings panel
843      const panel = document.createElement("div");
844      panel.id = "a11y-panel";
845      panel.innerHTML = this.getPanelHTML();
846
847      container.appendChild(toggleBtn);
848      container.appendChild(panel);
849      document.body.appendChild(container);
850
851      // Create additional accessibility elements
852      this.createReadingMask();
853      this.createMagnifier();
854    }
855
856    getPanelHTML() {
857      return `
858                <div class="a11y-header">
859                    <h3>${this.translations.title}</h3>
860                    <div class="a11y-header-actions">
861                        <div class="a11y-lang-toggle">
862                            <button class="a11y-lang-btn ${this.currentLang === "hr" ? "active" : ""}" data-lang="hr">HR</button>
863                            <button class="a11y-lang-btn ${this.currentLang === "en" ? "active" : ""}" data-lang="en">EN</button>
864                        </div>
865                        <button class="a11y-close-btn" aria-label="Close">
866                            <i class="fas fa-times"></i>
867                        </button>
868                    </div>
869                </div>
870                <div class="a11y-content">
871                    <div class="a11y-grid">
872                        ${this.createFeatureCards()}
873                    </div>
874                    ${this.createSliders()}
875                </div>
876                <div class="a11y-footer">
877                    <button class="a11y-btn a11y-btn-reset">
878                        <i class="fas fa-undo"></i>
879                        <span>${this.translations.reset}</span>
880                    </button>
881                </div>
882                <div class="a11y-powered-by">
883                    ${this.translations.poweredBy} <a href="https://www.accessibility-widget.net" target="_blank" rel="noopener">Accessibility Widget</a>
884                </div>
885            `;
886    }
887
888    createFeatureCards() {
889      const features = [
890        { id: "readingMask", icon: "fa-eye", label: this.translations.readingMask },
891        { id: "highContrast", icon: "fa-adjust", label: this.translations.highContrast },
892        { id: "darkMode", icon: "fa-moon", label: this.translations.darkMode },
893        { id: "dyslexiaFont", icon: "fa-font", label: this.translations.dyslexiaFont },
894        { id: "textAlignRight", icon: "fa-align-right", label: this.translations.textAlign },
895        { id: "textAlignCenter", icon: "fa-align-center", label: this.translations.textAlignCenter },
896        { id: "textAlignLeft", icon: "fa-align-left", label: this.translations.textAlignLeft },
897        { id: "highlightLinks", icon: "fa-link", label: this.translations.highlightLinks },
898        { id: "hideImages", icon: "fa-image", label: this.translations.hideImages },
899        { id: "highlightHeadings", icon: "fa-heading", label: this.translations.highlightHeadings },
900        { id: "textMagnifier", icon: "fa-search-plus", label: this.translations.textMagnifier },
901        { id: "focusOutline", icon: "fa-square", label: this.translations.focusOutline },
902        { id: "reduceMotion", icon: "fa-pause", label: this.translations.reduceMotion },
903        { id: "cursorSize", icon: "fa-mouse-pointer", label: this.translations.cursorSize },
904        { id: "stopAutoplay", icon: "fa-stop", label: this.translations.stopAutoplay },
905        { id: "invertColors", icon: "fa-exchange-alt", label: this.translations.invertColors },
906      ];
907
908      return features
909        .map((f) => {
910          const isActive = this.isFeatureActive(f.id);
911          return `
912                    <div class="a11y-card ${isActive ? "active" : ""}" data-feature="${f.id}">
913                        <div class="a11y-card-icon">
914                            <i class="fas ${f.icon}"></i>
915                        </div>
916                        <div class="a11y-card-label">${f.label}</div>
917                    </div>
918                `;
919        })
920        .join("");
921    }
922
923    createSliders() {
924      const sliders = [
925        { id: "fontSize", label: this.translations.fontSize, min: 80, max: 150 },
926        { id: "lineHeight", label: this.translations.lineHeight, min: 100, max: 200 },
927        { id: "letterSpacing", label: this.translations.letterSpacing, min: 100, max: 150 },
928        { id: "contentScale", label: this.translations.contentScale, min: 80, max: 130 },
929        { id: "columnWidth", label: this.translations.columnWidth, min: 400, max: 1200 },
930      ];
931
932      return sliders
933        .map(
934          (s) => `
935                <div class="a11y-slider-group">
936                    <div class="a11y-slider-header">
937                        <span class="a11y-slider-label">${s.label}</span>
938                        <span class="a11y-slider-value" id="${s.id}Value">${this.settings[s.id] || (s.id === "columnWidth" ? 600 : 100)}${
939            s.id === "columnWidth" ? "px" : "%"
940          }</span>
941                    </div>
942                    <div class="a11y-slider-controls">
943                        <button class="a11y-slider-btn" data-action="decrease" data-target="${s.id}">
944                            <i class="fas fa-minus"></i>
945                        </button>
946                        <input type="range" class="a11y-slider" id="${s.id}" 
947                            min="${s.min}" max="${s.max}" value="${this.settings[s.id] || (s.id === "columnWidth" ? 600 : 100)}">
948                        <button class="a11y-slider-btn" data-action="increase" data-target="${s.id}">
949                            <i class="fas fa-plus"></i>
950                        </button>
951                    </div>
952                </div>
953            `
954        )
955        .join("");
956    }
957
958    createReadingMask() {
959      if (!document.getElementById("a11y-reading-mask")) {
960        const mask = document.createElement("div");
961        mask.id = "a11y-reading-mask";
962        document.body.appendChild(mask);
963      }
964    }
965
966    createMagnifier() {
967      if (!document.querySelector(".a11y-magnifier")) {
968        const magnifier = document.createElement("div");
969        magnifier.className = "a11y-magnifier";
970        magnifier.setAttribute("aria-hidden", "true");
971        document.body.appendChild(magnifier);
972      }
973    }
974
975    isFeatureActive(feature) {
976      if (feature.startsWith("textAlign")) {
977        const align = feature.replace("textAlign", "").toLowerCase();
978        return this.settings.textAlign === align;
979      }
980      return this.settings[feature] || false;
981    }
982
983    attachEventListeners() {
984      // Main toggle button
985      document.getElementById("a11y-toggle-btn").addEventListener("click", () => {
986        this.togglePanel();
987      });
988
989      // Close button
990      document.querySelector(".a11y-close-btn").addEventListener("click", () => {
991        this.togglePanel();
992      });
993
994      // Language toggle buttons
995      document.querySelectorAll(".a11y-lang-btn").forEach((btn) => {
996        btn.addEventListener("click", (e) => {
997          this.changeLanguage(e.target.dataset.lang);
998        });
999      });
1000
1001      // Feature toggle cards
1002      document.querySelectorAll(".a11y-card").forEach((card) => {
1003        card.addEventListener("click", () => {
1004          this.toggleFeature(card.dataset.feature, card);
1005        });
1006      });
1007
1008      // Range sliders
1009      document.querySelectorAll(".a11y-slider").forEach((slider) => {
1010        slider.addEventListener("input", (e) => {
1011          const id = e.target.id;
1012          const value = e.target.value;
1013          const suffix = id === "columnWidth" ? "px" : "%";
1014          document.getElementById(id + "Value").textContent = value + suffix;
1015          this.settings[id] = parseInt(value);
1016          this.applySettings();
1017          this.saveSettings();
1018        });
1019      });
1020
1021      // Slider increment/decrement buttons
1022      document.querySelectorAll(".a11y-slider-btn").forEach((btn) => {
1023        btn.addEventListener("click", () => {
1024          const action = btn.dataset.action;
1025          const target = btn.dataset.target;
1026          const slider = document.getElementById(target);
1027          const current = parseInt(slider.value);
1028          const step = target === "columnWidth" ? 50 : 1;
1029
1030          if (action === "increase") {
1031            slider.value = Math.min(current + step, parseInt(slider.max));
1032          } else {
1033            slider.value = Math.max(current - step, parseInt(slider.min));
1034          }
1035
1036          slider.dispatchEvent(new Event("input", { bubbles: true }));
1037        });
1038      });
1039
1040      // Reset button
1041      document.querySelector(".a11y-btn-reset").addEventListener("click", () => {
1042        this.resetSettings();
1043      });
1044
1045      // Set up mouse tracking for reading aids
1046      this.initMouseTracking();
1047    }
1048
1049    initMouseTracking() {
1050      let magnifierTimeout;
1051
1052      document.addEventListener("mousemove", (e) => {
1053        // Update reading mask position
1054        if (this.settings.readingMask) {
1055          const mask = document.getElementById("a11y-reading-mask");
1056          if (mask) {
1057            mask.style.top = e.clientY - 60 + "px";
1058          }
1059        }
1060
1061        // Update text magnifier
1062        if (this.settings.textMagnifier) {
1063          clearTimeout(magnifierTimeout);
1064          magnifierTimeout = setTimeout(() => {
1065            this.updateMagnifier(e);
1066          }, 10);
1067        }
1068      });
1069    }
1070
1071    updateMagnifier(e) {
1072      const magnifier = document.querySelector(".a11y-magnifier");
1073      if (!magnifier) return;
1074
1075      // Temporarily hide magnifier to get the element underneath
1076      magnifier.style.display = "none";
1077      const targetElement = document.elementFromPoint(e.clientX, e.clientY);
1078      magnifier.style.display = "block";
1079
1080      if (!targetElement || targetElement === document.body || targetElement === document.documentElement) {
1081        magnifier.style.display = "none";
1082        return;
1083      }
1084
1085      // Don't show magnifier over the widget itself
1086      if (targetElement.closest("#a11y-widget-container")) {
1087        magnifier.style.display = "none";
1088        return;
1089      }
1090
1091      // Find the text container
1092      let textContainer = targetElement;
1093      const blockTags = ["P", "DIV", "ARTICLE", "SECTION", "LI", "TD", "TH", "H1", "H2", "H3", "H4", "H5", "H6", "SPAN", "A"];
1094
1095      // Look for the nearest element with text content
1096      while (textContainer && textContainer.textContent.trim().length === 0) {
1097        textContainer = textContainer.parentElement;
1098      }
1099
1100      // For inline elements, try to get the parent block
1101      if (textContainer && !blockTags.includes(textContainer.tagName)) {
1102        const parent = textContainer.closest(blockTags.join(","));
1103        if (parent) textContainer = parent;
1104      }
1105
1106      if (!textContainer || !textContainer.textContent.trim()) {
1107        magnifier.style.display = "none";
1108        return;
1109      }
1110
1111      // Extract and clean text content
1112      const fullText = textContainer.textContent.trim().replace(/\s+/g, " ");
1113      const words = fullText.split(" ");
1114
1115      if (words.length === 0) {
1116        magnifier.style.display = "none";
1117        return;
1118      }
1119
1120      // Calculate relative position for word estimation
1121      const rect = textContainer.getBoundingClientRect();
1122      const relativeX = (e.clientX - rect.left) / rect.width;
1123      const relativeY = (e.clientY - rect.top) / rect.height;
1124
1125      // Estimate which word the cursor is near
1126      const estimatedPosition = Math.floor(words.length * (relativeY * 0.7 + relativeX * 0.3));
1127      const wordIndex = Math.max(0, Math.min(estimatedPosition, words.length - 1));
1128
1129      // Show a window of words around the cursor
1130      const wordsToShow = 20;
1131      const wordsBeforeCursor = 8;
1132
1133      let startIndex = Math.max(0, wordIndex - wordsBeforeCursor);
1134      let endIndex = Math.min(words.length, startIndex + wordsToShow);
1135
1136      // Adjust if we're near the end
1137      if (endIndex - startIndex < wordsToShow && startIndex > 0) {
1138        startIndex = Math.max(0, endIndex - wordsToShow);
1139      }
1140
1141      // Build the display text
1142      let displayText = words.slice(startIndex, endIndex).join(" ");
1143
1144      // Add ellipsis for truncated text
1145      if (startIndex > 0) displayText = "... " + displayText;
1146      if (endIndex < words.length) displayText = displayText + " ...";
1147
1148      // Update magnifier content
1149      magnifier.textContent = displayText;
1150
1151      // Position the magnifier near the cursor
1152      let left = e.clientX + 20;
1153      let top = e.clientY - magnifier.offsetHeight - 10;
1154
1155      // Keep magnifier within viewport
1156      const magnifierWidth = magnifier.offsetWidth;
1157      const magnifierHeight = magnifier.offsetHeight;
1158
1159      if (left + magnifierWidth > window.innerWidth - 20) {
1160        left = e.clientX - magnifierWidth - 20;
1161      }
1162      if (top < 20) {
1163        top = e.clientY + 20;
1164      }
1165      if (top + magnifierHeight > window.innerHeight - 20) {
1166        top = window.innerHeight - magnifierHeight - 20;
1167      }
1168
1169      magnifier.style.left = left + "px";
1170      magnifier.style.top = top + "px";
1171    }
1172
1173    toggleFeature(feature, card) {
1174      if (feature.startsWith("textAlign")) {
1175        // Handle text alignment features
1176        document.querySelectorAll('[data-feature^="textAlign"]').forEach((c) => {
1177          c.classList.remove("active");
1178        });
1179
1180        const align = feature.replace("textAlign", "").toLowerCase();
1181        if (this.settings.textAlign === align) {
1182          this.settings.textAlign = "default";
1183        } else {
1184          this.settings.textAlign = align;
1185          card.classList.add("active");
1186        }
1187      } else {
1188        // Handle boolean toggle features
1189        this.settings[feature] = !this.settings[feature];
1190        card.classList.toggle("active");
1191      }
1192
1193      this.applySettings();
1194      this.saveSettings();
1195    }
1196
1197    changeLanguage(lang) {
1198      this.currentLang = lang;
1199      this.translations = WIDGET_CONFIG.translations[lang];
1200      this.settings.language = lang;
1201
1202      // Update language toggle buttons
1203      document.querySelectorAll(".a11y-lang-btn").forEach((btn) => {
1204        btn.classList.toggle("active", btn.dataset.lang === lang);
1205      });
1206
1207      // Update all UI text
1208      this.updateUITexts();
1209      this.saveSettings();
1210    }
1211
1212    updateUITexts() {
1213      // Update header title
1214      document.querySelector(".a11y-header h3").textContent = this.translations.title;
1215
1216      // Update feature card labels
1217      const cardMappings = {
1218        readingMask: this.translations.readingMask,
1219        highContrast: this.translations.highContrast,
1220        darkMode: this.translations.darkMode,
1221        dyslexiaFont: this.translations.dyslexiaFont,
1222        textAlignRight: this.translations.textAlign,
1223        textAlignCenter: this.translations.textAlignCenter,
1224        textAlignLeft: this.translations.textAlignLeft,
1225        highlightLinks: this.translations.highlightLinks,
1226        hideImages: this.translations.hideImages,
1227        highlightHeadings: this.translations.highlightHeadings,
1228        textMagnifier: this.translations.textMagnifier,
1229        focusOutline: this.translations.focusOutline,
1230        reduceMotion: this.translations.reduceMotion,
1231        cursorSize: this.translations.cursorSize,
1232        stopAutoplay: this.translations.stopAutoplay,
1233        invertColors: this.translations.invertColors,
1234      };
1235
1236      Object.entries(cardMappings).forEach(([feature, text]) => {
1237        const label = document.querySelector(`[data-feature="${feature}"] .a11y-card-label`);
1238        if (label) label.textContent = text;
1239      });
1240
1241      // Update slider labels
1242      const sliderMappings = {
1243        fontSize: this.translations.fontSize,
1244        lineHeight: this.translations.lineHeight,
1245        letterSpacing: this.translations.letterSpacing,
1246        contentScale: this.translations.contentScale,
1247        columnWidth: this.translations.columnWidth,
1248      };
1249
1250      Object.entries(sliderMappings).forEach(([id, text]) => {
1251        const slider = document.getElementById(id);
1252        if (slider) {
1253          const label = slider.closest(".a11y-slider-group").querySelector(".a11y-slider-label");
1254          if (label) label.textContent = text;
1255        }
1256      });
1257
1258      // Update reset button text
1259      const resetBtn = document.querySelector(".a11y-btn-reset span");
1260      if (resetBtn) resetBtn.textContent = this.translations.reset;
1261    }
1262
1263    togglePanel() {
1264      this.isOpen = !this.isOpen;
1265      const panel = document.getElementById("a11y-panel");
1266      panel.classList.toggle("active", this.isOpen);
1267    }
1268
1269    // Enhanced applySettings method with better Flarum support
1270    applySettings() {
1271      // Remove all feature classes for clean state
1272      document.body.classList.remove(
1273        "a11y-dyslexia",
1274        "a11y-highlight-links",
1275        "a11y-hide-images",
1276        "a11y-highlight-headings",
1277        "a11y-reading-mask",
1278        "a11y-text-magnifier",
1279        "a11y-high-contrast",
1280        "a11y-dark-mode",
1281        "a11y-focus-outline",
1282        "a11y-reduce-motion",
1283        "a11y-cursor-size",
1284        "a11y-stop-autoplay"
1285      );
1286
1287      // Clear existing inline styles
1288      document.documentElement.style.fontSize = "";
1289      document.body.style.lineHeight = "";
1290      document.body.style.letterSpacing = "";
1291      document.body.style.zoom = "";
1292
1293      // Enhanced Flarum selectors for comprehensive coverage
1294      const flarumSelectors = [
1295        // Basic text elements
1296        "p",
1297        'div:not([class*="a11y"])',
1298        "li",
1299        "td",
1300        "th",
1301        "article",
1302        "section",
1303        "span",
1304        "a",
1305
1306        // Core Flarum classes
1307        ".Post-body",
1308        ".PostUser-name",
1309        ".PostUser",
1310        ".UserCard",
1311        ".DiscussionHero-title",
1312        ".TagLabel-name",
1313        ".TagItem",
1314        ".Post-header",
1315        ".CommentPost",
1316        ".PostMeta",
1317        ".Composer",
1318        ".UserCard-name",
1319        ".item-count",
1320        ".Notification-content",
1321        ".DiscussionListItem-title",
1322        ".DiscussionListItem-info",
1323        ".DiscussionListItem-excerpt",
1324        ".Header-title",
1325        ".Alert",
1326        ".container",
1327        ".sp_footer",
1328        ".App-header",
1329        ".App-footer",
1330        ".App-composer",
1331        ".Header-primary",
1332        ".Header-secondary",
1333        ".App-drawer",
1334        ".App-navigation",
1335        ".App-content",
1336        ".item-excerpt",
1337        ".Post-stream",
1338        ".Post",
1339        ".DiscussionListItem",
1340        ".DiscussionList",
1341
1342        // Additional Flarum UI elements
1343        ".Button-label",
1344        ".Dropdown-toggle",
1345        ".FormControl",
1346        ".TextEditor-editor",
1347        ".NotificationList",
1348        ".Badge",
1349        ".Tooltip",
1350        ".Modal-content",
1351        ".Alert-body",
1352        ".Search-input",
1353        ".IndexPage-nav",
1354        ".DiscussionPage-nav",
1355        ".UserPage-content",
1356        ".SettingsPage-content",
1357        ".ExtensionPage-content",
1358        ".TagsPage-content",
1359
1360        // Dynamic content selectors
1361        '[class*="Post"]',
1362        '[class*="Discussion"]',
1363        '[class*="User"]',
1364        '[class*="Tag"]',
1365        '[class*="Comment"]',
1366        '[class*="Stream"]',
1367        '[class*="List"]',
1368      ];
1369
1370      const textElements = document.querySelectorAll(flarumSelectors.join(", "));
1371
1372      // Clear existing styles from all elements
1373      textElements.forEach((el) => {
1374        // el.style.fontSize = "";
1375        // el.style.textAlign = "";
1376        // el.style.maxWidth = "";
1377        el.classList.remove("a11y-flarum-text");
1378      });
1379
1380      // Apply font size with enhanced Flarum support
1381      if (this.settings.fontSize && this.settings.fontSize !== 100) {
1382        const fontSizePercent = this.settings.fontSize + "%";
1383
1384        // Set global font size
1385        document.documentElement.style.setProperty("font-size", fontSizePercent, "important");
1386        document.body.style.setProperty("font-size", fontSizePercent, "important");
1387
1388        // Apply to all Flarum elements specifically
1389        textElements.forEach((el) => {
1390          el.style.setProperty("font-size", fontSizePercent, "important");
1391          el.classList.add("a11y-flarum-text");
1392        });
1393
1394        // Inject CSS for dynamically loaded content
1395        this.injectDynamicFontSizeCSS(this.settings.fontSize);
1396      } else {
1397        this.removeDynamicFontSizeCSS();
1398      }
1399
1400      // Apply line height
1401      if (this.settings.lineHeight && this.settings.lineHeight !== 100) {
1402        const value = this.settings.lineHeight / 100;
1403        document.body.style.setProperty("line-height", value, "important");
1404        textElements.forEach((el) => {
1405          el.style.setProperty("line-height", value, "important");
1406        });
1407      }
1408
1409      // Apply letter spacing
1410      if (this.settings.letterSpacing && this.settings.letterSpacing !== 100) {
1411        const value = (this.settings.letterSpacing - 100) * 0.05;
1412        document.body.style.setProperty("letter-spacing", value + "em", "important");
1413        textElements.forEach((el) => {
1414          el.style.setProperty("letter-spacing", value + "em", "important");
1415        });
1416      }
1417
1418      // Apply content scaling
1419      if (this.settings.contentScale && this.settings.contentScale !== 100) {
1420        document.body.style.zoom = this.settings.contentScale + "%";
1421      }
1422
1423      // Apply column width
1424      if (this.settings.columnWidth && this.settings.columnWidth !== 600) {
1425        textElements.forEach((el) => {
1426          el.style.maxWidth = this.settings.columnWidth + "px";
1427        });
1428      }
1429
1430      // Apply text alignment
1431      if (this.settings.textAlign && this.settings.textAlign !== "default") {
1432        textElements.forEach((el) => {
1433          el.style.setProperty("text-align", this.settings.textAlign, "important");
1434        });
1435      }
1436
1437      // Apply feature classes
1438      if (this.settings.dyslexiaFont) document.body.classList.add("a11y-dyslexia");
1439      if (this.settings.highlightLinks) document.body.classList.add("a11y-highlight-links");
1440      if (this.settings.hideImages) document.body.classList.add("a11y-hide-images");
1441      if (this.settings.highlightHeadings) document.body.classList.add("a11y-highlight-headings");
1442      if (this.settings.readingMask) document.body.classList.add("a11y-reading-mask");
1443      if (this.settings.textMagnifier) document.body.classList.add("a11y-text-magnifier");
1444      if (this.settings.highContrast) document.body.classList.add("a11y-high-contrast");
1445      if (this.settings.darkMode) document.body.classList.add("a11y-dark-mode");
1446      if (this.settings.focusOutline) document.body.classList.add("a11y-focus-outline");
1447      if (this.settings.reduceMotion) document.body.classList.add("a11y-reduce-motion");
1448      if (this.settings.cursorSize) document.body.classList.add("a11y-cursor-size");
1449      if (this.settings.stopAutoplay) {
1450        document.body.classList.add("a11y-stop-autoplay");
1451        document.querySelectorAll("video[autoplay], audio[autoplay]").forEach((media) => {
1452          media.pause();
1453          media.removeAttribute("autoplay");
1454        });
1455      }
1456
1457      // Handle color inversion
1458      if (this.settings.invertColors) {
1459        document.documentElement.classList.add("a11y-invert");
1460      } else {
1461        document.documentElement.classList.remove("a11y-invert");
1462      }
1463    }
1464
1465    // Helper methods for dynamic CSS rules
1466    injectDynamicFontSizeCSS(fontSize) {
1467      // Remove any existing dynamic CSS
1468      this.removeDynamicFontSizeCSS();
1469
1470      const style = document.createElement("style");
1471      style.id = "a11y-dynamic-fontsize";
1472      style.textContent = `
1473        /* Dynamic font size for all Flarum elements */
1474        .Post-body *, .DiscussionListItem *, .UserCard *, .CommentPost *,
1475        .Header-title *, .App-content *, .Post-stream *, .DiscussionList *,
1476        [class*="Post"] *, [class*="Discussion"] *, [class*="User"] *,
1477        [class*="Comment"] *, [class*="Stream"] *, [class*="List"] * {
1478          font-size: ${fontSize}% !important;
1479        }
1480        
1481        /* Specific Flarum classes */
1482        .Post-body, .PostUser-name, .PostUser, .UserCard, .DiscussionHero-title,
1483        .TagLabel-name, .TagItem, .Post-header, .CommentPost, .PostMeta,
1484        .UserCard-name, .item-count, .Notification-content, .DiscussionListItem-title,
1485        .DiscussionListItem-info, .DiscussionListItem-excerpt, .Header-title,
1486        .Button-label, .Dropdown-toggle, .FormControl, .TextEditor-editor {
1487          font-size: ${fontSize}% !important;
1488        }
1489      `;
1490      document.head.appendChild(style);
1491    }
1492
1493    removeDynamicFontSizeCSS() {
1494      const existingStyle = document.getElementById("a11y-dynamic-fontsize");
1495      if (existingStyle) {
1496        existingStyle.remove();
1497      }
1498    }
1499
1500    resetSettings() {
1501      // Reset all numeric settings to defaults
1502      this.settings.fontSize = 100;
1503      this.settings.lineHeight = 100;
1504      this.settings.letterSpacing = 100;
1505      this.settings.contentScale = 100;
1506      this.settings.columnWidth = 600;
1507      this.settings.textAlign = "default";
1508
1509      // Reset all boolean features
1510      this.settings.readingMask = false;
1511      this.settings.highContrast = false;
1512      this.settings.darkMode = false;
1513      this.settings.dyslexiaFont = false;
1514      this.settings.highlightLinks = false;
1515      this.settings.hideImages = false;
1516      this.settings.highlightHeadings = false;
1517      this.settings.textMagnifier = false;
1518      this.settings.focusOutline = false;
1519      this.settings.reduceMotion = false;
1520      this.settings.cursorSize = false;
1521      this.settings.stopAutoplay = false;
1522      this.settings.invertColors = false;
1523
1524      // Update slider values in UI
1525      ["fontSize", "lineHeight", "letterSpacing", "contentScale"].forEach((id) => {
1526        const slider = document.getElementById(id);
1527        if (slider) {
1528          slider.value = 100;
1529          document.getElementById(id + "Value").textContent = "100%";
1530        }
1531      });
1532
1533      // Update column width slider
1534      const columnSlider = document.getElementById("columnWidth");
1535      if (columnSlider) {
1536        columnSlider.value = 600;
1537        document.getElementById("columnWidthValue").textContent = "600px";
1538      }
1539
1540      // Remove active state from all feature cards
1541      document.querySelectorAll(".a11y-card").forEach((card) => {
1542        card.classList.remove("active");
1543      });
1544
1545      // Clean up dynamic CSS
1546      this.removeDynamicFontSizeCSS();
1547
1548      this.applySettings();
1549      this.saveSettings();
1550    }
1551
1552    getDefaultSettings() {
1553      return {
1554        fontSize: 100,
1555        lineHeight: 100,
1556        letterSpacing: 100,
1557        contentScale: 100,
1558        columnWidth: 600,
1559        textAlign: "default",
1560        language: this.currentLang,
1561        readingMask: false,
1562        highContrast: false,
1563        darkMode: false,
1564        dyslexiaFont: false,
1565        highlightLinks: false,
1566        hideImages: false,
1567        highlightHeadings: false,
1568        textMagnifier: false,
1569        focusOutline: false,
1570        reduceMotion: false,
1571        cursorSize: false,
1572        stopAutoplay: false,
1573        invertColors: false,
1574      };
1575    }
1576
1577    saveSettings() {
1578      try {
1579        localStorage.setItem(WIDGET_CONFIG.storageKey, JSON.stringify(this.settings));
1580      } catch (e) {
1581        console.warn("Could not save accessibility settings:", e);
1582      }
1583    }
1584
1585    loadSettings() {
1586      try {
1587        const saved = localStorage.getItem(WIDGET_CONFIG.storageKey);
1588        if (saved) {
1589          return { ...this.getDefaultSettings(), ...JSON.parse(saved) };
1590        }
1591      } catch (e) {
1592        console.warn("Could not load accessibility settings:", e);
1593      }
1594      return this.getDefaultSettings();
1595    }
1596
1597    // Cleanup method for observers and intervals
1598    destroy() {
1599      // Stop all observers
1600      this.observers.forEach((observer) => observer.disconnect());
1601      this.observers = [];
1602
1603      // Stop the reapply interval
1604      if (this.reapplyInterval) {
1605        clearInterval(this.reapplyInterval);
1606      }
1607
1608      // Remove dynamic CSS rules
1609      this.removeDynamicFontSizeCSS();
1610    }
1611  }
1612
1613  // Initialize the accessibility widget
1614  function initAccessibilityWidget() {
1615    if (window.accessibilityWidget) return;
1616    window.accessibilityWidget = new AccessibilityWidget();
1617  }
1618
1619  // Wait for DOM to be ready
1620  if (document.readyState === "loading") {
1621    document.addEventListener("DOMContentLoaded", initAccessibilityWidget);
1622  } else {
1623    initAccessibilityWidget();
1624  }
1625})();

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.