PageSourceSearch

https://cosmos.bank.in/js/wcag-remediation.js?v=20260804r37

js cosmos.bank.in collected 2026-10-10 02:36:26 UTC 79,581 bytes, 1,432 lines download raw bytes

1/* ============================================================================
2   wcag-remediation.js
3   ----------------------------------------------------------------------------
4   WCAG 2.2 AA remediation (Sugamya audit) — additive, progressive-enhancement
5   JavaScript companion to wcag-remediation.css. Loaded last in _Layout.cshtml.
6
7   Everything here only ADDS accessibility attributes to existing markup; it
8   never changes behaviour or layout, and the page works without it.
9
10   To revert: delete this file and remove its <script> from _Layout.cshtml.
11   ============================================================================ */
12(function () {
13    'use strict';
14
15    /* ------------------------------------------------------------------------
16       jQuery 4 compatibility shim
17       ------------------------------------------------------------------------
18       The site upgraded to jquery-4.0.0, which REMOVED long-deprecated helpers
19       ($.isArray, $.trim, $.isFunction, $.type, $.browser, and the .bind()/
20       .unbind()/.delegate()/.size() instance methods). The old jquery.fancybox
21       library (Customer Care / Branch Locator / "Write to us" / board-of-directors
22       popups across the site) calls these and throws "$.isArray is not a function",
23       so those dialogs NEVER OPEN on the current site. Restore the removed helpers
24       so the popups work again — this both fixes a live site regression and is a
25       prerequisite for the Home Sr30 dialog focus-trap (a dialog must open to trap).
26       Runs immediately (jQuery is already loaded by the time this script runs).
27       ------------------------------------------------------------------------ */
28    (function jqueryCompatShim() {
29        var $ = window.jQuery;
30        if (!$) { return; }
31        if (typeof $.isArray !== 'function') { $.isArray = Array.isArray; }
32        if (typeof $.isFunction !== 'function') { $.isFunction = function (o) { return typeof o === 'function'; }; }
33        if (typeof $.isWindow !== 'function') { $.isWindow = function (o) { return o != null && o === o.window; }; }
34        if (typeof $.trim !== 'function') { $.trim = function (s) { return s == null ? '' : String(s).replace(/^[\s\xA0]+|[\s\xA0]+$/g, ''); }; }
35        if (typeof $.type !== 'function') {
36            $.type = function (o) {
37                if (o == null) { return o + ''; }
38                return (typeof o === 'object' || typeof o === 'function')
39                    ? Object.prototype.toString.call(o).slice(8, -1).toLowerCase()
40                    : typeof o;
41            };
42        }
43        if (typeof $.browser === 'undefined') {
44            var ua = navigator.userAgent.toLowerCase();
45            $.browser = { msie: /msie|trident/.test(ua), version: '0', webkit: /webkit/.test(ua), mozilla: /firefox/.test(ua), safari: /safari/.test(ua) };
46        }
47        if ($.fn) {
48            if (typeof $.fn.bind !== 'function') { $.fn.bind = function (t, d, f) { return this.on(t, null, d, f); }; }
49            if (typeof $.fn.unbind !== 'function') { $.fn.unbind = function (t, f) { return this.off(t, f); }; }
50            if (typeof $.fn.delegate !== 'function') { $.fn.delegate = function (s, t, d, f) { return this.on(t, s, d, f); }; }
51            if (typeof $.fn.undelegate !== 'function') { $.fn.undelegate = function (s, t, f) { return arguments.length ? this.off(t, s, f) : this.off('.'); }; }
52            if (typeof $.fn.size !== 'function') { $.fn.size = function () { return this.length; }; }
53            if (typeof $.fn.andSelf !== 'function' && typeof $.fn.addBack === 'function') { $.fn.andSelf = $.fn.addBack; }
54        }
55    })();
56
57    /* ------------------------------------------------------------------------
58       BATCH B3 — Link Purpose for links opening a new window/tab
59       WCAG 2.4.4 Link Purpose (In Context) (A)
60       ------------------------------------------------------------------------
61       Many target="_blank" links (footer, header, "Need Help?" section, social
62       icons) give no warning that they open a new window. We append an
63       accessible "(opens in a new window)" cue to each link's accessible name,
64       and add rel="noopener" for security. Visible text is unchanged.
65       ------------------------------------------------------------------------ */
66    // Derive a readable name for an icon-only link from its URL host
67    // (e.g. facebook.com -> "Facebook"). Covers the social-media icons.
68    var HOST_NAMES = {
69        'facebook.com': 'Facebook',
70        'twitter.com': 'Twitter',
71        'x.com': 'X',
72        'instagram.com': 'Instagram',
73        'linkedin.com': 'LinkedIn',
74        'youtube.com': 'YouTube'
75    };
76    function nameFromHref(a) {
77        var host;
78        try { host = new URL(a.href).hostname.replace(/^www\./, ''); } catch (e) { return ''; }
79        if (HOST_NAMES[host]) { return HOST_NAMES[host]; }
80        for (var key in HOST_NAMES) {
81            if (HOST_NAMES.hasOwnProperty(key) && host.indexOf(key) !== -1) {
82                return HOST_NAMES[key];
83            }
84        }
85        return '';
86    }
87
88    function enhanceExternalLinks() {
89        var links = document.querySelectorAll('a[target="_blank"]');
90        Array.prototype.forEach.call(links, function (a) {
91            // Security hardening for target="_blank".
92            var rel = a.getAttribute('rel') || '';
93            if (rel.indexOf('noopener') === -1) {
94                a.setAttribute('rel', (rel + ' noopener').trim());
95            }
96            // Don't double-annotate.
97            var existing = a.getAttribute('aria-label') || '';
98            if (/opens? in (a )?new (window|tab)/i.test(existing)) {
99                return;
100            }
101            // Pick the best existing accessible name; for icon-only links
102            // (no text/label/title) derive one from the URL (4.1.2 Name).
103            var name = existing ||
104                (a.textContent || '').replace(/\s+/g, ' ').trim() ||
105                a.getAttribute('title') ||
106                nameFromHref(a);
107            if (!name) {
108                return; // truly unnameable — leave for manual review
109            }
110            a.setAttribute('aria-label', name + ' (opens in a new window)');
111        });
112    }
113
114    /* ------------------------------------------------------------------------
115       BATCH B4 — News ticker Pause/Play control
116       WCAG 2.2.2 Pause, Stop, Hide (A)
117       ------------------------------------------------------------------------
118       Wires the keyboard-operable button rendered by _Marquee.cshtml. The
119       scrolling itself is CSS; this just toggles the .is-paused state and keeps
120       the button's accessible name / pressed state in sync.
121       ------------------------------------------------------------------------ */
122    /* The ticker scroll is a CSS animation over a fixed DURATION, so with a lot
123       of news items the pixel-speed becomes very fast. The original <marquee>
124       scrolled at a CONSTANT pixel rate. Re-derive the animation duration from
125       the content width so the pace matches the old marquee (~70 px/sec),
126       regardless of how much content there is. */
127    var TICKER_PX_PER_SEC = 70; // matches the original <marquee> default pace
128    function setTickerSpeed() {
129        Array.prototype.forEach.call(document.querySelectorAll('.wcag-ticker-track'), function (track) {
130            // track holds two copies of the content; one copy = half its width,
131            // which is exactly the distance the -50% animation travels.
132            var contentWidth = track.scrollWidth / 2;
133            if (contentWidth > 0) {
134                track.style.animationDuration = Math.max(10, Math.round(contentWidth / TICKER_PX_PER_SEC)) + 's';
135            }
136        });
137    }
138
139    function initTickerControls() {
140        var tickers = document.querySelectorAll('.wcag-ticker');
141        Array.prototype.forEach.call(tickers, function (ticker) {
142            var btn = ticker.querySelector('.wcag-ticker-btn');
143            if (!btn || btn.dataset.wcagBound) { return; }
144            btn.dataset.wcagBound = '1';
145            btn.addEventListener('click', function () {
146                var paused = ticker.classList.toggle('is-paused');
147                btn.setAttribute('aria-pressed', paused ? 'true' : 'false');
148                btn.setAttribute('aria-label', paused ? 'Play news ticker' : 'Pause news ticker');
149            });
150        });
151    }
152
153    /* ------------------------------------------------------------------------
154       BATCH C1 — Accessible names for <select> dropdowns
155       WCAG 4.1.2 Name, Role, Value (A)
156       ------------------------------------------------------------------------
157       Product Finder, Locate-Us (state/city/branch), customer type and the
158       language switcher are <select>s with NO programmatic name. Screen readers
159       announce them only as "combo box". We give each an aria-label derived from
160       its placeholder option (or its id), without changing anything visible.
161       ------------------------------------------------------------------------ */
162    function titleCase(s) {
163        return s.toLowerCase().replace(/\b\w/g, function (c) { return c.toUpperCase(); });
164    }
165    function selectHasName(s) {
166        if (s.getAttribute('aria-label') || s.getAttribute('aria-labelledby') || s.getAttribute('title')) {
167            return true;
168        }
169        return !!(s.id && document.querySelector('label[for="' + (window.CSS && CSS.escape ? CSS.escape(s.id) : s.id) + '"]'));
170    }
171    function deriveSelectLabel(s) {
172        if (s.id === 'selectLocale') { return 'Select language'; }
173        var opt = (s.options && s.options[0]) ? (s.options[0].text || '').trim() : '';
174        var t = opt.replace(/[-–—]/g, ' ').replace(/\s+/g, ' ').trim();
175        if (/select/i.test(t) && t.length <= 40) { return titleCase(t); }
176        var base = (s.id || s.name || 'option')
177            .replace(/Dropdown$/i, '')
178            .replace(/^ddl/i, '')
179            .replace(/Category1$/, 'Category')
180            .replace(/([a-z])([A-Z])/g, '$1 $2')
181            .replace(/[_-]+/g, ' ')
182            .trim();
183        return 'Select ' + titleCase(base);
184    }
185    function initSelectLabels() {
186        var selects = document.querySelectorAll('select');
187        Array.prototype.forEach.call(selects, function (s) {
188            if (selectHasName(s)) { return; }
189            s.setAttribute('aria-label', deriveSelectLabel(s));
190        });
191    }
192
193    /* ------------------------------------------------------------------------
194       BATCH C2 — Carousel regions + Prev/Next controls
195       WCAG 4.1.2 Name, Role, Value (A) + 2.1.1 Keyboard (A)
196       ------------------------------------------------------------------------
197       - Bootstrap banner controls (<a>): add Prev/Next accessible names.
198       - Owl carousel controls (<div>): not focusable -> make them real buttons
199         (role=button, tabindex=0, Enter/Space activation) and name them.
200       - Carousels themselves: add a region role + label (from a nearby heading).
201       Re-run a few times because Owl builds its nav asynchronously after init.
202       ------------------------------------------------------------------------ */
203    function nearestHeadingText(el) {
204        var sec = el.closest('section, .section, [class*="-section"]') || el.parentElement;
205        for (var i = 0; i < 4 && sec; i++) {
206            var h = sec.querySelector('h1, h2, h3, h4');
207            if (h && h.textContent.trim()) { return h.textContent.trim().replace(/\s+/g, ' ').slice(0, 40); }
208            sec = sec.parentElement;
209        }
210        return '';
211    }
212    function enhanceCarousels() {
213        // Bootstrap banner/dialog prev/next controls (these DO navigate) -> name
214        // them. Auditor wants the "Go to Previous/Next Slide" wording (Home Sr70).
215        Array.prototype.forEach.call(
216            document.querySelectorAll('.carousel-control, .carousel-control-prev, .carousel-control-next'),
217            function (c) {
218                if (!c.getAttribute('aria-label')) {
219                    c.setAttribute('aria-label', /prev|left/i.test(c.className) ? 'Go to Previous Slide' : 'Go to Next Slide');
220                }
221            }
222        );
223        // Carousel region -> name ONLY a #themeSlider that ACTUALLY rotates (more
224        // than one slide, e.g. the 22-slide home banner). Inner-page banners use
225        // the same #themeSlider markup but hold a SINGLE slide, so announcing them
226        // as a "carousel" is misleading — the auditor flagged "unnecessary carousel
227        // region ... applicable to all pages" (About Us New row). For those, strip
228        // the carousel semantics. The .owl-carousel grids and #popupCarousel are
229        // handled elsewhere.
230        Array.prototype.forEach.call(document.querySelectorAll('#themeSlider'), function (c) {
231            var slides = c.querySelectorAll('.carousel-item, .item');
232            if (slides.length <= 1) {
233                c.removeAttribute('aria-roledescription');
234                if (c.getAttribute('role') === 'region') { c.removeAttribute('role'); }
235                return;
236            }
237            if (!c.getAttribute('aria-label')) {
238                var name = nearestHeadingText(c);
239                c.setAttribute('role', 'region');
240                c.setAttribute('aria-roledescription', 'carousel');
241                c.setAttribute('aria-label', (name ? name + ' carousel' : 'Banner carousel'));
242            }
243        });
244        // NOTE: Owl arrows (.owl-prev/.owl-next) are hidden by the site and do
245        // not navigate (all items shown at once), so they are intentionally not
246        // turned into focus stops. Content cards remain Tab-reachable.
247    }
248
249    /* ------------------------------------------------------------------------
250       BATCH R2-CT — In-page content tabs: full WAI-ARIA Tabs pattern
251       WCAG 4.1.2 / 1.3.1 / 2.1.1 (About Us Sr30/66/67/68; also interest-rates)
252       ------------------------------------------------------------------------
253       Content tab strips (<ul class="nav nav-tabs"> with data-toggle / data-bs-
254       toggle="tab" anchors) carry NO ARIA and switch via Bootstrap + hover only.
255       Add role=tablist/tab/tabpanel, aria-selected + aria-controls, roving
256       tabindex and arrow-key navigation. The header mega-menu (#nav-tab) is
257       handled by enhanceHeaderMenuTabs and skipped here.
258       ------------------------------------------------------------------------ */
259    function enhanceContentTabs() {
260        Array.prototype.forEach.call(document.querySelectorAll('ul.nav-tabs, .nav.nav-tabs'), function (list) {
261            if (list.id === 'nav-tab' || list.dataset.wcagCTabs) { return; }
262            var links = Array.prototype.slice.call(list.querySelectorAll('a[data-toggle="tab"], a[data-bs-toggle="tab"]'));
263            if (!links.length) { return; }
264            list.dataset.wcagCTabs = '1';
265            list.setAttribute('role', 'tablist');
266            var tabs = links.map(function (a) {
267                var href = a.getAttribute('href') || '';
268                var panel = href.charAt(0) === '#' && href.length > 1 ? document.getElementById(href.slice(1)) : null;
269                a.setAttribute('role', 'tab');
270                ensureId(a);
271                var li = a.closest('li');
272                if (li) { li.setAttribute('role', 'presentation'); }
273                if (panel) {
274                    a.setAttribute('aria-controls', panel.id);
275                    panel.setAttribute('role', 'tabpanel');
276                    panel.setAttribute('aria-labelledby', a.id);
277                    if (!panel.hasAttribute('tabindex')) { panel.setAttribute('tabindex', '0'); }
278                }
279                return { a: a, panel: panel, li: li };
280            });
281            function isActive(t) {
282                if (t.li && t.li.classList.contains('active')) { return true; }
283                if (t.a.classList.contains('active')) { return true; }
284                if (t.panel && (t.panel.classList.contains('show') || t.panel.classList.contains('active'))) { return true; }
285                return false;
286            }
287            function sync() {
288                var anyActive = tabs.some(isActive);
289                tabs.forEach(function (t, i) {
290                    var on = anyActive ? isActive(t) : i === 0;
291                    t.a.setAttribute('aria-selected', on ? 'true' : 'false');
292                    t.a.setAttribute('tabindex', on ? '0' : '-1');
293                });
294            }
295            // Switch panels MANUALLY. The markup uses Bootstrap-3/4 data-toggle="tab"
296            // but the site loads Bootstrap 5 (which only wires data-bs-toggle), so a
297            // plain click doesn't switch the panel — the site relies on hover only.
298            // Toggling the classes ourselves makes tabs work by keyboard AND click,
299            // independent of Bootstrap version, and keeps aria-selected in sync.
300            function activate(t, focus) {
301                tabs.forEach(function (x) {
302                    var on = x === t;
303                    x.a.setAttribute('aria-selected', on ? 'true' : 'false');
304                    x.a.setAttribute('tabindex', on ? '0' : '-1');
305                    x.a.classList.toggle('active', on);
306                    if (x.li) { x.li.classList.toggle('active', on); }
307                    if (x.panel) {
308                        x.panel.classList.toggle('active', on);
309                        x.panel.classList.toggle('show', on);
310                    }
311                });
312                if (focus) { try { t.a.focus(); } catch (e) {} }
313            }
314            tabs.forEach(function (t, i) {
315                t.a.addEventListener('click', function (e) { e.preventDefault(); activate(t, false); });
316                t.a.addEventListener('keydown', function (e) {
317                    var next = null;
318                    switch (e.key) {
319                        case 'ArrowRight': case 'ArrowDown': next = tabs[(i + 1) % tabs.length]; break;
320                        case 'ArrowLeft': case 'ArrowUp': next = tabs[(i - 1 + tabs.length) % tabs.length]; break;
321                        case 'Home': next = tabs[0]; break;
322                        case 'End': next = tabs[tabs.length - 1]; break;
323                        case 'Enter': case ' ': case 'Spacebar': e.preventDefault(); activate(t, true); return;
324                        default: return;
325                    }
326                    e.preventDefault();
327                    activate(next, true);
328                });
329            });
330            sync();
331            // The site also switches panels on hover (about_us.js / tabchange.js);
332            // keep aria-selected in sync after those class changes.
333            list.addEventListener('mouseover', function () { setTimeout(sync, 60); });
334        });
335    }
336
337    /* ------------------------------------------------------------------------
338       BATCH C4 — Descriptive names for vague "Know more" / "Read more" links
339       WCAG 4.1.2 Name, Role, Value (A) + 2.4.4 Link Purpose
340       ------------------------------------------------------------------------
341       Many cards have a "Know more" / "Read more" control whose text gives no
342       context. We append the card's heading via aria-label ("Know more about
343       <scheme>"), leaving the visible text unchanged.
344       ------------------------------------------------------------------------ */
345    var VAGUE = /^(know more|read more|learn more|more|click here|details|view more)$/i;
346    function cardHeadingFor(el) {
347        var card = el.closest('.card, [class*="card"], [class*="single-content"], [class*="scheme"], li, article, .col-lg-3, .col-lg-4, .col-md-3, .col-md-4');
348        if (card) {
349            var h = card.querySelector('h1,h2,h3,h4,h5,h6,.title,[class*="title"],[class*="heading"]');
350            if (h && h.textContent.trim()) { return h.textContent.trim(); }
351        }
352        // walk back to a preceding heading
353        var n = el;
354        for (var i = 0; i < 30 && n; i++) {
355            n = n.previousElementSibling || (n.parentElement);
356            if (!n || n === document.body) { break; }
357            if (/^H[1-6]$/.test(n.tagName) && n.textContent.trim()) { return n.textContent.trim(); }
358            var hh = n.querySelector && n.querySelector('h1,h2,h3,h4,h5,h6');
359            if (hh && hh.textContent.trim()) { return hh.textContent.trim(); }
360        }
361        return '';
362    }
363    function enhanceVagueLinks() {
364        Array.prototype.forEach.call(document.querySelectorAll('a, button'), function (el) {
365            if (el.getAttribute('aria-label')) { return; }
366            var txt = (el.textContent || '').replace(/\s+/g, ' ').trim();
367            if (!VAGUE.test(txt)) { return; }
368            var ctx = cardHeadingFor(el).replace(/\s+/g, ' ').slice(0, 60);
369            if (ctx) { el.setAttribute('aria-label', txt + ' about ' + ctx); }
370        });
371    }
372
373    /* ------------------------------------------------------------------------
374       BATCH C3 — Dialog/popup name + "has popup" hint
375       WCAG 4.1.2 Name, Role, Value (A)
376       ------------------------------------------------------------------------
377       The on-load popup has role="dialog" but no aria-modal and no accessible
378       name; dialog triggers don't announce that they open a popup. We add those.
379       (Full focus-trap for the custom Customer Care dialog is tracked separately
380       as C3b — it needs the dialog's open/close hook, which is in compiled JS.)
381       ------------------------------------------------------------------------ */
382    var idSeq = 0;
383    function ensureId(el) {
384        if (!el.id) { el.id = 'wcag-id-' + (++idSeq); }
385        return el.id;
386    }
387    function enhanceDialogs() {
388        Array.prototype.forEach.call(document.querySelectorAll('[role="dialog"], .modal'), function (dlg) {
389            if (!dlg.getAttribute('role')) { dlg.setAttribute('role', 'dialog'); }
390            if (!dlg.getAttribute('aria-modal')) { dlg.setAttribute('aria-modal', 'true'); }
391            if (!dlg.getAttribute('aria-label') && !dlg.getAttribute('aria-labelledby')) {
392                var h = dlg.querySelector('.modal-title, h1, h2, h3, h4');
393                if (h && h.textContent.trim()) {
394                    dlg.setAttribute('aria-labelledby', ensureId(h));
395                } else {
396                    dlg.setAttribute('aria-label', 'Information dialog');
397                }
398            }
399        });
400        // Triggers that open a dialog -> announce the popup behaviour. Scope to
401        // ACTUAL modal toggles only. (Do NOT use the broad [data-target^="#"]:
402        // carousel indicators/controls also use data-target/data-slide-to, and
403        // tagging them aria-haspopup="dialog" made screen readers wrongly say
404        // "opens dialog" on every slide dot — Home audit New row.)
405        Array.prototype.forEach.call(
406            document.querySelectorAll('[data-toggle="modal"], [data-bs-toggle="modal"]'),
407            function (t) {
408                if (!t.getAttribute('aria-haspopup')) { t.setAttribute('aria-haspopup', 'dialog'); }
409            }
410        );
411    }
412
413    /* ------------------------------------------------------------------------
414       BATCH C3b — Focus trap for the fancybox dialogs (Customer Care etc.)
415       WCAG 2.1.1 / 2.4.3 (Home Sr30)
416       ------------------------------------------------------------------------
417       The "Customer Care"/"Write to us"/"Branch Locator" links open a jQuery
418       fancybox popup, but keyboard focus stays on the page behind it. Library-
419       agnostic fix: watch for the fancybox container appearing, move focus into
420       it, trap Tab within it, close on Escape, and restore focus to the trigger
421       on close. No change to the site's fancybox init.
422       ------------------------------------------------------------------------ */
423    function focusablesIn(c) {
424        return Array.prototype.slice.call(c.querySelectorAll(
425            'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
426        )).filter(function (e) { return e.offsetParent !== null; });
427    }
428    // Intercept inline-content popup triggers (a.fancybox1/.various/.fancybox with
429    // href="#someId") and show them in a proper accessible dialog. This bypasses
430    // the site's fancybox (broken under jQuery 4) and adds a real focus trap for
431    // Customer Care / Branch Locator / Write to us (Home Sr30).
432    function enhanceInlineDialogs() {
433        var links = document.querySelectorAll(
434            'a.fancybox1[href^="#"], a.various[href^="#"], a.fancybox[href^="#"]'
435        );
436        Array.prototype.forEach.call(links, function (link) {
437            if (link.dataset.wcagInlineDlg) { return; }
438            var id = (link.getAttribute('href') || '').slice(1);
439            var target = id && document.getElementById(id);
440            if (!target) { return; }              // only inline-content dialogs
441            link.dataset.wcagInlineDlg = '1';
442            link.setAttribute('aria-haspopup', 'dialog');
443            // Give a vague "Read More" trigger a descriptive name from the person it
444            // opens (About Us Sr39). The member name lives in a .bod_name div (not a
445            // heading), and it's reliably present inside the dialog CONTENT (target),
446            // so read it from there. Set authoritatively (override any generic label
447            // enhanceVagueLinks may have derived from a section heading).
448            var txt = (link.textContent || '').replace(/\s+/g, ' ').trim();
449            if (/^(read more|more|view more|details)$/i.test(txt)) {
450                var nameEl = (link.closest('.bod_box') || target).querySelector('.bod_name')
451                    || target.querySelector('.modal-title, h1, h2, h3, h4, .title');
452                var nm = nameEl ? nameEl.textContent.replace(/\s+/g, ' ').trim() : '';
453                if (nm) { link.setAttribute('aria-label', txt + ' about ' + nm); }
454            }
455            // Capture phase + stopImmediatePropagation to beat fancybox's handler.
456            link.addEventListener('click', function (e) {
457                e.preventDefault();
458                e.stopImmediatePropagation();
459                openInlineDialog(target, link);
460            }, true);
461        });
462    }
463    function openInlineDialog(target, returnEl) {
464        var overlay = document.createElement('div');
465        overlay.className = 'wcag-modal-overlay';
466        var dialog = document.createElement('div');
467        dialog.className = 'wcag-modal';
468        dialog.setAttribute('role', 'dialog');
469        dialog.setAttribute('aria-modal', 'true');
470        var h = target.querySelector('h1,h2,h3,h4,h5,h6,.modal-title,.bod_name,.title');
471        dialog.setAttribute('aria-label', (h && h.textContent.replace(/\s+/g, ' ').trim()) || 'Dialog');
472        var close = document.createElement('button');
473        close.type = 'button';
474        close.className = 'wcag-modal-close';
475        close.setAttribute('aria-label', 'Close');
476        close.innerHTML = '&times;';
477        dialog.appendChild(close);
478        var content = target.cloneNode(true);   // clone so the original stays put
479        content.removeAttribute('id');
480        content.style.display = 'block';
481        dialog.appendChild(content);
482        overlay.appendChild(dialog);
483        document.body.appendChild(overlay);
484
485        function focusables() { return focusablesIn(dialog); }
486        try { (focusables()[0] || close).focus(); } catch (e) {}
487        function onKey(e) {
488            if (e.key === 'Escape') { e.preventDefault(); teardown(); return; }
489            if (e.key !== 'Tab') { return; }
490            var f = focusables();
491            if (!f.length) { e.preventDefault(); close.focus(); return; }
492            var first = f[0], last = f[f.length - 1];
493            if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
494            else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
495            else if (!dialog.contains(document.activeElement)) { e.preventDefault(); first.focus(); }
496        }
497        function teardown() {
498            document.removeEventListener('keydown', onKey, true);
499            if (overlay.parentNode) { overlay.parentNode.removeChild(overlay); }
500            if (returnEl && returnEl.focus) { try { returnEl.focus(); } catch (e) {} }
501        }
502        close.addEventListener('click', teardown);
503        overlay.addEventListener('mousedown', function (e) { if (e.target === overlay) { teardown(); } });
504        document.addEventListener('keydown', onKey, true);
505    }
506    function enhanceFancyboxDialogs() {
507        if (document.body.dataset.wcagFbxObs) { return; }
508        document.body.dataset.wcagFbxObs = '1';
509        var FBX = '.fancybox-wrap, .fancybox-inner, .fancybox-content, .fancybox__content, #fancybox-content';
510        var active = null, returnEl = null, keyHandler = null;
511        function openTrap(box) {
512            if (active === box) { return; }
513            active = box; returnEl = document.activeElement;
514            if (!box.getAttribute('role')) { box.setAttribute('role', 'dialog'); }
515            box.setAttribute('aria-modal', 'true');
516            if (!box.hasAttribute('tabindex')) { box.setAttribute('tabindex', '-1'); }
517            var f = focusablesIn(box);
518            try { (f[0] || box).focus(); } catch (e) {}
519            keyHandler = function (e) {
520                if (e.key === 'Tab') {
521                    var items = focusablesIn(box);
522                    if (!items.length) { e.preventDefault(); box.focus(); return; }
523                    var first = items[0], last = items[items.length - 1];
524                    if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
525                    else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
526                    else if (!box.contains(document.activeElement)) { e.preventDefault(); first.focus(); }
527                } else if (e.key === 'Escape') {
528                    var cb = document.querySelector('#fancybox-close, .fancybox-close, .fancybox-item-close, .fancybox-button--close, .fancybox-close-small');
529                    if (cb) { cb.click(); }
530                }
531            };
532            document.addEventListener('keydown', keyHandler, true);
533        }
534        function closeTrap() {
535            if (!active) { return; }
536            if (keyHandler) { document.removeEventListener('keydown', keyHandler, true); }
537            active = null; keyHandler = null;
538            if (returnEl && returnEl.focus) { try { returnEl.focus(); } catch (e) {} }
539            returnEl = null;
540        }
541        try {
542            new MutationObserver(function () {
543                var box = document.querySelector(FBX);
544                if (box && box.offsetParent !== null) { openTrap(box); }
545                else if (active) { closeTrap(); }
546            }).observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });
547        } catch (e) { /* */ }
548    }
549
550    /* ------------------------------------------------------------------------
551       BATCH D (safety net) — name any unlabeled form field from its placeholder
552       WCAG 3.3.2 / 4.1.2
553       ------------------------------------------------------------------------
554       The Get-in-Touch form now has real <label>s (in markup). This catches any
555       remaining placeholder-only fields on other forms (career, complaint, ...)
556       without editing each page: if a field has no programmatic name, use its
557       placeholder as an aria-label. Visible appearance is unchanged.
558       ------------------------------------------------------------------------ */
559    function labelUnlabeledFields() {
560        var SKIP = { hidden: 1, submit: 1, button: 1, reset: 1, image: 1 };
561        Array.prototype.forEach.call(document.querySelectorAll('input, textarea'), function (el) {
562            var type = (el.getAttribute('type') || 'text').toLowerCase();
563            if (SKIP[type]) { return; }
564            if (el.getAttribute('aria-label') || el.getAttribute('aria-labelledby') || el.getAttribute('title')) { return; }
565            if (el.id && document.querySelector('label[for="' + (window.CSS && CSS.escape ? CSS.escape(el.id) : el.id) + '"]')) { return; }
566            if (el.closest('label')) { return; }
567            var ph = (el.getAttribute('placeholder') || '').trim();
568            if (ph) { el.setAttribute('aria-label', ph); return; }
569            // Last resort: derive a readable name from the id/name attribute.
570            var raw = el.id || el.name || '';
571            if (raw) {
572                var lbl = raw
573                    .replace(/^Form[_-]?/i, '')
574                    .replace(/[_-]+/g, ' ')
575                    .replace(/([a-z])([A-Z])/g, '$1 $2')
576                    .replace(/(\D)(\d)/g, '$1 $2')
577                    .replace(/\s+/g, ' ')
578                    .trim();
579                if (lbl) { el.setAttribute('aria-label', titleCase(lbl)); }
580            }
581        });
582    }
583
584    /* ------------------------------------------------------------------------
585       BATCH R2-FORM — Form landmark, required state, error focus & announcement
586       WCAG 4.1.2 / 3.3.2 / 3.3.1 / 2.4.3 / 4.1.3 (Forms tab)
587       ------------------------------------------------------------------------
588       The site's forms (career, complaint, DND…) use a bare <form> with no
589       accessible name (so it isn't announced as a form landmark), no aria-required
590       on required fields, and error messages that aren't announced / don't take
591       focus. This adds all of that generically; per-form heading/label markup is
592       fixed in the templates.
593       ------------------------------------------------------------------------ */
594    function enhanceForms() {
595        Array.prototype.forEach.call(document.querySelectorAll('form'), function (form) {
596            if (form.dataset.wcagForm) { return; }
597            // Only real data-entry forms (skip the header search etc.).
598            if (!form.querySelector('input:not([type="hidden"]), select, textarea')) { return; }
599            if (form.closest('header, footer, #uw-main')) { return; }
600            form.dataset.wcagForm = '1';
601
602            // (1) Form landmark — give it an accessible name (4.1.2).
603            if (!form.getAttribute('aria-label') && !form.getAttribute('aria-labelledby') && !form.getAttribute('title') && form.getAttribute('role') !== 'form') {
604                var name = '';
605                var hIn = form.querySelector('h1, h2, h3, [role="heading"]');
606                if (hIn && hIn.textContent.trim()) { name = hIn.textContent.trim(); }
607                if (!name) {
608                    var prev = form.previousElementSibling;
609                    for (var i = 0; i < 5 && prev; i++) {
610                        var ph = (prev.matches && prev.matches('h1,h2,h3')) ? prev : (prev.querySelector && prev.querySelector('h1,h2,h3'));
611                        if (ph && ph.textContent.trim()) { name = ph.textContent.trim(); break; }
612                        prev = prev.previousElementSibling;
613                    }
614                }
615                form.setAttribute('aria-label', (name ? name.replace(/\s+/g, ' ').slice(0, 60) + ' form' : 'Form'));
616            }
617
618            // (2) Programmatically associate stray <label>s (no `for`, not wrapping an
619            // input) with their field — the site's labels omit `for` (1.3.1/3.3.2).
620            var GRP = '.form-group, .field, .col-md-6, .col-md-12, .col-lg-6, .col-lg-4, li, p, td, div';
621            Array.prototype.forEach.call(form.querySelectorAll('label'), function (lab) {
622                if (lab.getAttribute('for') || lab.querySelector('input, select, textarea')) { return; }
623                var field = null, n = lab.nextElementSibling;
624                for (var i = 0; i < 4 && n; i++) {
625                    if (n.matches && n.matches('input:not([type="hidden"]), select, textarea')) { field = n; break; }
626                    var q = n.querySelector && n.querySelector('input:not([type="hidden"]), select, textarea');
627                    if (q) { field = q; break; }
628                    n = n.nextElementSibling;
629                }
630                if (!field) { var g = lab.closest(GRP); if (g) { field = g.querySelector('input:not([type="hidden"]), select, textarea'); } }
631                if (field) {
632                    if (!field.id) { field.id = 'wcag-fld-' + (++idSeq); }
633                    lab.setAttribute('for', field.id);
634                }
635            });
636
637            // (3) aria-required from `required`/jQuery-val/a visible asterisk (3.3.2).
638            var hasRequired = false;
639            Array.prototype.forEach.call(form.querySelectorAll('input, select, textarea'), function (f) {
640                var t = (f.getAttribute('type') || 'text').toLowerCase();
641                if (t === 'hidden' || t === 'submit' || t === 'button' || t === 'reset' || f.hasAttribute('aria-required')) { return; }
642                var req = f.required || f.hasAttribute('data-val-required') || f.getAttribute('aria-required') === 'true';
643                if (!req) {
644                    var lab = null;
645                    if (f.id) { lab = document.querySelector('label[for="' + (window.CSS && CSS.escape ? CSS.escape(f.id) : f.id) + '"]'); }
646                    var grp = f.closest(GRP);
647                    // asterisk inside the label OR a .required-star/.req marker in the group
648                    if (lab && /[*✱]/.test(lab.textContent)) { req = true; }
649                    else if (grp && (grp.querySelector('.required-star, .wcag-req, .req') || (grp.querySelector('label') && /[*✱]/.test(grp.querySelector('label').textContent)))) { req = true; }
650                }
651                if (req) { f.setAttribute('aria-required', 'true'); hasRequired = true; }
652            });
653
654            // (4) Inject a visible "* = required" instruction at the top if the form has
655            // required fields but no such note (3.3.2).
656            if (hasRequired && !form.querySelector('.wcag-form-required-note') && !/required field/i.test(form.textContent)) {
657                var note = document.createElement('p');
658                note.className = 'wcag-form-required-note';
659                note.innerHTML = 'Fields marked with an asterisk (<span aria-hidden="true">*</span>) are required.';
660                var af = form.querySelector('input, select, textarea');
661                var anchor = af ? (af.closest(GRP) || af) : form.firstElementChild;
662                form.insertBefore(note, form.firstChild);
663            }
664
665            // (5) Announce validation errors (4.1.3) — scope live regions to this form.
666            var summary = form.querySelector('.validation-summary-errors, [data-valmsg-summary="true"], [asp-validation-summary]');
667            if (summary && !summary.getAttribute('role')) { summary.setAttribute('role', 'alert'); summary.setAttribute('aria-live', 'assertive'); }
668            Array.prototype.forEach.call(
669                form.querySelectorAll('.field-validation-error, .field-validation-valid, span.text-danger, span[data-valmsg-for]'),
670                function (el) {
671                    if (!el.getAttribute('role') && !el.getAttribute('aria-live')) { el.setAttribute('aria-live', 'polite'); }
672                }
673            );
674
675            // (4) On submit, move focus to the first invalid field (3.3.1 / 2.4.3).
676            form.addEventListener('submit', function () {
677                setTimeout(function () {
678                    var bad = form.querySelector('.input-validation-error, [aria-invalid="true"], :invalid:not([type="hidden"]):not(fieldset)');
679                    if (bad && bad.focus) { try { bad.focus(); } catch (e) {} }
680                }, 60);
681            });
682        });
683    }
684
685    /* ------------------------------------------------------------------------
686       BATCH E — Decorative image alt + stray ticker headings
687       WCAG 1.1.1 Non-text Content (A) + 1.3.1 Info and Relationships (A)
688       ------------------------------------------------------------------------
689       - Images with NO alt that are clearly decorative (svg shapes/paths/icons,
690         spacers, logos) get alt="" (logo gets a real name). Content photos are
691         left alone — they need human-written alt (flagged, not faked).
692       - The news ticker's DB content wraps each item in <h6>; those aren't real
693         headings, so we strip the heading role (role="presentation").
694       ------------------------------------------------------------------------ */
695    var DECORATIVE = /shape|path|icon|decor|spacer|arrow|dots|pattern|bg[-_]|background|ellipse|circle|line\b/;
696    function enhanceImages() {
697        Array.prototype.forEach.call(document.querySelectorAll('img:not([alt])'), function (img) {
698            var src = (img.getAttribute('src') || '').toLowerCase();
699            if (/logo/.test(src)) { img.setAttribute('alt', 'Cosmos Co-op Bank'); return; }
700            // Treat decorative SVGs / icon-shapes / spacers as decorative.
701            if (/\.svg(\?|$)/.test(src) || DECORATIVE.test(src) || DECORATIVE.test((img.className || '').toLowerCase())) {
702                img.setAttribute('alt', '');
703                return;
704            }
705            // Content raster image: derive a name from its card heading or the
706            // link that wraps it; if there is no usable context it is almost
707            // certainly decorative, so use alt="" (never leave alt missing).
708            var ctx = cardHeadingFor(img).replace(/\s+/g, ' ').trim();
709            if (ctx) { img.setAttribute('alt', ctx.slice(0, 90)); return; }
710            var a = img.closest('a');
711            if (a) {
712                var t = (a.getAttribute('aria-label') || a.textContent || '').replace(/\s+/g, ' ').trim();
713                if (t && !VAGUE.test(t)) { img.setAttribute('alt', t.slice(0, 90)); return; }
714            }
715            img.setAttribute('alt', '');
716        });
717    }
718    /* Guarantee every page has an h1 (1.3.1 / 2.4.6). On inner pages the banner
719       title is already promoted to level 1; this is the fallback for pages the
720       banner fix doesn't cover (homepage, special layouts): inject a visually
721       hidden h1 built from the page title. */
722    function ensureH1() {
723        if (document.querySelector('h1, [role="heading"][aria-level="1"]')) { return; }
724        var main = document.querySelector('main#main-content, [role="main"]') || document.body;
725        var name = (document.title || '').replace(/\s*\|\s*Cosmos.*$/i, '').trim();
726        if (!name || /^home$/i.test(name)) { name = 'Cosmos Co-op Bank'; }
727        var h1 = document.createElement('h1');
728        h1.textContent = name;
729        h1.className = 'wcag-sr-only';
730        main.insertBefore(h1, main.firstChild);
731    }
732    /* Empty headings (e.g. the homepage carousel's <h3>&nbsp;</h3> spacers)
733       confuse screen readers. Demote any heading with no text/image to
734       presentation. WCAG 1.3.1. */
735    function demoteEmptyHeadings() {
736        Array.prototype.forEach.call(
737            document.querySelectorAll('h1, h2, h3, h4, h5, h6'),
738            function (h) {
739                var txt = (h.textContent || '').replace(/ /g, ' ').trim();
740                if (!txt && !h.querySelector('img, svg, [aria-label]')) {
741                    h.setAttribute('role', 'presentation');
742                }
743            }
744        );
745    }
746
747    function demoteStrayTickerHeadings() {
748        Array.prototype.forEach.call(
749            document.querySelectorAll('.wcag-ticker h1, .wcag-ticker h2, .wcag-ticker h3, .wcag-ticker h4, .wcag-ticker h5, .wcag-ticker h6'),
750            function (h) {
751                h.setAttribute('role', 'presentation');
752            }
753        );
754    }
755
756    /* Inner-page banner: (1) the breadcrumb is read AFTER the page heading even
757       though it sits visually above it -> wrong reading sequence (1.3.2). The
758       breadcrumb is position:absolute, so moving it earlier in the DOM fixes the
759       reading order with no visual change. (2) the banner title is the page's
760       primary heading but is marked up as <h3>; expose it to AT as level 1 so
761       every inner page has an h1 (1.3.1) without changing its visual size. */
762    function fixBannerHeadingsAndOrder() {
763        Array.prototype.forEach.call(document.querySelectorAll('.inner-pg-banner'), function (banner) {
764            var bc = banner.querySelector(':scope > .breadcrumbs-div');
765            var carousel = banner.querySelector(':scope > .carousel, :scope > .carousel.slide');
766            if (bc && carousel &&
767                (bc.compareDocumentPosition(carousel) & Node.DOCUMENT_POSITION_PRECEDING)) {
768                banner.insertBefore(bc, banner.firstChild);
769            }
770            // Promote the banner title to an h1 for assistive tech (visual size kept).
771            if (!document.querySelector('h1')) {
772                var title = banner.querySelector('.inner-pg-banner-text-wrap h1, .inner-pg-banner-text-wrap h2, .inner-pg-banner-text-wrap h3, .carousel-caption h1, .carousel-caption h2, .carousel-caption h3');
773                if (title && !title.getAttribute('role')) {
774                    title.setAttribute('role', 'heading');
775                    title.setAttribute('aria-level', '1');
776                }
777            }
778        });
779    }
780
781    /* ------------------------------------------------------------------------
782       BATCH F1 — Breadcrumb navigation landmark + current page (4.1.2)
783       BATCH F2 — Descriptive "Enquire Now about <topic>" links (2.4.4 / 4.1.2)
784       ------------------------------------------------------------------------ */
785    function enhanceBreadcrumbs() {
786        Array.prototype.forEach.call(document.querySelectorAll('.breadcrumbs-div, .breadcrumbs-inner-div'), function (bc) {
787            var ol = bc.querySelector('ol.breadcrumb, ul.breadcrumb, .breadcrumb');
788            if (!ol) { return; }
789            var landmark = ol.closest('nav, [role="navigation"]') || bc;
790            if (!landmark.getAttribute('role') && landmark.tagName !== 'NAV') {
791                landmark.setAttribute('role', 'navigation');
792            }
793            if (!landmark.getAttribute('aria-label')) {
794                landmark.setAttribute('aria-label', 'Breadcrumb');
795            }
796            var active = ol.querySelector('.active a, .breadcrumb-item.active a, li.active a');
797            if (active && !active.getAttribute('aria-current')) {
798                active.setAttribute('aria-current', 'page');
799            }
800        });
801    }
802    function enhanceEnquireLinks() {
803        var topicEl = document.querySelector('.inner-pg-banner-text-wrap h1, .inner-pg-banner-text-wrap [aria-level="1"], h1');
804        var topic = ((topicEl ? topicEl.textContent : '') || (document.title || '').replace(/\s*\|.*$/, '')).replace(/\s+/g, ' ').trim();
805        Array.prototype.forEach.call(document.querySelectorAll('a, button'), function (a) {
806            var txt = (a.textContent || '').replace(/\s+/g, ' ').trim();
807            if (/^enquire now$/i.test(txt) && !a.getAttribute('aria-label')) {
808                a.setAttribute('aria-label', topic ? 'Enquire Now about ' + topic : 'Enquire Now');
809            }
810        });
811    }
812
813    /* ------------------------------------------------------------------------
814       BATCH F4 — Accordion headers keyboard-operable
815       WCAG 2.1.1 Keyboard (A)
816       ------------------------------------------------------------------------
817       Accordions (.acc_head / smk-accordion) toggle on click only. Make the
818       header a real button: focusable, Enter/Space activates, aria-expanded
819       reflects state.
820       ------------------------------------------------------------------------ */
821    function isAccordionOpen(head) {
822        var sec = head.parentElement;
823        return (head.classList.contains('acc_active') ||
824                (sec && (sec.classList.contains('acc_active') || sec.classList.contains('open') || sec.classList.contains('active'))));
825    }
826    function enhanceAccordions() {
827        var heads = document.querySelectorAll('.acc_head, .accordion_header, .smk-accordion .head, [class*="accordion"] > .acc_head');
828        Array.prototype.forEach.call(heads, function (h) {
829            if (h.dataset.wcagAcc) { return; }
830            h.dataset.wcagAcc = '1';
831            if (!h.getAttribute('role')) { h.setAttribute('role', 'button'); }
832            if (!h.hasAttribute('tabindex')) { h.setAttribute('tabindex', '0'); }
833            h.setAttribute('aria-expanded', String(isAccordionOpen(h)));
834            h.addEventListener('keydown', function (e) {
835                if (e.key === 'Enter' || e.key === ' ' || e.key === 'Spacebar') {
836                    e.preventDefault();
837                    h.click();
838                }
839            });
840            h.addEventListener('click', function () {
841                setTimeout(function () { h.setAttribute('aria-expanded', String(isAccordionOpen(h))); }, 60);
842            });
843        });
844    }
845
846    /* ------------------------------------------------------------------------
847       BATCH G — Carousel keyboard + slide announcements
848       WCAG 2.1.1 Keyboard (A) + 4.1.3 Status Changes (AA) + 2.2.2 
848(A, focus pause)
849       ------------------------------------------------------------------------
850       - Bootstrap slide indicators (<li data-slide-to>) and Owl dots are not
851         focusable -> make them real buttons (Enter/Space activate).
852       - Announce "Slide X of N" via a hidden live region on slide change.
853       - Pause the auto-rotating banner while it (or its controls) has keyboard
854         focus, and resume on blur.
855       ------------------------------------------------------------------------ */
856    function makeKbdButton(el, label) {
857        if (el.getAttribute('role') !== 'button') { el.setAttribute('role', 'button'); }
858        if (!el.hasAttribute('tabindex')) { el.setAttribute('tabindex', '0'); }
859        if (label && !el.getAttribute('aria-label')) { el.setAttribute('aria-label', label); }
860        if (!el.dataset.wcagKbd) {
861            el.dataset.wcagKbd = '1';
862            el.addEventListener('keydown', function (e) {
863                if (e.key === 'Enter' || e.key === ' ' || e.key === 'Spacebar') {
864                    e.preventDefault();
865                    el.click();
866                }
867            });
868        }
869    }
870    function enhanceCarouselKeyboard() {
871        // Bootstrap indicators: roving-tabindex tablist (ONE tab stop, arrow keys
872        // move between slides) — avoids dumping 22 tab stops into the page.
873        // Indicator dots are decorative navigation, redundant with the prev/next
874        // controls. Previously we exposed them as role=tablist/tab, which made the
875        // screen reader announce "tab, slide 1" (R2 re-audit 4.1.2 New — flagged on
876        // ALL pages). Remove the carousel roles/labels and hide the dot group from
877        // assistive tech; keyboard users navigate via the labelled prev/next controls.
878        Array.prototype.forEach.call(document.querySelectorAll('.carousel-indicators'), function (group) {
879            if (group.dataset.wcagIndHidden) { return; }
880            group.dataset.wcagIndHidden = '1';
881            group.setAttribute('aria-hidden', 'true');
882            group.removeAttribute('role');
883            group.removeAttribute('aria-label');
884            Array.prototype.forEach.call(group.querySelectorAll('li, [data-slide-to], [data-bs-slide-to]'), function (li) {
885                li.removeAttribute('role');
886                li.removeAttribute('aria-label');
887                li.removeAttribute('aria-selected');
888                li.removeAttribute('tabindex');
889            });
890        });
891        // Owl dots: few, simple buttons are fine.
892        Array.prototype.forEach.call(document.querySelectorAll('.owl-page, .owl-dot'), function (d, i) {
893            if (d.offsetParent !== null) { makeKbdButton(d, 'Go to item ' + (i + 1)); }
894        });
895    }
896    // Wire a Bootstrap carousel for manual-only slide announcements + focus pause.
897    // Used for BOTH the banner (#themeSlider) and the notice dialog (#popupCarousel,
898    // Home Sr74 — announce the slide when the user hits prev/next).
899    function wireCarouselAnnounce(ts) {
900        if (!ts || ts.dataset.wcagLive) { return; }
901        ts.dataset.wcagLive = '1';
902        var slides = ts.querySelectorAll('.item, .carousel-item');
903        if (!slides.length) { return; }
904        var live = document.createElement('div');
905        live.className = 'wcag-sr-only';
906        live.setAttribute('aria-live', 'polite');
907        live.setAttribute('aria-atomic', 'true');
908        ts.appendChild(live);
909        // Announce ONLY when the user manually navigates (prev/next/indicator/
910        // keyboard). Auto-rotation must NOT announce, or the screen reader keeps
911        // interrupting with "Slide 14 of 22, Slide 15 of 22..." while the user is
912        // reading elsewhere (Home audit 4.1.3 New row). A manualNav flag is set on
913        // real interaction and consumed by the slide-change handler.
914        var manualNav = false, manualTimer = null;
915        function flagManual() {
916            manualNav = true;
917            if (manualTimer) { clearTimeout(manualTimer); }
918            manualTimer = setTimeout(function () { manualNav = false; }, 1500);
919        }
920        Array.prototype.forEach.call(
921            ts.querySelectorAll('.carousel-control, .carousel-control-prev, .carousel-control-next, .carousel-indicators [data-sli
921de-to], .carousel-indicators [data-bs-slide-to]'),
922            function (c) {
923                c.addEventListener('click', flagManual);
924                c.addEventListener('keydown', function (e) {
925                    if (e.key === 'Enter' || e.key === ' ' || e.key === 'Spacebar' || e.key.indexOf('Arrow') === 0) { flagManual(); }
926                });
927            }
928        );
929        function announce() {
930            if (!manualNav) { return; }           // suppress auto-rotation chatter
931            manualNav = false;
932            var a = ts.querySelector('.item.active, .carousel-item.active');
933            var idx = Array.prototype.indexOf.call(slides, a) + 1;
934            if (idx <= 0) { return; }
935            // Prefer the active slide's own description (image alt / heading) so the
936            // dialog announces the slide's content, not just a number (Sr74).
937            var img = a.querySelector('img[alt]'), h = a.querySelector('h1,h2,h3,h4,.title');
938            var label = (img && img.getAttribute('alt').trim()) || (h && h.textContent.trim()) || '';
939            live.textContent = (label ? label + ', ' : '') + 'slide ' + idx + ' of ' + slides.length;
940        }
941        if (window.jQuery) {
942            try { window.jQuery(ts).on('slid.bs.carousel', announce); } catch (e) { /* */ }
943        }
944        // Pause auto-rotation while keyboard focus is inside the carousel (2.2.2)
945        // so it can't announce/rotate under the user. Resume when focus leaves.
946        function pause() { if (window.jQuery) { try { window.jQuery(ts).carousel('pause'); } catch (e) {} } }
947        function resume() { if (!ts.contains(document.activeElement) && window.jQuery) { try { window.jQuery(ts).carousel('cycle'); } catch (e) {} } }
948        ts.addEventListener('focusin', pause);
949        ts.addEventListener('focusout', resume);
950    }
951    function enhanceBannerCarousel() {
952        wireCarouselAnnounce(document.getElementById('themeSlider'));
953        wireCarouselAnnounce(document.getElementById('popupCarousel'));
954    }
955
956    /* ------------------------------------------------------------------------
957       BATCH H — Promote section-label divs to real headings
958       WCAG 1.3.1 Info and Relationships (A)
959       ------------------------------------------------------------------------
960       Several section labels are styled <div>s, not headings ("Ideal for
961       people", "Good to Know", Digital World items, product feature titles).
962       Expose them to assistive tech as the right heading level (no visual
963       change). Levels chosen to sit under the page h1 / section h2.
964       ------------------------------------------------------------------------ */
965    function promoteSectionHeadings() {
966        var MAP = [
967            // "Ideal for people" / "Good to Know" are top-level right-column section
968            // headings under the product h1 — the auditor wants level 2 (product pages).
969            ['.right-box-base-heading', '2'],
970            ['.dw-heading', '3'],
971            ['.bluband-products-details-heading', '3'],
972            ['.blueband-products-details-heading', '3']
973        ];
974        MAP.forEach(function (m) {
975            Array.prototype.forEach.call(document.querySelectorAll(m[0]), function (el) {
976                if (!el.getAttribute('role') && (el.textContent || '').trim()) {
977                    el.setAttribute('role', 'heading');
978                    el.setAttribute('aria-level', m[1]);
979                }
980            });
981        });
982    }
983
984    /* ------------------------------------------------------------------------
985       Single-item lists -> remove list semantics (WCAG 1.3.1)
986       ------------------------------------------------------------------------
987       Auditors flag a <ul>/<ol> that wraps exactly ONE <li> as "unnecessary list
988       markup" (e.g. product "Ideal for people"/"Good to Know" with one entry).
989       role="presentation" strips the "list, 1 item" announcement while keeping the
990       content readable. Scoped to MAIN content, skipping functional lists (nav,
991       breadcrumb, carousel indicators, the social list, and anything already given
992       a role such as a tablist/menu).
993       ------------------------------------------------------------------------ */
994    // Remove <br> that sit as DIRECT children of a <ul>/<ol> — invalid markup an
995    // auditor flags as "list has children which are not allowed: br" (e.g. the
996    // Nodal Office address list). Harmless to strip; keeps the list valid.
997    function removeStrayListBreaks() {
998        var root = document.getElementById('main-content') || document.body;
999        Array.prototype.forEach.call(root.querySelectorAll('ul, ol'), function (list) {
1000            Array.prototype.slice.call(list.children).forEach(function (c) {
1001                if (c.tagName === 'BR') { list.removeChild(c); }
1002            });
1003        });
1004    }
1005    function demoteSingleItemLists() {
1006        var root = document.getElementById('main-content') || document.body;
1007        Array.prototype.forEach.call(root.querySelectorAll('ul, ol'), function (list) {
1008            if (list.getAttribute('role')) { return; }
1009            if (list.classList.contains('wcag-social-list')) { return; }
1010            if (list.closest('nav, [role="navigation"], .breadcrumb, .carousel-indicators, .nav-tabs, [role="tablist"], [role="menu"]')) { return; }
1011            var items = Array.prototype.filter.call(list.children, function (c) { return c.tagName === 'LI'; });
1012            if (items.length === 1) {
1013                list.setAttribute('role', 'presentation');
1014                items[0].setAttribute('role', 'presentation');
1015            }
1016        });
1017    }
1018
1019    /* ------------------------------------------------------------------------
1020       BATCH R2-S — Service page heading levels (Services tab, 1.3.1)
1021       ------------------------------------------------------------------------
1022       The service body is DB-authored HTML (@Html.Raw(Model.Description)) with
1023       CMS classes, so these heading-level bugs can't be fixed in the template —
1024       but we can correct the levels the screen reader announces at runtime:
1025         - the banner service title is already the single h1 (fixBannerHeadingsAndOrder);
1026         - "Our Services"/"Service Rules" are authored as <h1 class="section-title-h-one">
1027           — demote to level 2 (only the page title may be h1);
1028         - the service tagline ("Forex made easy for you!") is an <h3> inside
1029           .pro-det-sub-title — the auditor wants it at level 2, directly under h1.
1030       Scoped to these exact CMS classes, so pages without them are untouched.
1031       ------------------------------------------------------------------------ */
1032    function fixServiceContentHeadings() {
1033        var banner = document.querySelector('.inner-pg-banner');
1034        if (!banner) { return; }
1035        // "Our Services"/"Service Rules" are DB-authored as real <h1> tags. Because
1036        // they are h1 TAGS, ensureH1()/fixBannerHeadingsAndOrder() think the page
1037        // already has an h1 and skip promoting the banner title — which would leave
1038        // NO level-1 heading once we demote them. So ONLY when such content <h1>s
1039        // exist, promote the banner service title to level 1 and demote the content
1040        // h1s to level 2 (exactly one h1 = the page title). Scoped this way so other
1041        // page types, where fixBannerHeadingsAndOrder already handles the title, are
1042        // left untouched.
1043        var contentH1s = document.querySelectorAll('.section-title-h-one');
1044        if (contentH1s.length) {
1045            var bannerTitle = banner.querySelector('.inner-pg-banner-text-wrap h1, .inner-pg-banner-text-wrap h2, .inner-pg-banner-text-wrap h3, .carousel-caption h3, .carousel-caption h2');
1046            if (bannerTitle) {
1047                bannerTitle.setAttribute('role', 'heading');
1048                bannerTitle.setAttribute('aria-level', '1');
1049            }
1050            Array.prototype.forEach.call(contentH1s, function (el) {
1051                if ((el.textContent || '').trim()) {
1052                    el.setAttribute('role', 'heading');
1053                    el.setAttribute('aria-level', '2');
1054                }
1055            });
1056        }
1057        Array.prototype.forEach.call(
1058            document.querySelectorAll('.pro-det-sub-title h1, .pro-det-sub-title h2, .pro-det-sub-title h3, .pro-det-sub-title h4, .pro-det-sub-title h5, .pro-det-sub-title h6'),
1059            function (el) {
1060                el.setAttribute('role', 'heading');
1061                el.setAttribute('aria-level', '2');
1062            }
1063        );
1064    }
1065
1066    function init() {
1067        try {
1068            promoteSectionHeadings();
1069        } catch (e) { /* never break the page */ }
1070        try {
1071            fixServiceContentHeadings();
1072        } catch (e) { /* never break the page */ }
1073        try {
1074            removeStrayListBreaks();
1075        } catch (e) { /* never break the page */ }
1076        try {
1077            demoteSingleItemLists();
1078        } catch (e) { /* never break the page */ }
1079        try {
1080            enhanceCarouselKeyboard();
1081            enhanceBannerCarousel();
1082            setTimeout(enhanceCarouselKeyboard, 800);
1083        } catch (e) { /* never break the page */ }
1084        try {
1085            enhanceAccordions();
1086        } catch (e) { /* never break the page */ }
1087        try {
1088            enhanceBreadcrumbs();
1089        } catch (e) { /* never break the page */ }
1090        try {
1091            enhanceEnquireLinks();
1092        } catch (e) { /* never break the page */ }
1093        try {
1094            enhanceImages();
1095        } catch (e) {
1096            /* never let remediation throw and break the page */
1097        }
1098        try {
1099            demoteStrayTickerHeadings();
1100        } catch (e) {
1101            /* never let remediation throw and break the page */
1102        }
1103        try {
1104            demoteEmptyHeadings();
1105        } catch (e) {
1106            /* never let remediation throw and break the page */
1107        }
1108        try {
1109            fixBannerHeadingsAndOrder();
1110        } catch (e) {
1111            /* never let remediation throw and break the page */
1112        }
1113        try {
1114            ensureH1();
1115        } catch (e) {
1116            /* never let remediation throw and break the page */
1117        }
1118        try {
1119            enhanceDialogs();
1120        } catch (e) {
1121            /* never let remediation throw and break the page */
1122        }
1123        try {
1124            enhanceInlineDialogs();
1125        } catch (e) {
1126            /* never let remediation throw and break the page */
1127        }
1128        try {
1129            enhanceFancyboxDialogs();
1130        } catch (e) {
1131            /* never let remediation throw and break the page */
1132        }
1133        try {
1134            enhanceForms();
1135        } catch (e) {
1136            /* never let remediation throw and break the page */
1137        }
1138        try {
1139            labelUnlabeledFields();
1140        } catch (e) {
1141            /* never let remediation throw and break the page */
1142        }
1143        try {
1144            enhanceExternalLinks();
1145        } catch (e) {
1146            /* never let remediation throw and break the page */
1147        }
1148        try {
1149            initTickerControls();
1150            setTickerSpeed();
1151            // Re-measure after late layout (fonts/images) and on resize.
1152            setTimeout(setTickerSpeed, 1200);
1153            if (window.addEventListener) {
1154                var tkT;
1155                window.addEventListener('resize', function () {
1156                    clearTimeout(tkT);
1157                    tkT = setTimeout(setTickerSpeed, 250);
1158                });
1159            }
1160        } catch (e) {
1161            /* never let remediation throw and break the page */
1162        }
1163        try {
1164            initSelectLabels();
1165        } catch (e) {
1166            /* never let remediation throw and break the page */
1167        }
1168        try {
1169            enhanceCarousels();
1170            // Owl builds nav after its own init; re-apply shortly after.
1171            setTimeout(enhanceCarousels, 600);
1172            setTimeout(enhanceCarousels, 1800);
1173        } catch (e) {
1174            /* never let remediation throw and break the page */
1175        }
1176        try {
1177            enhanceContentTabs();
1178        } catch (e) {
1179            /* never let remediation throw and break the page */
1180        }
1181        try {
1182            enhanceVagueLinks();
1183            setTimeout(enhanceVagueLinks, 1800);
1184        } catch (e) {
1185            /* never let remediation throw and break the page */
1186        }
1187        try {
1188            enhanceHeaderMenuTabs();
1189        } catch (e) { /* never break the page */ }
1190        try {
1191            initAccessibilityWidget();
1192            // Accessibility.js builds/updates the widget after its own init.
1193            setTimeout(initAccessibilityWidget, 800);
1194        } catch (e) { /* never break the page */ }
1195        try {
1196            enhanceMenuDialog();
1197        } catch (e) { /* never break the page */ }
1198    }
1199
1200    /* ========================================================================
1201       BATCH R2-H — Header mega-menu tabs: full WAI-ARIA tabs keyboard pattern
1202       WCAG 1.3.1, 4.1.2, 2.1.1 (Header-Footer Sr9/Sr11 + New tab-state rows)
1203       ------------------------------------------------------------------------
1204       The markup already carries role="tablist" / role="tab" (on the <a>) /
1205       role="tabpanel", and the <li> now have role="presentation". This adds the
1206       missing behaviour: a single tab stop (roving tabindex), arrow-key
1207       navigation, aria-selected kept in sync, and strips any aria-expanded that
1208       Bootstrap's tab plugin leaves on the tabs (not valid in the Tabs pattern).
1209       ======================================================================== */
1210    function enhanceHeaderMenuTabs() {
1211        var tablist = document.getElementById('nav-tab');
1212        if (!tablist || tablist.dataset.wcagTabs) return;
1213        var tabs = Array.prototype.slice.call(tablist.querySelectorAll('[role="tab"]'));
1214        if (!tabs.length) return;
1215        tablist.dataset.wcagTabs = '1';
1216
1217        // Each tab panel wraps its product links in a <nav>; unlabeled, they become
1218        // several anonymous "navigation" landmarks a screen reader can't tell apart
1219        // (Home audit New row). Name each from its tab so they are distinct.
1220        Array.prototype.forEach.call(document.querySelectorAll('#nav-tabContent [role="tabpanel"]'), function (panel) {
1221            var nav = panel.querySelector('nav');
1222            if (nav && !nav.getAttribute('aria-label')) {
1223                var lbId = panel.getAttribute('aria-labelledby');
1224                var tab = lbId && document.getElementById(lbId);
1225                var nm = tab ? tab.textContent.replace(/\s+/g, ' ').trim() : '';
1226                nav.setAttribute('aria-label', nm ? nm + ' products' : 'Products');
1227            }
1228        });
1229
1230        function selected(tab) { return tab.getAttribute('aria-selected') === 'true'; }
1231        function syncRoving() {
1232            tabs.forEach(function (t) {
1233                // aria-expanded is not part of the Tabs pattern — remove it.
1234                t.removeAttribute('aria-expanded');
1235                t.setAttribute('tabindex', selected(t) ? '0' : '-1');
1236            });
1237        }
1238        function activate(tab, focus) {
1239            tabs.forEach(function (t) { t.setAttribute('aria-selected', t === tab ? 'true' : 'false'); });
1240            syncRoving();
1241            if (focus) tab.focus();
1242            // Let the site's Bootstrap tab plugin actually switch the panel.
1243            if (window.jQuery) { try { window.jQuery(tab).tab('show'); } catch (e) {} }
1244        }
1245        syncRoving();
1246        // Keep aria-selected correct if the panel is switched by mouse/Bootstrap.
1247        tabs.forEach(function (tab) {
1248            tab.addEventListener('click', function () { setTimeout(syncRoving, 0); });
1249            tab.addEventListener('keydown', function (e) {
1250                var i = tabs.indexOf(tab), n = tabs.length, next = null;
1251                switch (e.key) {
1252                    case 'ArrowRight': case 'ArrowDown': next = tabs[(i + 1) % n]; break;
1253                    case 'ArrowLeft':  case 'ArrowUp':   next = tabs[(i - 1 + n) % n]; break;
1254                    case 'Home': next = tabs[0]; break;
1255                    case 'End':  next = tabs[n - 1]; break;
1256                    default: return;
1257                }
1258                e.preventDefault();
1259                activate(next, true);
1260            });
1261        });
1262    }
1263
1264    /* ========================================================================
1265       BATCH R2-H — Accessibility Options widget semantics
1266       WCAG 4.1.2 / 2.1.1 (Header-Footer Sr35/Sr38/Sr77/Sr78/Sr79/Sr80 + New)
1267       ------------------------------------------------------------------------
1268       Accessibility.js (re-enabled) provides the widget FUNCTIONALITY. This
1269       layer fixes the SEMANTICS the auditor flagged without touching that logic:
1270       (1) each feature button's accessible name is set from its visible label
1271           (they all wrongly shared aria-label="Enable the UserWay screen reader");
1272       (2) aria-pressed is mirrored from the widget's own "tick"/active state via a
1273           MutationObserver, so screen readers hear the true on/off state;
1274       (3) the div-based trigger and close controls get keyboard activation
1275           (Enter/Space) — they already have role="button"/tabindex in markup.
1276       ======================================================================== */
1277    function initAccessibilityWidget() {
1278        var widget = document.getElementById('uw-main');
1279        if (!widget) return;
1280
1281        // (1)+(2) Feature buttons: correct name + live pressed state.
1282        var btns = widget.querySelectorAll('.uwaw-features__item__i');
1283        Array.prototype.forEach.call(btns, function (btn) {
1284            var nameEl = btn.querySelector('.uwaw-features__item__name');
1285            var name = nameEl ? nameEl.textContent.replace(/\s+/g, ' ').trim() : '';
1286            if (name) { btn.setAttribute('aria-label', name); btn.removeAttribute('data-uw-reader-content'); }
1287            var tick = btn.querySelector('.tick-active, .uwaw-features__item__enabled');
1288            function syncPressed() {
1289                var on = false;
1290                if (tick) { var d = window.getComputedStyle(tick).display; on = d !== 'none'; }
1291                if (btn.classList.contains('active') || btn.parentElement.classList.contains('active')) on = true;
1292                btn.setAttribute('aria-pressed', on ? 'true' : 'false');
1293            }
1294            if (!btn.hasAttribute('aria-pressed')) btn.setAttribute('aria-pressed', 'false');
1295            syncPressed();
1296            if (tick && !btn.dataset.wcagPressObs) {
1297                btn.dataset.wcagPressObs = '1';
1298                try {
1299                    new MutationObserver(syncPressed).observe(tick, { attributes: true, attributeFilter: ['style', 'class'] });
1300                } catch (e) {}
1301                btn.addEventListener('click', function () { setTimeout(syncPressed, 60); });
1302            }
1303        });
1304
1305        // (3) Keyboard activation for the div-based trigger + close controls.
1306        function keyActivate(el) {
1307            if (!el || el.dataset.wcagKey) return;
1308            el.dataset.wcagKey = '1';
1309            el.addEventListener('keydown', function (e) {
1310                if (e.key === 'Enter' || e.key === ' ' || e.key === 'Spacebar') {
1311                    e.preventDefault(); el.click();
1312                }
1313            });
1314        }
1315        keyActivate(document.getElementById('uw-widget-custom-trigger'));
1316        keyActivate(document.getElementById('uw-widget-custom-trigger2'));
1317
1318        // (4) Dialog focus management (R3 re-audit Sr19 SC 1.3.2 + Sr49 SC 2.4.3, New/Critical):
1319        // when the panel opens, move focus INTO it (the Close button); trap Tab within
1320        // it while open; Esc closes and returns focus to the trigger. The panel
1321        // (#uw-main) already carries role=dialog / aria-modal / aria-label in markup
1322        // and is display:none when closed (so it is out of the AT tree until opened).
1323        var uwTrigger = document.getElementById('uw-widget-custom-trigger');
1324        var uwClose = document.getElementById('uw-widget-custom-trigger2');
1325        function uwOpen() { return window.getComputedStyle(widget).display !== 'none'; }
1326        function uwFocusables() {
1327            return Array.prototype.filter.call(
1328                widget.querySelectorAll('a[href], button:not([disabled]), input:not([disabled]), [role="button"], [tabindex]:not
1328([tabindex="-1"])'),
1329                function (el) { return el.offsetParent !== null; }
1330            );
1331        }
1332        if (!widget.dataset.wcagDlgFocus) {
1333            widget.dataset.wcagDlgFocus = '1';
1334            try {
1335                new MutationObserver(function () {
1336                    if (uwOpen() && !widget.dataset.wcagWasOpen) {
1337                        widget.dataset.wcagWasOpen = '1';
1338                        setTimeout(function () { if (uwClose && uwOpen()) uwClose.focus(); }, 60);
1339                    } else if (!uwOpen() && widget.dataset.wcagWasOpen) {
1340                        widget.dataset.wcagWasOpen = '';
1341                        if (uwTrigger) uwTrigger.focus();
1342                    }
1343                }).observe(widget, { attributes: true, attributeFilter: ['style', 'class'] });
1344            } catch (e) {}
1345            widget.addEventListener('keydown', function (e) {
1346                if (e.key === 'Escape') {
1347                    if (uwClose) uwClose.click();
1348                    if (uwTrigger) uwTrigger.focus();
1349                    return;
1350                }
1351                if (e.key !== 'Tab') return;
1352                var f = uwFocusables();
1353                if (!f.length) return;
1354                var first = f[0], last = f[f.length - 1];
1355                if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
1356                else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
1357            });
1358        }
1359    }
1360
1361    /* ========================================================================
1362       BATCH R2-H — "Menu" (Internet Banking) dialog + search: keyboard + state
1363       WCAG 2.1.1 / 4.1.2 / 1.3.2 (Header-Footer Sr36/Sr37/Sr46/Sr74/Sr75)
1364       ------------------------------------------------------------------------
1365       The site's onlineloans.js/site-footer.js bind CLICK to the inner <i> of the
1366       hamburger and close controls, so keyboard users can't open/close the panel.
1367       We forward Enter/Space to the existing handlers (no logic duplicated),
1368       keep aria-expanded in sync, and gate the off-screen panel from assistive
1369       tech with aria-hidden until it is opened.
1370       ======================================================================== */
1371    function enhanceMenuDialog() {
1372        var panel = document.querySelector('.right-side-slider-details-div');
1373        if (!panel) return;
1374        var closeBtn = document.querySelector('.close_btn-div');
1375        // There are MULTIPLE .nav-icon-wrap hamburger instances (responsive clones);
1376        // only one is visible at a given width. The old code wired only the FIRST
1377        // (which is display:none), so the VISIBLE hamburger had no keyboard handler
1378        // (R2 re-audit Sr38, SC 2.1.1). Wire EVERY instance. The site binds click to
1379        // the inner <i>, so we forward Enter/Space to that (verified: opens panel).
1380        var triggers = document.querySelectorAll('.nav-icon-wrap a');
1381        var lastTrigger = null;
1382        function isOpen() { return panel.classList.contains('right-side-slider-details-div-show'); }
1383        function reflect() {
1384            panel.setAttribute('aria-hidden', isOpen() ? 'false' : 'true');
1385            Array.prototype.forEach.call(triggers, function (t) {
1386                t.setAttribute('aria-expanded', isOpen() ? 'true' : 'false');
1387            });
1388        }
1389        if (panel.dataset.wcagDialog) { return; }
1390        panel.dataset.wcagDialog = '1';
1391        Array.prototype.forEach.call(triggers, function (trigger) {
1392            var innerIcon = trigger.querySelector('i');
1393            trigger.addEventListener('keydown', function (e) {
1394                if (e.key === 'Enter' || e.key === ' ' || e.key === 'Spacebar') {
1395                    e.preventDefault();
1396                    lastTrigger = trigger;
1397                    if (innerIcon) { innerIcon.click(); } else { trigger.click(); }
1398                    setTimeout(function () { reflect(); if (isOpen() && closeBtn) closeBtn.focus(); }, 40);
1399                }
1400            });
1401            trigger.addEventListener('click', function () {
1402                lastTrigger = trigger;
1403                setTimeout(function () { reflect(); if (isOpen() && closeBtn) closeBtn.focus(); }, 40);
1404            });
1405        });
1406        if (closeBtn) {
1407            closeBtn.addEventListener('keydown', function (e) {
1408                if (e.key === 'Enter' || e.key === ' ' || e.key === 'Spacebar') {
1409                    e.preventDefault();
1410                    var ci = closeBtn.querySelector('i'); if (ci) { ci.click(); } else { closeBtn.click(); }
1411                }
1412            });
1413            closeBtn.addEventListener('click', function () {
1414                setTimeout(function () { reflect(); if (!isOpen() && lastTrigger) lastTrigger.focus(); }, 40);
1415            });
1416        }
1417        // Escape closes the dialog for keyboard users and restores focus.
1418        panel.addEventListener('keydown', function (e) {
1419            if (e.key === 'Escape' && isOpen()) {
1420                var ci = closeBtn && closeBtn.querySelector('i'); if (ci) { ci.click(); } else if (closeBtn) { closeBtn.click(); }
1421                if (lastTrigger) lastTrigger.focus();
1422            }
1423        });
1424        reflect();
1425    }
1426
1427    if (document.readyState === 'loading') {
1428        document.addEventListener('DOMContentLoaded', init);
1429    } else {
1430        init();
1431    }
1432})();

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.