PageSourceSearch

https://brandefense.io/wp-content/themes/brandefense/assets/js/custom.js?ver=1790532067

js brandefense.io collected 2026-10-01 11:53:37 UTC 125,907 bytes, 3,474 lines download raw bytes

1/**
2 * OUTROLABS Custom JavaScript
3 */
4
5(function($) {
6    'use strict';
7
8    // Lenis Smooth Scroll
9    const lenis = new Lenis({
10        duration: 1.2,
11        easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
12        smoothWheel: true,
13    });
14    function raf(time) {
15        lenis.raf(time);
16        requestAnimationFrame(raf);
17    }
18    requestAnimationFrame(raf);
19
20    // Document ready
21    $(document).ready(function() {
22
23        // Email validation + Request A Demo / Subscribe
24        $('.btn-email-submit').on('click', function(e) {
25            var $btn = $(this);
26            var $input = $btn.closest('.relative').find('.email-input');
27            // Dedicated error slot placed by the form template, right below
28            // the input + button box.
29            var $slot = $btn.parents().has('.email-error-slot').first().find('.email-error-slot').first();
30            var email = $.trim($input.val());
31            var emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
32
33            // Consent checkbox is only enforced for the footer forms.
34            var $footer = $btn.closest('footer');
35            var $consent = $footer.find('.custom-checkbox').first();
36
37            function showEmailError(msg) {
38                $slot.html('<p class="email-error text-red-500 text-12 font-manrope text-center lg:text-left">' + msg + '</p>');
39            }
40
41            // Remove previous errors
42            $input.removeClass('border-red-500');
43            $slot.empty();
44            $footer.find('.checkbox-error').remove();
45
46            if (!email) {
47                $input.addClass('border-red-500');
48                showEmailError('Please enter your email address.');
49                return;
50            }
51
52            if (!emailRegex.test(email)) {
53                $input.addClass('border-red-500');
54                showEmailError('Please enter a valid email address.');
55                return;
56            }
57
58            // Consent checkbox must be ticked before continuing
59            if ($consent.length && !$consent.is(':checked')) {
60                $consent.closest('label').after('<p class="checkbox-error text-red-500 text-12 font-manrope mt-1">Please check the box to continue.</p>');
61                return;
62            }
63
64            // Valid email — redirect for demo buttons
65            if ($btn.hasClass('btn-request-demo')) {
66                window.location.href = '/request-a-demo/?email=' + encodeURIComponent(email);
67            }
68        });
69
70        // Clear errors on input
71        $('.email-input').on('input', function() {
72            $(this).removeClass('border-red-500');
73            $(this).parents().has('.email-error-slot').first().find('.email-error-slot').first().empty();
74        });
75
76        // Clear consent error once the box is ticked
77        $('.custom-checkbox').on('change', function() {
78            $(this).closest('label').next('.checkbox-error').remove();
79        });
80
81        // Initialize Diagram Canvas (Pixi.js) - only on front page
82        // Wrapped in try-catch to prevent blocking other initializations
83        // Pixi.js (~170 KB gz) is fetched only when the diagram section nears the viewport (BD-08)
84        var diagramContainer = document.getElementById('diagram-canvas-container');
85        if (typeof brandefenseData !== 'undefined' && brandefenseData.isFrontPage && diagramContainer && 'IntersectionObserver' in window) {
86            var diagramObserver = new IntersectionObserver(function(entries) {
87                if (!entries[0].isIntersecting) return;
88                diagramObserver.disconnect();
89                var pixiScript = document.createElement('script');
90                pixiScript.src = brandefenseData.pixiUrl;
91                pixiScript.onload = function() {
92                    initDiagramCanvas().catch(function(error) {
93                        console.warn('Diagram Canvas initialization failed:', error);
94                    });
95                };
96                document.head.appendChild(pixiScript);
97            }, { rootMargin: '600px 0px' });
98            diagramObserver.observe(diagramContainer);
99        }
100
101        // Home Tab System with GSAP Animation
102        initHomeTabSystem();
103
104        // Initialize stat cards mouse tracking gradient
105        initStatCardsGradient();
106
107
108        // Initialize footer toggle accordions
109        initFooterToggle();
110
111        // Initialize feature card hover animations
112        initFeatureCardHover();
113
114        // Initialize all Swipers
115        initReportsSwiper();
116        initReportsSwiperWidth();
117        initResourcesHeroSwiper();
118        initRightAlignedSwiper();
119        initRightAlignedSwiperWidth();
120        initFeatureCardsSwiper();
121        initSuccessStoriesSwiper();
122        initBlogSwiper();
123        initPipelineSwiper();
124        initChartsSwiper();
125        initProductRadarInfoSwiper();
126        initReviewsSwiper();
127        initSecurityLeadersSwiper();
128        initLifeSwiper();
129        initLifeSwiperWidth();
130
131        // Initialize Glossary System (only on glossary page)
132        if (typeof brandefenseData !== 'undefined' && brandefenseData.isGlossaryPage) {
133            initGlossarySystem();
134        }
135
136        // Initialize References Carousel
137        initReferencesCarousel();
138
139        // Initialize Partner Accordion
140        initAccordion();
141
142        // Initialize FAQ Accordion
143        initFaqAccordion();
144
145        // Initialize Partner Benefits Swiper
146        initPartnerBenefitsSwiper();
147
148        // Initialize About Purpose Text Swiper
149        initAboutPurposeSwiper();
150
151        // Initialize Customer Stories Swiper
152        initCustomerStoriesSwiper();
153
154        // Initialize Resources AJAX Filter
155        initResourcesFilter();
156
157        // Initialize Reports AJAX Filter
158        initReportsFilter();
159
160        // Initialize Blog Category AJAX Filter
161        initBlogFilter();
162
163        // Initialize Global Dropdowns
164        initDropdowns();
165
166        // Initialize Blog Detail Tabs
167        initBlogDetailTabs();
168
169        // Initialize Pillar Card Stack (Platform page)
170        initPillarStack();
171
172        // Initialize Pipeline Animation (Platform page)
173        initPipelineAnimation();
174
175        // Initialize Platform Chart Animations
176        initPlatformCharts();
177    });
178
179    /**
180     * Initialize Platform Chart Animations
181     */
182    function initPlatformCharts() {
183        var $grid = $('.platform-charts-grid');
184        var $mobileSwiper = $('.charts-swiper');
185        if ($grid.length === 0 && $mobileSwiper.length === 0) return;
186
187        var animatedDesktop = false;
188        var animatedMobile = false;
189
190        function animateCharts(container) {
191            var $container = $(container);
192
193            // Animate donut masks - stroke-dashoffset from 314 to 0 (sweeping reveal)
194            $container.find('.platform-chart-mask').each(function() {
195                gsap.to(this, {
196                    strokeDashoffset: 0,
197                    duration: 1.5,
198                    ease: 'power2.out'
199                });
200            });
201
202            // Animate stroke-based paths - stroke-dashoffset from 500 to 0
203            $container.find('.platform-chart-fill-stroke').each(function() {
204                gsap.to(this, {
205                    strokeDashoffset: 0,
206                    duration: 1.5,
207                    ease: 'power2.out'
208                });
209            });
210
211            // Animate endpoint dots and glow - opacity from 0 to 1 after stroke completes
212            $container.find('.platform-chart-fill-dot, .platform-chart-fill-dot-glow').each(function() {
213                gsap.to(this, {
214                    attr: { opacity: 1 },
215                    duration: 0.5,
216                    delay: 1.3,
217                    ease: 'power2.out'
218                });
219            });
220        }
221
222        // Desktop: trigger on scroll into view
223        if ($grid.length) {
224            var desktopObserver = new IntersectionObserver(function(entries) {
225                entries.forEach(function(entry) {
226                    if (entry.isIntersecting && !animatedDesktop) {
227                        animatedDesktop = true;
228                        animateCharts($grid[0]);
229                        desktopObserver.disconnect();
230                    }
231                });
232            }, { threshold: 0.3 });
233            desktopObserver.observe($grid[0]);
234        }
235
236        // Mobile: trigger on scroll into view
237        if ($mobileSwiper.length) {
238            var mobileObserver = new IntersectionObserver(function(entries) {
239                entries.forEach(function(entry) {
240                    if (entry.isIntersecting && !animatedMobile) {
241                        animatedMobile = true;
242                        animateCharts($mobileSwiper[0]);
243                        mobileObserver.disconnect();
244                    }
245                });
246            }, { threshold: 0.3 });
247            mobileObserver.observe($mobileSwiper[0]);
248        }
249    }
250
251    /**
252     * Initialize Pipeline Animation - Platform Page
253     */
254    function initPipelineAnimation() {
255        const gates = document.querySelectorAll('.pipeline-gate');
256        const payload = document.getElementById('pipeline-payload');
257
258        if (!payload || gates.length === 0) return;
259
260        const xPositions = [50, 130, 210, 290, 370, 450, 490];
261        let currentStep = 0;
262
263        function animate() {
264            gates.forEach(function(gate) {
265                gate.classList.remove('active');
266            });
267
268            if (currentStep >= xPositions.length) {
269                currentStep = 0;
270                payload.style.transition = 'none';
271                payload.style.transform = 'translateX(0px)';
272
273                setTimeout(function() {
274                    payload.style.transition = 'transform 0.4s ease-in-out';
275                    animate();
276                }, 100);
277                return;
278            }
279
280            var travelDistance = xPositions[currentStep] - 10;
281            payload.style.transform = 'translateX(' + travelDistance + 'px)';
282
283            if (currentStep < gates.length) {
284                gates[currentStep].classList.add('active');
285            }
286
287            currentStep++;
288            setTimeout(animate, 800);
289        }
290
291        setTimeout(animate, 500);
292    }
293
294    /**
295     * Initialize Pillar Card Stack - Platform Page
296     */
297    function initPillarStack() {
298        $('.pillar-stack').each(function() {
299            var $stack = $(this);
300            var $cards = $stack.find('.pillar-card');
301            var $row = $stack.closest('.row');
302            var $buttons = $row.find('.pillar-btn');
303
304            if ($buttons.length === 0 || $cards.length === 0) return;
305
306            // Set initial card positions dynamically
307            function positionCards(activeIndex) {
308                $cards.removeClass('pillar-card-active');
309                $cards.filter('[data-index="' + activeIndex + '"]').addClass('pillar-card-active');
310
311                var order = [];
312                $cards.each(function() {
313                    var ci = $(this).data('index');
314                    if (ci !== activeIndex) order.push(ci);
315                });
316                order.sort(function(a, b) {
317                    return Math.abs(a - activeIndex) - Math.abs(b - activeIndex);
318                });
319
320                $cards.each(function() {
321                    var ci = $(this).data('index');
322                    var $card = $(this);
323                    if (ci === activeIndex) {
324                        $card.css({ transform: 'scale(1) translateY(0)', opacity: 1, zIndex: 50, visibility: 'visible', pointerEvents: 'auto' });
325                        $card.find('.bg-dots, .border-fade-down').css('opacity', 1);
326                    } else {
327                        var pos = order.indexOf(ci);
328                        var depth = pos + 1;
329                        if (depth > 2) {
330                            $card.css({ transform: 'scale(0.94) translateY(-24px)', opacity: 0, zIndex: 0, visibility: 'hidden', pointerEvents: 'none' });
331                        } else {
332                            $card.css({
333                                transform: 'scale(' + (1 - depth * 0.03) + ') translateY(' + -(depth * 12) + 'px)',
334                                opacity: Math.max(1 - depth * 0.15, 0.2),
335                                zIndex: 50 - depth * 5,
336                                visibility: 'visible',
337                                pointerEvents: 'none'
338                            });
339                        }
340                        $card.find('.bg-dots, .border-fade-down').css('opacity', 0);
341                    }
342                });
343            }
344
345            // Get initial active index from the first pillar-card-front
346            var initialIndex = $cards.filter('.pillar-card-front').first().data('index') || 0;
347            positionCards(initialIndex);
348
349            // Measure tallest card off-screen so nothing flashes
350            function setEqualHeight() {
351                var $clone = $stack.clone();
352                $clone.css({
353                    position: 'absolute',
354                    left: '-9999px',
355                    top: '-9999px',
356                    visibility: 'hidden',
357                    width: $stack.width()
358                });
359                $clone.find('.pillar-card').css({
360                    position: 'relative',
361                    visibility: 'hidden',
362                    display: 'block',
363                    opacity: 1,
364                    transform: 'none'
365                });
366                $clone.find('.pillar-card > div').css('min-height', '');
367                $('body').append($clone);
368
369                var maxHeight = 0;
370                $clone.find('.pillar-card').each(function() {
371                    var h = $(this).outerHeight();
372                    if (h > maxHeight) maxHeight = h;
373                });
374                $clone.remove();
375
376                $cards.find('> div').css('min-height', maxHeight + 'px');
377                $stack.css('min-height', maxHeight + 'px');
378            }
379
380            setEqualHeight();
381            $(window).on('resize', setEqualHeight);
382
383            $buttons.on('click', function() {
384                var index = $(this).data('index');
385
386                $buttons.removeClass('pillar-btn-active');
387                $(this).addClass('pillar-btn-active');
388
389                $cards.removeClass('pillar-card-active');
390                $cards.filter('[data-index="' + index + '"]').addClass('pillar-card-active');
391
392                var order = [];
393                $cards.each(function() {
394                    var ci = $(this).data('index');
395                    if (ci !== index) order.push(ci);
396                });
397                order.sort(function(a, b) {
398                    return Math.abs(a - index) - Math.abs(b - index);
399                });
400
401                $cards.each(function() {
402                    var cardIndex = $(this).data('index');
403                    var $card = $(this);
404                    var $decorations = $card.find('.bg-dots, .border-fade-down');
405
406                    if (cardIndex === index) {
407                        gsap.to($card, {
408                            scale: 1, y: 0, opacity: 1, zIndex: 50,
409                            visibility: 'visible', pointerEvents: 'auto',
410                            duration: 0.5, ease: 'power3.out'
411                        });
412                        gsap.to($decorations, { opacity: 1, duration: 0.4, ease: 'power3.out' });
413                    } else {
414                        var pos = order.indexOf(cardIndex);
415                        var depth = pos + 1;
416                        if (depth > 2) {
417                            gsap.to($card, {
418                                scale: 0.94, y: -24, opacity: 0, zIndex: 0,
419                                visibility: 'hidden', pointerEvents: 'none',
420                                duration: 0.5, ease: 'power3.out'
421                            });
422                        } else {
423                            gsap.to($card, {
424                                scale: 1 - (depth * 0.03), y: -(depth * 12),
425                                opacity: Math.max(1 - (depth * 0.15), 0.2),
426                                zIndex: 50 - (depth * 5),
427                                visibility: 'visible', pointerEvents: 'none',
428                                duration: 0.5, ease: 'power3.out'
429                            });
430                        }
431                        gsap.to($decorations, { opacity: 0, duration: 0.3, ease: 'power3.out' });
432                    }
433                });
434            });
435        });
436    }
437
438    /**
439     * Initialize Blog Detail Tabs
440     */
441    function initBlogDetailTabs() {
442        var $desktopTabs = $('.blog-detail-tabs [data-toc-target]');
443
444        // Mobile dropdown is built from the desktop list so the TOC is rendered once in HTML (BD-21)
445        var $mobileContent = $('.blog-detail-tabs-mobile .dropdown-content');
446        if ($mobileContent.children().length === 0) {
447            $desktopTabs.each(function(i) {
448                $('<div class="dropdown-item dropdown-item-category"></div>')
449                    .toggleClass('active', i === 0)
450                    .attr('data-toc-target', $(this).data('toc-target'))
451                    .text($.trim($(this).text()))
452                    .appendTo($mobileContent);
453            });
454        }
455
456        var $mobileTabs = $('.blog-detail-tabs-mobile [data-toc-target]');
457        var $mobileToggle = $('.blog-detail-tabs-mobile .dropdown-toggle span');
458        var isScrolling = false;
459        var headerOffset = 150;
460
461        if ($desktopTabs.length === 0) return;
462
463        // Collect section IDs
464        var sectionIds = [];
465        $desktopTabs.each(function() {
466            sectionIds.push($(this).data('toc-target'));
467        });
468
469        function setActive(targetId) {
470            // Update desktop buttons
471            $desktopTabs.removeClass('btn-category-active');
472            $desktopTabs.filter('[data-toc-target="' + targetId + '"]').addClass('btn-category-active');
473
474            // Update mobile dropdown items
475            $mobileTabs.removeClass('active');
476            var $activeItem = $mobileTabs.filter('[data-toc-target="' + targetId + '"]');
477            $activeItem.addClass('active');
478
479            // Update mobile toggle text
480            $mobileToggle.text($activeItem.text());
481        }
482
483        function scrollToSection(targetId) {
484            var $target = $('#' + targetId);
485            if ($target.length === 0) return;
486
487            isScrolling = true;
488            var top = $target.offset().top - headerOffset;
489
490            $('html, body').animate({ scrollTop: top }, 400, function() {
491                setTimeout(function() { isScrolling = false; }, 100);
492            });
493
494            setActive(targetId);
495        }
496
497        // Desktop TOC click
498        $desktopTabs.on('click', function() {
499            scrollToSection($(this).data('toc-target'));
500        });
501
502        // Mobile dropdown item click
503        $mobileTabs.on('click', function() {
504            scrollToSection($(this).data('toc-target'));
505        });
506
507        // Scroll spy — highlight active section on scroll
508        $(window).on('scroll', function() {
509            if (isScrolling) return;
510
511            var scrollPos = $(window).scrollTop() + headerOffset + 20;
512            var activeId = sectionIds[0];
513
514            for (var i = 0; i < sectionIds.length; i++) {
515                var $el = $('#' + sectionIds[i]);
516                if ($el.length && $el.offset().top <= scrollPos) {
517                    activeId = sectionIds[i];
518                }
519            }
520
521            setActive(activeId);
522        });
523    }
524
525    /**
526     * Initialize Resources AJAX Filter (All Resources page)
527     */
528    function initResourcesFilter() {
529        var resultsEl = document.getElementById('resourcesResults');
530        var paginationEl = document.getElementById('resourcesPagination');
531        var searchInput = document.getElementById('resourcesSearch');
532        var yearToggle = document.getElementById('yearToggleText');
533        var filterBar = document.getElementById('resourcesFilterBar');
534
535        if (!resultsEl || !searchInput || !filterBar) return;
536
537        var AJAX_URL = (typeof brandefenseData !== 'undefined' && brandefenseData.ajaxUrl) ? brandefenseData.ajaxUrl : '/wp-admin/admin-ajax.php';
538
539        var stateEl = document.getElementById('resourcesState');
540        var state = {
541            type: stateEl ? stateEl.getAttribute('data-type') : 'all',
542            q: stateEl ? stateEl.getAttribute('data-q') : '',
543            filter_year: stateEl ? stateEl.getAttribute('data-year') : '',
544            paged: stateEl ? parseInt(stateEl.getAttribute('data-paged'), 10) : 1
545        };
546
547        function fetchResources(pushState) {
548            var params = new URLSearchParams({ action: 'filter_resources' });
549            if (state.type && state.type !== 'all') params.set('type', state.type);
550            if (state.q) params.set('q', state.q);
551            if (state.filter_year) params.set('filter_year', state.filter_year);
552            if (state.paged > 1) params.set('paged', state.paged);
553
554            if (pushState !== false) {
555                var urlParams = new URLSearchParams();
556                if (state.type && state.type !== 'all') urlParams.set('type', state.type);
557                if (state.q) urlParams.set('q', state.q);
558                if (state.filter_year) urlParams.set('filter_year', state.filter_year);
559                if (state.paged > 1) urlParams.set('paged', state.paged);
560                var newUrl = window.location.pathname + (urlParams.toString() ? '?' + urlParams.toString() : '');
561                history.pushState(Object.assign({}, state), '', newUrl);
562            }
563
564            resultsEl.style.opacity = '0.5';
565
566            fetch(AJAX_URL + '?' + params.toString())
567                .then(function(r) { return r.json(); })
568                .then(function(res) {
569                    if (res.success) {
570                        resultsEl.innerHTML = '<div class="container"><div class="row pt-16"><div class="col-lg-12">' + res.data.html + '</div></div></div>';
571                        paginationEl.innerHTML = res.data.pagination;
572                    }
573                    resultsEl.style.opacity = '1';
574                })
575                .catch(function(err) {
576                    console.error('Filter error:', err);
577                    resultsEl.style.opacity = '1';
578                });
579        }
580
581        // Search on Enter
582        searchInput.addEventListener('keydown', function(e) {
583            if (e.key === 'Enter') {
584                e.preventDefault();
585                state.q = searchInput.value.trim();
586                state.paged = 1;
587                fetchResources();
588            }
589        });
590
591        // Year dropdown
592        filterBar.addEventListener('click', function(e) {
593            var item = e.target.closest('.dropdown-item[data-year]');
594            if (!item) return;
595            var dropdown = item.closest('.dropdown');
596            dropdown.querySelectorAll('.dropdown-item').forEach(function(el) { el.classList.remove('active'); });
597            item.classList.add('active');
598            state.filter_year = item.getAttribute('data-year') || '';
599            state.paged = 1;
600            yearToggle.textContent = state.filter_year || 'Year';
601            fetchResources();
602        });
603
604        // Category filter buttons (desktop)
605        $(document).on('click', '.btn-filter[data-filter]', function(e) {
606            e.preventDefault();
607            $('.btn-filter').removeClass('btn-filter-active');
608            $(this).addClass('btn-filter-active');
609            state.type = $(this).attr('data-filter');
610            state.paged = 1;
611            fetchResources();
612        });
613
614        // Category filter dropdown (mobile)
615        $(document).on('click', '.dropdown-item-navy[data-filter]', function(e) {
616            e.preventDefault();
617            var $dropdown = $(this).closest('.dropdown');
618            $dropdown.find('.dropdown-item').removeClass('active');
619            $(this).addClass('active');
620            $dropdown.find('.dropdown-toggle span').text($(this).text().trim());
621            state.type = $(this).attr('data-filter');
622            state.paged = 1;
623            fetchResources();
624        });
625
626        // Pagination
627        paginationEl.addEventListener('click', function(e) {
628            var link = e.target.closest('a[href]');
629            if (!link) return;
630            e.preventDefault();
631            var href = link.getAttribute('href');
632            try {
633                var url = new URL(href, window.location.origin);
634                var pagedParam = url.searchParams.get('paged');
635                if (pagedParam) {
636                    state.paged = parseInt(pagedParam, 10);
637                } else {
638                    var match = href.match(/\/page\/(\d+)/);
639                    state.paged = match ? parseInt(match[1], 10) : 1;
640                }
641            } catch(err) {
642                state.paged = 1;
643            }
644            fetchResources();
645            window.scrollTo({ top: resultsEl.offsetTop - 120, behavior: 'smooth' });
646        });
647
648        // Browser back/forward
649        window.addEventListener('popstate', function(e) {
650            if (e.state) {
651                state = e.state;
652                searchInput.value = state.q || '';
653                yearToggle.textContent = state.filter_year || 'Year';
654                document.querySelectorAll('.btn-filter').forEach(function(b) {
655                    b.classList.toggle('btn-filter-active', b.getAttribute('data-filter') === state.type);
656                });
657                fetchResources(false);
658            }
659        });
660
661        history.replaceState(Object.assign({}, state), '', window.location.href);
662    }
663
664    /**
665     * Initialize Reports AJAX Filter (reports.php)
666     */
667    function initReportsFilter() {
668        var resultsEl = document.getElementById('reportsResults');
669        var stateEl = document.getElementById('reportsState');
670        if (!resultsEl || !stateEl) return;
671
672        var paginationEl = document.getElementById('reportsPagination');
673        var titleEl = document.getElementById('reportsTitle');
674        var form = document.getElementById('reportsForm');
675        var searchInput = form ? form.querySelector('input[name="q"]') : null;
676        var AJAX_URL = (typeof brandefenseData !== 'undefined' && brandefenseData.ajaxUrl) ? brandefenseData.ajaxUrl : '/wp-admin/admin-ajax.php';
677
678        var state = {
679            type: stateEl.getAttribute('data-type') || 'all',
680            q: stateEl.getAttribute('data-q') || '',
681            paged: parseInt(stateEl.getAttribute('data-paged'), 10) || 1
682        };
683
684        function syncFilterButtons() {
685            document.querySelectorAll('.btn-filter[data-filter]').forEach(function (b) {
686                b.classList.toggle('btn-filter-active', b.getAttribute('data-filter') === state.type);
687            });
688        }
689
690        function fetchReports(pushState) {
691            var params = new URLSearchParams({ action: 'filter_reports' });
692            if (state.type && state.type !== 'all') params.set('type', state.type);
693            if (state.q) params.set('q', state.q);
694            if (state.paged > 1) params.set('paged', state.paged);
695
696            if (pushState !== false) {
697                var urlParams = new URLSearchParams();
698                if (state.type && state.type !== 'all') urlParams.set('type', state.type);
699                if (state.q) urlParams.set('q', state.q);
700                if (state.paged > 1) urlParams.set('paged', state.paged);
701                var newUrl = window.location.pathname + (urlParams.toString() ? '?' + urlParams.toString() : '');
702                history.pushState(Object.assign({}, state), '', newUrl);
703            }
704
705            resultsEl.style.opacity = '0.5';
706
707            fetch(AJAX_URL + '?' + params.toString())
708                .then(function (r) { return r.json(); })
709                .then(function (res) {
710                    if (res.success) {
711                        resultsEl.innerHTML = '<div class="container"><div class="row justify-center pt-16"><div class="col-lg-12">' + res.data.html + '</div></div></div>';
712                        if (paginationEl) paginationEl.innerHTML = res.data.pagination;
713                        if (titleEl && res.data.title) titleEl.textContent = res.data.title;
714                    }
715                    resultsEl.style.opacity = '1';
716                })
717                .catch(function (err) {
718                    console.error('Reports filter error:', err);
719                    resultsEl.style.opacity = '1';
720                });
721        }
722
723        // Category filter buttons (desktop)
724        $(document).on('click', '.btn-filter[data-filter]', function (e) {
725            e.preventDefault();
726            state.type = $(this).attr('data-filter');
727            state.paged = 1;
728            syncFilterButtons();
729            fetchReports();
730        });
731
732        // Category filter dropdown (filter-buttons mobile)
733        $(document).on('click', '.dropdown-item-navy[data-filter]', function (e) {
734            e.preventDefault();
735            var $dropdown = $(this).closest('.dropdown');
736            $dropdown.find('.dropdown-item').removeClass('active');
737            $(this).addClass('active');
738            $dropdown.find('.dropdown-toggle span').text($(this).text().trim());
739            state.type = $(this).attr('data-filter');
740            state.paged = 1;
741            syncFilterButtons();
742            fetchReports();
743        });
744
745        if (form) {
746            // Hero category dropdown
747            form.querySelectorAll('.dropdown-item[data-filter="type"]').forEach(function (item) {
748                item.addEventListener('click', function () {
749                    var dropdown = item.closest('.dropdown');
750                    dropdown.querySelectorAll('.dropdown-item').forEach(function (el) { el.classList.remove('active'); });
751                    item.classList.add('active');
752                    var label = item.textContent.trim();
753                    dropdown.querySelector('.dropdown-toggle span').textContent = (label === 'All') ? 'Category' : label;
754                    state.type = item.getAttribute('data-value') || 'all';
755                    if (!state.type) state.type = 'all';
756                    state.paged = 1;
757                    syncFilterButtons();
758                    fetchReports();
759                });
760            });
761
762            // Hero search
763            form.addEventListener('submit', function (e) {
764                e.preventDefault();
765                state.q = searchInput ? searchInput.value.trim() : '';
766                state.paged = 1;
767                fetchReports();
768            });
769        }
770
771        // Pagination
772        if (paginationEl) {
773            paginationEl.addEventListener('click', function (e) {
774                var link = e.target.closest('a[href]');
775                if (!link) return;
776                e.preventDefault();
777                var paged = 1;
778                var href = link.getAttribute('href');
779                try {
780                    var url = new URL(href, window.location.origin);
781                    var p = url.searchParams.get('paged');
782                    if (p) {
783                        paged = parseInt(p, 10);
784                    } else {
785                        var m = href.match(/\/page\/(\d+)/);
786                        paged = m ? parseInt(m[1], 10) : 1;
787                    }
788                } catch (err) { paged = 1; }
789                state.paged = paged || 1;
790                fetchReports();
791                window.scrollTo({ top: resultsEl.offsetTop - 120, behavior: 'smooth' });
792            });
793        }
794
795        // Browser back/forward
796        window.addEventListener('popstate', function (e) {
797            if (e.state) {
798                state = Object.assign({ type: 'all', q: '', paged: 1 }, e.state);
799                if (searchInput) searchInput.value = state.q || '';
800                syncFilterButtons();
801                fetchReports(false);
802            }
803        });
804
805        history.replaceState(Object.assign({}, state), '', window.location.href);
806    }
807
808    /**
809     * Initialize Blog Category AJAX Filter (blog.php + categories.php)
810     */
811    function initBlogFilter() {
812        var resultsEl = document.getElementById('blog-results');
813        var paginationEl = document.getElementById('blog-pagination');
814        if (!resultsEl) return;
815
816        var AJAX_URL = (typeof brandefenseData !== 'undefined' && brandefenseData.ajaxUrl) ? brandefenseData.ajaxUrl : '/wp-admin/admin-ajax.php';
817
818        function slugFromHref(href) {
819            try {
820                var url = new URL(href, window.location.origin);
821                var m = url.pathname.match(/\/categories\/([^\/]+)\/?/);
822                if (m) return m[1];
823            } catch (e) {}
824            return '';
825        }
826
827        function pagedFromHref(href) {
828            try {
829                var url = new URL(href, window.location.origin);
830                var m = url.pathname.match(/\/page\/(\d+)\/?/);
831                if (m) return parseInt(m[1], 10);
832            } catch (e) {}
833            return 1;
834        }
835
836        function initialState() {
837            var path = window.location.pathname;
838            var catMatch = path.match(/\/categories\/([^\/]+)\/?/);
839            var pageMatch = path.match(/\/page\/(\d+)\/?/);
840            return {
841                cat: catMatch ? catMatch[1] : '',
842                paged: pageMatch ? parseInt(pageMatch[1], 10) : 1
843            };
844        }
845
846        var state = initialState();
847
848        function setActive() {
849            document.querySelectorAll('.btn-category').forEach(function(el) {
850                var slug = slugFromHref(el.getAttribute('href') || '');
851                el.classList.toggle('btn-category-active', slug === state.cat);
852            });
853            document.querySelectorAll('.dropdown-item-category').forEach(function(el) {
854                var slug = slugFromHref(el.getAttribute('href') || '');
855                var match = slug === state.cat;
856                el.classList.toggle('active', match);
857                if (match) {
858                    var $dd = el.closest('.dropdown');
859                    if ($dd) {
860                        var label = $dd.querySelector('.dropdown-toggle span');
861                        if (label) label.textContent = el.textContent.trim();
862                    }
863                }
864            });
865        }
866
867        function fetchBlog(pushState) {
868            var params = new URLSearchParams({ action: 'bd_blog_filter' });
869            if (state.cat) params.set('cat', state.cat);
870            if (state.paged > 1) params.set('paged', state.paged);
871
872            resultsEl.style.opacity = '0.5';
873
874            fetch(AJAX_URL + '?' + params.toString())
875                .then(function(r) { return r.json(); })
876                .then(function(res) {
877                    if (res.success) {
878                        resultsEl.innerHTML = res.data.results;
879                        if (paginationEl) paginationEl.innerHTML = res.data.pagination;
880                        var titleEl = document.getElementById('blog-hero-title');
881                        if (titleEl && res.data.title) titleEl.textContent = res.data.title;
882                        if (res.data.title) {
883                            document.title = res.data.title + ' - Blog - Brandefense';
884                        }
885                        if (pushState !== false) {
886                            history.pushState({ cat: state.cat, paged: state.paged }, '', res.data.url);
887                        }
888                        setActive();
889                    }
890                    resultsEl.style.opacity = '1';
891                })
892                .catch(function(err) {
893                    console.error('Blog filter error:', err);
894                    resultsEl.style.opacity = '1';
895                });
896        }
897
898        // Category click (desktop list + mobile dropdown)
899        document.addEventListener('click', function(e) {
900            var link = e.target.closest('.btn-category, .dropdown-item-category');
901            if (!link) return;
902            e.preventDefault();
903            state.cat = slugFromHref(link.getAttribute('href') || '');
904            state.paged = 1;
905            fetchBlog();
906            // Close mobile dropdown
907            var $dd = link.closest('.dropdown');
908            if ($dd) {
909                var $content = $dd.querySelector('.dropdown-content');
910                if ($content) $content.classList.remove('open');
911                var $toggle = $dd.querySelector('.dropdown-toggle');
912                if ($toggle) $toggle.classList.remove('active');
913            }
914            window.scrollTo({ top: resultsEl.offsetTop - 120, behavior: 'smooth' });
915        });
916
917        // Pagination
918        if (paginationEl) {
919            paginationEl.addEventListener('click', function(e) {
920                var link = e.target.closest('a[href]');
921                if (!link) return;
922                e.preventDefault();
923                state.paged = pagedFromHref(link.getAttribute('href') || '');
924                fetchBlog();
925                window.scrollTo({ top: resultsEl.offsetTop - 120, behavior: 'smooth' });
926            });
927        }
928
929        // Back/forward
930        window.addEventListener('popstate', function(e) {
931            state = e.state ? { cat: e.state.cat || '', paged: e.state.paged || 1 } : initialState();
932            fetchBlog(false);
933        });
934
935        history.replaceState({ cat: state.cat, paged: state.paged }, '', window.location.href);
936    }
937
938    /**
939     * Initialize Global Dropdowns
940     */
941    function initDropdowns() {
942        const $dropdowns = $('.dropdown');
943
944        if ($dropdowns.length === 0) return;
945
946        // Toggle dropdown on click
947        $dropdowns.each(function() {
948            const $dropdown = $(this);
949            const $toggle = $dropdown.find('.dropdown-toggle');
950            const $content = $dropdown.find('.dropdown-content');
951            const $items = $dropdown.find('.dropdown-item');
952
953            // Toggle dropdown
954            $toggle.on('click', function(e) {
955                e.stopPropagation();
956
957                // Close other dropdowns
958                $('.dropdown-content').not($content).removeClass('open');
959                $('.dropdown-toggle').not($toggle).removeClass('active');
960
961                // Toggle this dropdown
962                $content.toggleClass('open');
963                $toggle.toggleClass('active');
964            });
965
966            // Handle item click
967            $items.on('click', function() {
968                const $item = $(this);
969                const value = $item.text().trim();
970
971                // Update toggle text
972                $toggle.find('span').text(value);
973
974                // Update active state
975                $items.removeClass('active');
976                $item.addClass('active');
977
978                // Close dropdown
979                $content.removeClass('open');
980                $toggle.removeClass('active');
981            });
982        });
983
984        // Close dropdowns when clicking outside
985        $(document).on('click', function() {
986            $('.dropdown-content').removeClass('open');
987            $('.dropdown-toggle').removeClass('active');
988        });
989    }
990
991    /**
992     * Initialize Home Tab System - Poker Card Stack Style
993     */
994    function initHomeTabSystem() {
995        const $tabButtons = $('.tab-button');
996        const $tabContents = $('.home-tab-content');
997        const $prevButton = $('#home-tab-prev');
998        const $nextButton = $('#home-tab-next');
999        const $track = $('#home-tab-track');
1000
1001        // Current active tab index
1002        let currentTabIndex = 0;
1003
1004        // Wrap tab contents if not already wrapped
1005        if (!$tabContents.parent().hasClass('home-tab-wrapper')) {
1006            $tabContents.wrapAll('<div class="home-tab-wrapper"></div>');
1007        }
1008
1009        // Initialize all cards in stack position
1010        initializeCardStack($tabContents, 0);
1011
1012        // Get how many tabs are visible based on screen width
1013        function getVisibleCount() {
1014            var w = window.innerWidth;
1015            if (w >= 1200) return $tabButtons.length; // xl+: all visible
1016            if (w >= 992) return 3;  // lg: 3
1017            if (w >= 768) return 2;  // md: 2
1018            return 1;                // mobile: 1
1019        }
1020
1021        // Slide the tab track so the active tab is visible
1022        function slideTrack(activeIndex) {
1023            if (!$track.length) return;
1024            var visible = getVisibleCount();
1025            if (visible >= $tabButtons.length) {
1026                $track.css('transform', 'translateX(0)');
1027                return;
1028            }
1029            // Calculate offset so active tab is centered when possible
1030            var maxOffset = $tabButtons.length - visible;
1031            var offset = Math.min(Math.max(activeIndex - Math.floor((visible - 1) / 2), 0), maxOffset);
1032            var pct = -(offset * (100 / visible));
1033            $track.css('transform', 'translateX(' + pct + '%)');
1034        }
1035
1036        // Set active tab function
1037        function setActiveTab(newIndex) {
1038            if (newIndex < 0 || newIndex >= $tabContents.length) return;
1039            if (newIndex === currentTabIndex) return;
1040
1041            currentTabIndex = newIndex;
1042
1043            $tabButtons.removeClass('home-tab-active');
1044            $tabButtons.eq(newIndex).addClass('home-tab-active');
1045
1046            slideTrack(newIndex);
1047            animateCardStack($tabContents, newIndex);
1048        }
1049
1050        // Tab button click handler
1051        $tabButtons.on('click', function() {
1052            var tabIndex = $(this).parent().index();
1053            setActiveTab(tabIndex);
1054        });
1055
1056        // Prev button
1057        $prevButton.on('click', function() {
1058            var newIndex = currentTabIndex - 1;
1059            if (newIndex < 0) newIndex = $tabContents.length - 1;
1060            setActiveTab(newIndex);
1061        });
1062
1063        // Next button
1064        $nextButton.on('click', function() {
1065            var newIndex = currentTabIndex + 1;
1066            if (newIndex >= $tabContents.length) newIndex = 0;
1067            setActiveTab(newIndex);
1068        });
1069
1070        // Re-adjust track and card heights on resize
1071        $(window).on('resize', function() {
1072            slideTrack(currentTabIndex);
1073            recalcTabHeight($tabContents);
1074        });
1075
1076        // Initial slide position
1077        slideTrack(0);
1078    }
1079
1080    /**
1081     * Recalculate wrapper height based on tallest card
1082     */
1083    function recalcTabHeight($tabContents) {
1084        var $wrapper = $tabContents.parent('.home-tab-wrapper');
1085        if (!$wrapper.length) return;
1086        var maxHeight = 0;
1087        // Measure each card individually to avoid stacking interference
1088        $tabContents.each(function() {
1089            var $card = $(this);
1090            var savedStyle = $card.attr('style') || '';
1091            $card.css({ position: 'absolute', height: 'auto', visibility: 'hidden', display: 'flex', opacity: 0 });
1092            var h = $card.outerHeight();
1093            if (h > maxHeight) maxHeight = h;
1094            $card.attr('style', savedStyle);
1095        });
1096        $wrapper.css('min-height', maxHeight + 'px');
1097    }
1098
1099    /**
1100     * Initialize Card Stack Positions
1101     */
1102    function initializeCardStack($tabContents, activeIndex) {
1103        recalcTabHeight($tabContents);
1104
1105        $tabContents.each(function(index) {
1106            if (index === activeIndex) {
1107                // Active tab - front and visible, full size
1108                gsap.set($(this), {
1109                    y: 0,
1110                    scale: 1,
1111                    opacity: 1,
1112                    zIndex: 50,
1113                    display: 'block',
1114                    visibility: 'visible',
1115                    pointerEvents: 'auto'
1116                });
1117            } else if (index < activeIndex && (activeIndex - index) <= 2) {
1118                // Previous tabs (max 2) - stacked behind
1119                const stackPosition = activeIndex - index;
1120                gsap.set($(this), {
1121                    y: -20 * stackPosition,
1122                    scale: 1 - (stackPosition * 0.05), // Each card 5% smaller
1123                    opacity: Math.max(0.3, 1 - (stackPosition * 0.2)),
1124                    zIndex: 50 - stackPosition,
1125                    display: 'block',
1126                    visibility: 'visible',
1127                    pointerEvents: 'none'
1128                });
1129            } else {
1130                // Future tabs - hidden
1131                gsap.set($(this), {
1132                    y: 100,
1133                    scale: 1,
1134                    opacity: 0,
1135                    zIndex: 0,
1136                    display: 'none',
1137                    visibility: 'hidden',
1138                    pointerEvents: 'none'
1139                });
1140            }
1141        });
1142    }
1143
1144    /**
1145     * Animate Card Stack - Poker Style
1146     */
1147    function animateCardStack($tabContents, newActiveIndex) {
1148        $tabContents.each(function(index) {
1149            const $tab = $(this);
1150
1151            if (index === newActiveIndex) {
1152                // Active tab - fade in from bottom with accelerating animation
1153                const wasHidden = $tab.css('display') === 'none';
1154
1155                if (wasHidden) {
1156                    gsap.set($tab, { display: 'block', visibility: 'visible', y: 100, scale: 1, opacity: 0, zIndex: 50 });
1157                }
1158
1159                // Fade in - accelerating ease
1160                gsap.to($tab, {
1161                    y: 0,
1162                    scale: 1,
1163                    opacity: 1,
1164                    zIndex: 50,
1165                    visibility: 'visible',
1166                    pointerEvents: 'auto',
1167                    duration: 0.5,
1168                    delay: 0.15,
1169                    ease: 'power3.out' // Starts fast, ends slow
1170                });
1171
1172            } else if (index < newActiveIndex && (newActiveIndex - index) <= 2) {
1173                // Previous tabs (max 2) - fade out then fade in at stack position
1174                const stackPosition = newActiveIndex - index;
1175                const wasHidden = $tab.css('display') === 'none';
1176
1177                if (wasHidden) {
1178                    gsap.set($tab, { display: 'block', visibility: 'visible', y: 100, scale: 1, opacity: 0, zIndex: 50 - stackPosition });
1179                }
1180
1181                // Create timeline for fade out -> fade in effect
1182                const tl = gsap.timeline();
1183
1184                // First fade out quickly
1185                tl.to($tab, {
1186                    opacity: 0,
1187                    duration: 0.15,
1188                    ease: 'power2.in'
1189                })
1190                // Then move to stack position and fade in
1191                .to($tab, {
1192                    y: -20 * stackPosition,
1193                    scale: 1 - (stackPosition * 0.05),
1194                    opacity: Math.max(0.3, 1 - (stackPosition * 0.2)),
1195                    zIndex: 50 - stackPosition,
1196                    visibility: 'visible',
1197                    pointerEvents: 'none',
1198                    duration: 0.35,
1199                    ease: 'back.out(1.2)' // Accelerating with slight overshoot
1200                });
1201
1202            } else {
1203                // Future tabs - quick fade out
1204                gsap.to($tab, {
1205                    y: 100,
1206                    scale: 1,
1207                    opacity: 0,
1208                    zIndex: 0,
1209                    duration: 0.3,
1210                    ease: 'power3.in', // Accelerating out
1211                    onComplete: function() {
1212                        gsap.set($tab, { display: 'none', visibility: 'hidden', pointerEvents: 'none' });
1213                    }
1214                });
1215
1216            }
1217        });
1218    }
1219
1220    /**
1221     * Calculate card position in stack
1222     */
1223    function getCardPosition(cardIndex, activeIndex, totalCards) {
1224        if (cardIndex === activeIndex) {
1225            return 0; // Front card
1226        }
1227
1228        // Calculate relative position
1229        let position = cardIndex - activeIndex;
1230
1231        // Wrap around if negative
1232        if (position < 0) {
1233            position = totalCards + position;
1234        }
1235
1236        return position;
1237    }
1238
1239    /**
1240     * Initialize Stat Cards Mouse Tracking Gradient
1241     */
1242    function initStatCardsGradient() {
1243        const $statCardsWrapper = $('.stat-cards-wrapper');
1244        const $statCards = $statCardsWrapper.find('.bg-focus');
1245
1246        if ($statCards.length === 0) return;
1247
1248        // Get the dots background image URL from the first stat-card-dots element
1249        var dotsImageUrl = '';
1250        var $firstDots = $statCards.first().find('.stat-card-dots');
1251        if ($firstDots.length) {
1252            dotsImageUrl = $firstDots.css('background-image');
1253        }
1254
1255        // Create overlays for each card: one for red glow (blurred), one for dots (sharp)
1256        $statCards.each(function() {
1257            var $card = $(this);
1258            $card.append('<div class="stat-card-gradient"></div>');
1259            $card.append('<div class="stat-card-dots-overlay"></div>');
1260        });
1261
1262        // Style the dots overlay
1263        $('.stat-card-dots-overlay').css({
1264            position: 'absolute',
1265            top: 0, left: 0, right: 0, bottom: 0,
1266            backgroundImage: dotsImageUrl,
1267            backgroundRepeat: 'repeat',
1268            opacity: 0,
1269            pointerEvents: 'none',
1270            zIndex: 2,
1271            transition: 'opacity 0.3s ease-out',
1272            borderRadius: 'inherit'
1273        });
1274
1275        // Helper: apply gradient effect to cards given a simulated page position
1276        function applyGradientAt(pageX, pageY) {
1277            $statCards.each(function() {
1278                var $card = $(this);
1279                var cardOffset = $card.offset();
1280                var cardWidth = $card.outerWidth();
1281                var cardHeight = $card.outerHeight();
1282
1283                var relativeX = pageX - cardOffset.left;
1284                var relativeY = pageY - cardOffset.top;
1285                var percentX = (relativeX / cardWidth) * 100;
1286                var percentY = (relativeY / cardHeight) * 100;
1287
1288                var cardCenterX = cardOffset.left + cardWidth / 2;
1289                var cardCenterY = cardOffset.top + cardHeight / 2;
1290                var distanceToCenter = Math.sqrt(
1291                    Math.pow(pageX - cardCenterX, 2) +
1292                    Math.pow(pageY - cardCenterY, 2)
1293                );
1294
1295                var maxDistance = 400;
1296                var intensity = Math.max(0, 1 - (distanceToCenter / maxDistance));
1297
1298                var $gradient = $card.find('.stat-card-gradient');
1299                var dotsOverlay = $card.find('.stat-card-dots-overlay')[0];
1300                var radialAt = percentX + '% ' + percentY + '%';
1301
1302                if (intensity > 0) {
1303                    $gradient.css({
1304                        background: 'radial-gradient(circle 300px at ' + radialAt + ', rgba(255, 0, 0, ' + (intensity * 0.5) + ') 0%, transparent 70%)',
1305                        opacity: 1
1306                    });
1307                    if (dotsOverlay) {
1308                        var mask = 'radial-gradient(circle 150px at ' + radialAt + ', black 0%, transparent 100%)';
1309                        dotsOverlay.style.setProperty('-webkit-mask-image', mask);
1310                        dotsOverlay.style.setProperty('mask-image', mask);
1311                        dotsOverlay.style.opacity = '1';
1312                    }
1313                } else {
1314                    $gradient.css('opacity', 0);
1315                    if (dotsOverlay) dotsOverlay.style.opacity = '0';
1316                }
1317            });
1318        }
1319
1320        // Show default state: simulate mouse at center of wrapper
1321        function showDefaultState() {
1322            var wrapperOffset = $statCardsWrapper.offset();
1323            var centerX = wrapperOffset.left + $statCardsWrapper.outerWidth() / 2;
1324            var centerY = wrapperOffset.top + $statCardsWrapper.outerHeight() / 2;
1325            applyGradientAt(centerX, centerY);
1326        }
1327
1328        // Initialize with center glow visible
1329        showDefaultState();
1330
1331        // Track mouse movement over the wrapper
1332        $statCardsWrapper.on('mousemove', function(e) {
1333            applyGradientAt(e.pageX, e.pageY);
1334        });
1335
1336        // On mouse leave: keep the last state (do nothing)
1337    }
1338
1339
1340    /**
1341     * Initialize Reports Swiper
1342     */
1343    function initReportsSwiper() {
1344        if (typeof Swiper === 'undefined') return;
1345
1346        const reportsSwiper = new Swiper('.reports-swiper', {
1347            slidesPerView: 'auto',
1348            spaceBetween: 20,
1349            loop: true,
1350            navigation: {
1351                nextEl: '.reports-swiper-next',
1352                prevEl: '.reports-swiper-prev',
1353            }
1354        });
1355    }
1356
1357    /**
1358     * Initialize Life at Brandefense Swiper
1359     */
1360    function initLifeSwiper() {
1361        if (typeof Swiper === 'undefined') return;
1362
1363        const lifeSwiper = new Swiper('.life-swiper', {
1364            slidesPerView: 'auto',
1365            spaceBetween: 20,
1366            loop: true,
1367            navigation: {
1368                nextEl: '.life-swiper-next',
1369                prevEl: '.life-swiper-prev',
1370            }
1371        });
1372    }
1373
1374    /**
1375     * Calculate and set Life Swiper width to extend to browser edge
1376     */
1377    function initLifeSwiperWidth() {
1378        const $swiperWrapper = $('#life-swiper-wrapper');
1379        const $colLg12 = $swiperWrapper.closest('.col-lg-12');
1380
1381        if ($swiperWrapper.length === 0 || $colLg12.length === 0) return;
1382
1383        function calculateWidth() {
1384            const colRect = $colLg12[0].getBoundingClientRect();
1385            const colRightEdge = colRect.right;
1386            const viewportWidth = window.innerWidth;
1387            const distanceToRight = viewportWidth - colRightEdge - 3;
1388
1389            $swiperWrapper.css('right', -distanceToRight + 'px');
1390        }
1391
1392        calculateWidth();
1393
1394        $(window).on('resize', function() {
1395            calculateWidth();
1396        });
1397    }
1398
1399    /**
1400     * Initialize Right Aligned Swiper
1401     */
1402    function initRightAlignedSwiper() {
1403        if (typeof Swiper === 'undefined') return;
1404
1405        const rightAlignedSwiper = new Swiper('.right-aligned-swiper', {
1406            slidesPerView: 'auto',
1407            spaceBetween: 20,
1408            loop: true,
1409            navigation: {
1410                nextEl: '.right-aligned-swiper-next',
1411                prevEl: '.right-aligned-swiper-prev',
1412            }
1413        });
1414    }
1415
1416    /**
1417     * Calculate and set Right Aligned Swiper width to extend to browser edge
1418     */
1419    function initRightAlignedSwiperWidth() {
1420        const $swiperWrapper = $('#right-aligned-swiper-wrapper');
1421        const $colLg = $swiperWrapper.closest('[class*="col-lg-"]');
1422
1423        if ($swiperWrapper.length === 0 || $colLg.length === 0) return;
1424
1425        function calculateWidth() {
1426            const colRect = $colLg[0].getBoundingClientRect();
1427            const colRightEdge = colRect.right;
1428            const viewportWidth = window.innerWidth;
1429            const distanceToRight = viewportWidth - colRightEdge - 3;
1430            $swiperWrapper.css('right', -distanceToRight + 'px');
1431        }
1432
1433        calculateWidth();
1434
1435        $(window).on('resize', function() {
1436            calculateWidth();
1437        });
1438    }
1439
1440    /**
1441     * Initialize Feature Cards Swiper (Mobile)
1442     */
1443    function initFeatureCardsSwiper() {
1444        if (typeof Swiper === 'undefined') return;
1445
1446        const featureCardsSwiper = new Swiper('.feature-cards-swiper', {
1447            slidesPerView: 1,
1448            spaceBetween: 20,
1449            loop: true,
1450            navigation: {
1451                nextEl: '.feature-cards-next',
1452                prevEl: '.feature-cards-prev',
1453            },
1454            breakpoints: {
1455                // sm (576px+): 1 slides
1456                576: {
1457                    slidesPerView: 1,
1458                    spaceBetween: 20
1459                },
1460                // md (768px+): 2 slide
1461                768: {
1462                    slidesPerView: 2,
1463                    spaceBetween: 20
1464                }
1465            }
1466        });
1467    }
1468
1469    /**
1470     * Initialize Success Stories Swiper (Mobile)
1471     */
1472    function initSuccessStoriesSwiper() {
1473        if (typeof Swiper === 'undefined') return;
1474
1475        const successStoriesSwiper = new Swiper('.success-stories-swiper', {
1476            slidesPerView: 1,
1477            spaceBetween: 20,
1478            loop: true,
1479            navigation: {
1480                nextEl: '.success-stories-next',
1481                prevEl: '.success-stories-prev',
1482            },
1483            breakpoints: {
1484                // md (768px) ve üzeri: 2 slide görünür
1485                768: {
1486                    slidesPerView: 2,
1487                    spaceBetween: 20
1488                }
1489            }
1490        });
1491    }
1492
1493    /**
1494     * Initialize Blog Swiper (Mobile)
1495     */
1496    function initBlogSwiper() {
1497        if (typeof Swiper === 'undefined') return;
1498
1499        const blogSwiper = new Swiper('.blog-swiper', {
1500            slidesPerView: 1.1,
1501            spaceBetween: 16,
1502            loop: true,
1503            navigation: {
1504                nextEl: '.blog-swiper-next',
1505                prevEl: '.blog-swiper-prev',
1506            }
1507        });
1508    }
1509
1510    /**
1511     * Initialize Pipeline Swiper (Mobile)
1512     */
1513    function initPipelineSwiper() {
1514        if (typeof Swiper === 'undefined') return;
1515
1516        const pipelineSwiper = new Swiper('.pipeline-swiper', {
1517            slidesPerView: 1,
1518            spaceBetween: 12,
1519            loop: true,
1520            navigation: {
1521                nextEl: '.pipeline-swiper-next',
1522                prevEl: '.pipeline-swiper-prev',
1523            }
1524        });
1525    }
1526
1527    /**
1528     * Initialize Charts Swiper (Mobile)
1529     */
1530    function initChartsSwiper() {
1531        if (typeof Swiper === 'undefined') return;
1532
1533        const chartsSwiper = new Swiper('.charts-swiper', {
1534            slidesPerView: 1,
1535            spaceBetween: 12,
1536            loop: true,
1537            navigation: {
1538                nextEl: '.charts-swiper-next',
1539                prevEl: '.charts-swiper-prev',
1540            }
1541        });
1542    }
1543
1544    /**
1545     * Initialize Radar Info Swiper (Mobile)
1546     */
1547    function initProductRadarInfoSwiper() {
1548        if (typeof Swiper === 'undefined') return;
1549
1550        const radarInfoSwiper = new Swiper('.radar-info-swiper', {
1551            slidesPerView: 1,
1552            spaceBetween: 20,
1553            loop: true,
1554            navigation: {
1555                nextEl: '.radar-info-next',
1556                prevEl: '.radar-info-prev',
1557            }
1558        });
1559    }
1560
1561    /**
1562     * Initialize Footer Toggle Accordions (xl and below - < 1200px)
1563     */
1564    function initFooterToggle() {
1565        const $toggleButtons = $('.footer-toggle-btn');
1566        const $toggleContents = $('.footer-toggle-content');
1567        const xlBreakpoint = 1200;
1568
1569        if ($toggleButtons.length === 0) return;
1570
1571        function handleToggle() {
1572            // Only work on xl and below
1573            if (window.innerWidth >= xlBreakpoint) return;
1574
1575            const $button = $(this);
1576            const $section = $button.closest('.footer-toggle-section');
1577            const $content = $section.find('.footer-toggle-content');
1578
1579            // Toggle active class on button
1580            $button.toggleClass('active');
1581
1582            // Toggle active class on content
1583            $content.toggleClass('active');
1584
1585            // Close other sections (accordion behavior)
1586            $toggleButtons.not($button).removeClass('active');
1587            $toggleContents.not($content).removeClass('active');
1588        }
1589
1590        function resetAccordion() {
1591            // Reset all accordions when screen is larger than xl
1592            if (window.innerWidth >= xlBreakpoint) {
1593                $toggleButtons.removeClass('active');
1594                $toggleContents.removeClass('active');
1595            }
1596        }
1597
1598        // Attach click handler
1599        $toggleButtons.on('click', handleToggle);
1600
1601        // Reset on resize
1602        $(window).on('resize', resetAccordion);
1603
1604        // Initial check
1605        resetAccordion();
1606    }
1607
1608    /**
1609     * Initialize Feature Card Hover Animations
1610     */
1611    function initFeatureCardHover() {
1612        const $featureCards = $('.feature-card');
1613
1614        if ($featureCards.length === 0) return;
1615
1616        // Configuration
1617        const cardDefaultWidth = 125;
1618        const cardExpandedWidth = 350;
1619        const animationDuration = 0.6;
1620
1621        // Set first card as expanded on load
1622        const $firstCard = $featureCards.first();
1623        const $firstText = $firstCard.find('.feature-card-text');
1624        const $firstVerticalText = $firstCard.find('.feature-card-vertical-text');
1625        gsap.set($firstCard, { width: cardExpandedWidth });
1626        gsap.set($firstText, { opacity: 1 });
1627        gsap.set($firstVerticalText, { display: 'none', opacity: 0 });
1628
1629        $featureCards.each(function() {
1630            const $card = $(this);
1631            const $text = $card.find('.feature-card-text');
1632            const $verticalText = $card.find('.feature-card-vertical-text');
1633
1634            // Mouse enter - collapse all others, expand this one (synchronized)
1635            $card.on('mouseenter', function() {
1636                // Collapse all other cards (same duration and easing)
1637                $featureCards.not($card).each(function() {
1638                    const $otherCard = $(this);
1639                    const $otherText = $otherCard.find('.feature-card-text');
1640                    const $otherVerticalText = $otherCard.find('.feature-card-vertical-text');
1641
1642                    // Hide body text instantly (no fade out)
1643                    gsap.killTweensOf($otherText);
1644                    gsap.set($otherText, { opacity: 0 });
1645
1646                    // Collapse card
1647                    gsap.to($otherCard, {
1648                        width: cardDefaultWidth,
1649                        duration: animationDuration,
1650                        ease: 'power2.inOut',
1651                        overwrite: true
1652                    });
1653
1654                    // Fade in vertical text concurrently, near the end of the collapse
1655                    if ($otherVerticalText.css('display') === 'none') {
1656                        gsap.set($otherVerticalText, { display: 'flex', opacity: 0 });
1657                    }
1658                    gsap.to($otherVerticalText, {
1659                        opacity: 1,
1660                        duration: animationDuration * 0.6,
1661                        delay: animationDuration * 0.4,
1662                        ease: 'power2.out',
1663                        overwrite: true
1664                    });
1665                });
1666
1667                // Fade out vertical text concurrently as this card expands
1668                gsap.to($verticalText, {
1669                    opacity: 0,
1670                    duration: animationDuration * 0.4,
1671                    ease: 'power2.inOut',
1672                    overwrite: true,
1673                    onComplete: function() {
1674                        gsap.set($verticalText, { display: 'none' });
1675                    }
1676                });
1677
1678                // Hide body text instantly so it can't pop during expansion
1679                gsap.set($text, { opacity: 0 });
1680
1681                // Expand this card
1682                gsap.to($card, {
1683                    width: cardExpandedWidth,
1684                    duration: animationDuration,
1685                    ease: 'power2.inOut',
1686                    overwrite: true
1687                });
1688
1689                // Fade in body text slowly (~1s) with a soft ease, overlapping
1690                // the last ~25% of the expansion so the motions blend
1691                gsap.to($text, {
1692                    opacity: 1,
1693                    duration: .6,
1694                    delay: animationDuration * 0.75,
1695                    ease: 'sine.inOut',
1696                    overwrite: true
1697                });
1698            });
1699        });
1700    }
1701
1702    /**
1703     * Initialize Security Leaders Swiper (Mobile)
1704     */
1705    function initSecurityLeadersSwiper() {
1706        if (typeof Swiper === 'undefined') return;
1707        if (!document.querySelector('.security-leaders-swiper')) return;
1708
1709        new Swiper('.security-leaders-swiper', {
1710            slidesPerView: 1,
1711            spaceBetween: 16,
1712            loop: true,
1713            navigation: {
1714                nextEl: '.security-leaders-swiper-next',
1715                prevEl: '.security-leaders-swiper-prev',
1716            },
1717        });
1718    }
1719
1720    /**
1721     * Initialize Reviews Swiper (Mobile - Gartner Section)
1722     */
1723    function initReviewsSwiper() {
1724        if (typeof Swiper === 'undefined') return;
1725
1726        const reviewsSwiper = new Swiper('.reviews-swiper', {
1727            slidesPerView: 1,
1728            spaceBetween: 20,
1729            loop: true,
1730            navigation: {
1731                nextEl: '.reviews-swiper-next',
1732                prevEl: '.reviews-swiper-prev',
1733            },
1734            breakpoints: {
1735                // sm (576px+): 1 slides
1736                576: {
1737                    slidesPerView: 1,
1738                    spaceBetween: 20
1739                },
1740                // md (768px+): 2 slide
1741                768: {
1742                    slidesPerView: 2,
1743                    spaceBetween: 20
1744                }
1745            }
1746        });
1747    }
1748
1749
1750    /**
1751     * Calculate and set Reports Swiper width to extend to browser edge
1752     */
1753    function initReportsSwiperWidth() {
1754        const $swiperWrapper = $('#reports-swiper-wrapper');
1755        const $colLg12 = $swiperWrapper.closest('.col-lg-12');
1756
1757        if ($swiperWrapper.length === 0 || $colLg12.length === 0) return;
1758
1759        function calculateWidth() {
1760            // Get col-lg-12's right edge position
1761            const colRect = $colLg12[0].getBoundingClientRect();
1762            const colRightEdge = colRect.right;
1763
1764            // Get viewport width
1765            const viewportWidth = window.innerWidth;
1766
1767            // Calculate distance from col-lg-12 right edge to viewport right edge
1768            // Add 12px (0.75rem) offset for padding compensation
1769            const distanceToRight = viewportWidth - colRightEdge - 3;
1770
1771            // Set negative right value so swiper extends to browser edge
1772            $swiperWrapper.css('right', -distanceToRight + 'px');
1773        }
1774
1775        // Calculate on load
1776        calculateWidth();
1777
1778        // Recalculate on resize
1779        $(window).on('resize', function() {
1780            calculateWidth();
1781        });
1782    }
1783
1784    /**
1785     * Initialize Resources Hero Swiper (mobile/tablet)
1786     */
1787    function initResourcesHeroSwiper() {
1788        const $swiper = $('.resources-hero-swiper');
1789        if ($swiper.length === 0) return;
1790
1791        new Swiper('.resources-hero-swiper', {
1792            slidesPerView: 1.1,
1793            spaceBetween: 16,
1794            loop: true,
1795            autoplay: {
1796                delay: 3000,
1797                disableOnInteraction: false
1798            },
1799            breakpoints: {
1800                768: {
1801                    slidesPerView: 2.3,
1802                    spaceBetween: 20
1803                }
1804            }
1805        });
1806    }
1807
1808
1809    /**
1810     * Initialize Diagram Canvas (Pixi.js + GSAP)
1811     * Only runs on front page where #diagram-canvas-container exists
1812     */
1813    async function initDiagramCanvas() {
1814        if (typeof PIXI === 'undefined') return;
1815
1816        const container = document.getElementById('diagram-canvas-container');
1817        if (!container) return;
1818
1819        const { Application, Assets, Sprite, Text, Graphics } = PIXI;
1820
1821        // Create a new application
1822        const app = new Application();
1823        // Initialize the application
1824        await app.init({ background: '#000313', width: 860, height: 520 });
1825
1826        const enableSpin = true;
1827        const textDefaultAlpha = 0.3;
1828
1829        if (document.fonts?.load) {
1830            await document.fonts.load('400 16px "Manrope"');
1831        }
1832
1833        container.appendChild(app.canvas);
1834
1835        const themeUrl = brandefenseData.themeUrl;
1836        const layerBackground = await Assets.load(`${themeUrl}/assets/images/layer-background.png`);
1837        const layerHoverBackground = await Assets.load(`${themeUrl}/assets/images/hove-bg.png`);
1838        const layerDots = await Assets.load(`${themeUrl}/assets/images/layer-dots.png`);
1839        const layerLogo = await Assets.load(`${themeUrl}/assets/images/layer-logo.png`);
1840        const layerTexts = await Assets.load(`${themeUrl}/assets/images/curved-texts.svg`);
1841        const layerHover = await Assets.load(`${themeUrl}/assets/images/ColouredBlur.png`);
1842
1843        const background = new Sprite(layerBackground);
1844        const hoverbackgroundLeft = new Sprite(layerHoverBackground);
1845        const hoverbackgroundCenter = new Sprite(layerHoverBackground);
1846        const hoverbackgroundRight = new Sprite(layerHoverBackground);
1847        const hoverTriangleLeft = new Graphics();
1848        const hoverTriangleCenter = new Graphics();
1849        const hoverTriangleRight = new Graphics();
1850        const hover = new Sprite(layerHover);
1851        const dots = new Sprite(layerDots);
1852        const logo = new Sprite(layerLogo);
1853        const texts = new Sprite(layerTexts);
1854
1855        const isMobile = window.innerWidth < 992;
1856
1857        const text1 = "Detect phishing, credential leaks,\nbrand impersonation, and fraud\nacross the open, deep, and dark\nweb. Before they impact your\nbusiness.";
1858        const text2 = "Map your entire external footprint:\nexposed assets, misconfigurations,\nand shadow IT. In real time.";
1859        const text3 = "AI-driven, industry-tailored threat \n intel. Your team prioritizes what\n matters and responds faster.";
1860        const copyStyle = {
1861            fontFamily: "Manrope",
1862            fontSize: 14,
1863            lineHeight: 22,
1864            fill: "#D4D2DE",
1865            wordWrap: true,
1866            wordWrapWidth: 250,
1867            align: "center",
1868        };
1869        const leftText = new Text({ text: text1, style: copyStyle });
1870        const centerText = new Text({ text: text2, style: copyStyle });
1871        const rightText = new Text({ text: text3, style: copyStyle });
1872
1873        // Hide text labels on mobile - the swiper handles text display
1874        if (isMobile) {
1875            leftText.visible = false;
1876            centerText.visible = false;
1877            rightText.visible = false;
1878        }
1879
1880        app.stage.addChild(hover);
1881        app.stage.addChild(hoverbackgroundLeft);
1882        app.stage.addChild(hoverbackgroundCenter);
1883        app.stage.addChild(hoverbackgroundRight);
1884        app.stage.addChild(dots);
1885        app.stage.addChild(logo);
1886        app.stage.addChild(texts);
1887        app.stage.addChild(leftText);
1888        app.stage.addChild(centerText);
1889        app.stage.addChild(rightText);
1890        app.stage.addChild(hoverTriangleCenter);
1891        app.stage.addChild(hoverTriangleLeft);
1892        app.stage.addChild(hoverTriangleRight);
1893
1894        hoverbackgroundLeft.anchor.set(0.5);
1895        hoverbackgroundLeft.x = app.screen.width / 2;
1896        hoverbackgroundLeft.y = 417;
1897        hoverbackgroundLeft.rotation = -1.0472;
1898        hoverbackgroundLeft.alpha = textDefaultAlpha;
1899        hoverbackgroundLeft.eventMode = "static";
1900        hoverbackgroundLeft.cursor = "pointer";
1901
1902        hoverbackgroundCenter.anchor.set(0.5);
1903        hoverbackgroundCenter.x = app.screen.width / 2;
1904        hoverbackgroundCenter.y = 417;
1905        hoverbackgroundCenter.eventMode = "static";
1906        hoverbackgroundCenter.cursor = "pointer";
1907
1908        hoverbackgroundRight.anchor.set(0.5);
1909        hoverbackgroundRight.x = app.screen.width / 2;
1910        hoverbackgroundRight.y = 417;
1911        hoverbackgroundRight.rotation = 1.0472;
1912        hoverbackgroundRight.alpha = textDefaultAlpha;
1913        hoverbackgroundRight.eventMode = "static";
1914        hoverbackgroundRight.cursor = "pointer";
1915
1916        const trianglePoints = [0, 0, -249, -417, 249, -417];
1917        hoverTriangleLeft.poly(trianglePoints).fill({ color: 0x00ffcc });
1918        hoverTriangleLeft.x = app.screen.width / 2;
1919        hoverTriangleLeft.y = 417;
1920        hoverTriangleLeft.rotation = -1.0472;
1921        hoverTriangleLeft.eventMode = "static";
1922        hoverTriangleLeft.cursor = "pointer";
1923        hoverTriangleLeft.alpha = 0;
1924
1925        hoverTriangleCenter.poly(trianglePoints).fill({ color: 0x00ffdd });
1926        hoverTriangleCenter.x = app.screen.width / 2;
1927        hoverTriangleCenter.y = 417;
1928        hoverTriangleCenter.eventMode = "static";
1929        hoverTriangleCenter.cursor = "pointer";
1930        hoverTriangleCenter.alpha = 0;
1931
1932        hoverTriangleRight.poly(trianglePoints).fill({ color: 0x00ffff });
1933        hoverTriangleRight.x = app.screen.width / 2;
1934        hoverTriangleRight.y = 417;
1935        hoverTriangleRight.rotation = 1.0472;
1936        hoverTriangleRight.eventMode = "static";
1937        hoverTriangleRight.cursor = "pointer";
1938        hoverTriangleRight.alpha = 0;
1939
1940        hover.anchor.set(0.5);
1941        hover.x = app.screen.width / 2;
1942        hover.y = 417;
1943        hover.alpha = .8;
1944
1945        leftText.anchor.set(0, 0.5);
1946        leftText.x = 110;
1947        leftText.y = app.screen.height / 2 + 50;
1948        leftText.alpha = textDefaultAlpha;
1949
1950        centerText.anchor.set(0.5);
1951        centerText.x = app.screen.width / 2;
1952        centerText.y = app.screen.height / 2 - 100;
1953        centerText.alpha = 1;
1954
1955        rightText.anchor.set(1, 0.5);
1956        rightText.x = app.screen.width - 100;
1957        rightText.y = app.screen.height / 2 + 20;
1958        rightText.alpha = textDefaultAlpha;
1959
1960        const leftGroup = [hoverbackgroundLeft, leftText];
1961        const centerGroup = [hoverbackgroundCenter, centerText];
1962        const rightGroup = [hoverbackgroundRight, rightText];
1963        const allHoverTargets = [...leftGroup, ...centerGroup, ...rightGroup];
1964        const defaultAlphas = new Map(allHoverTargets.map((target) => [target, target.alpha]));
1965
1966        const fadeGroup = (targets, alpha) =>
1967            gsap.to(targets, {
1968                alpha,
1969                duration: 0.95,
1970                ease: "power2.out",
1971                overwrite: "auto",
1972            });
1973
1974        const resetHoverState = () =>
1975            gsap.to(allHoverTargets, {
1976                alpha: (index, target) => defaultAlphas.get(target),
1977                duration: 0.35,
1978                ease: "power2.out",
1979                overwrite: "auto",
1980            });
1981
1982        const baseRotation = hover.rotation;
1983        const hoverSpin = gsap.to(hover, {
1984            rotation: baseRotation + (Math.PI / 2) + 0.5,
1985            duration: 8,
1986            ease: "power1.inOut",
1987            paused: true,
1988        });
1989        // Park the radar beam at the center position — no automatic left/right sweep.
1990        hoverSpin.progress(0.5);
1991
1992        const scrubSpinTo = (progress) => {
1993            hoverSpin.pause();
1994            gsap.to(hoverSpin, {
1995                progress,
1996                duration: 1.5,
1997                delay: 0,
1998                ease: "power2.inOut",
1999                overwrite: "auto",
2000            });
2001        };
2002
2003        app.canvas.addEventListener("pointerleave", () => {
2004            scrubSpinTo(0.5);
2005            resetHoverState();
2006        });
2007
2008        hoverTriangleLeft.on("pointerover", () => {
2009            fadeGroup(leftGroup, 1);
2010            fadeGroup(centerGroup, textDefaultAlpha);
2011            fadeGroup(rightGroup, textDefaultAlpha);
2012            enableSpin ? scrubSpinTo(0) : null;
2013        });
2014
2015        hoverTriangleCenter.on("pointerover", () => {
2016            fadeGroup(leftGroup, textDefaultAlpha);
2017            fadeGroup(centerGroup, 1);
2018            fadeGroup(rightGroup, textDefaultAlpha);
2019            enableSpin ? scrubSpinTo(0.5) : null;
2020        });
2021
2022        hoverTriangleRight.on("pointerover", () => {
2023            fadeGroup(leftGroup, textDefaultAlpha);
2024            fadeGroup(centerGroup, textDefaultAlpha);
2025            fadeGroup(rightGroup, 1);
2026            enableSpin ? scrubSpinTo(1) : null;
2027        });
2028
2029        // Mobile: connect swiper to canvas animation
2030        if (isMobile) {
2031            var radarSwiper = document.querySelector('.radar-info-swiper');
2032            if (radarSwiper && radarSwiper.swiper) {
2033                radarSwiper.swiper.on('slideChange', function () {
2034                    var idx = radarSwiper.swiper.realIndex;
2035                    if (idx === 0) {
2036                        fadeGroup(leftGroup, 1);
2037                        fadeGroup(centerGroup, textDefaultAlpha);
2038                        fadeGroup(rightGroup, textDefaultAlpha);
2039                        scrubSpinTo(0);
2040                    } else if (idx === 1) {
2041                        fadeGroup(leftGroup, textDefaultAlpha);
2042                        fadeGroup(centerGroup, 1);
2043                        fadeGroup(rightGroup, textDefaultAlpha);
2044                        scrubSpinTo(0.5);
2045                    } else if (idx === 2) {
2046                        fadeGroup(leftGroup, textDefaultAlpha);
2047                        fadeGroup(centerGroup, textDefaultAlpha);
2048                        fadeGroup(rightGroup, 1);
2049                        scrubSpinTo(1);
2050                    }
2051                });
2052            }
2053        }
2054    }
2055
2056    /**
2057     * Initialize Cybersecurity Glossary System
2058     * Handles letter selection, word selection, and card navigation
2059     */
2060    function initGlossarySystem() {
2061        if (typeof brandefenseData === 'undefined' || !brandefenseData.glossaryData) {
2062            console.warn('Glossary data not available');
2063            return;
2064        }
2065
2066        const glossaryData = brandefenseData.glossaryData;
2067        let currentLetter = 'A';
2068        let currentTermIndex = 0;
2069
2070        // DOM Elements
2071        const $alphabetButtons = $('#glossary-alphabet .btn-cybersecurity-glossary');
2072        const $currentLetterDisplay = $('#glossary-current-letter');
2073        const $wordList = $('#glossary-word-list');
2074        const $cardContainer = $('#glossary-card-container');
2075        const $cards = $cardContainer.find('.glossary-card');
2076        const $prevBtn = $('#glossary-prev');
2077        const $nextBtn = $('#glossary-next');
2078
2079        // Word Dropdown Elements (mobile/tablet)
2080        const $wordDropdownToggle = $('#glossary-word-toggle');
2081        const $wordDropdownToggleText = $('#glossary-word-toggle-text');
2082
2083        /**
2084         * Get terms for current letter
2085         */
2086        function getCurrentTerms() {
2087            return glossaryData[currentLetter] || [];
2088        }
2089
2090        /**
2091         * Update word list for selected letter
2092         */
2093        function updateWordList() {
2094            const terms = getCurrentTerms();
2095            $wordList.empty();
2096
2097            terms.forEach((term, index) => {
2098                const $btn = $('<button>')
2099                    .addClass('btn-cybersecurity-glossary-word')
2100                    .addClass(index === currentTermIndex ? 'btn-cybersecurity-glossary-word-active' : '')
2101                    .attr('data-index', index)
2102                    .text(term.term);
2103                $wordList.append($btn);
2104            });
2105
2106            // Rebind word click events
2107            bindWordClickEvents();
2108        }
2109
2110        /**
2111         * Update cards with current term data (Soft Poker Card Animation)
2112         */
2113        function updateCards(direction) {
2114            const terms = getCurrentTerms();
2115            const totalTerms = terms.length;
2116            direction = direction || 'next';
2117
2118            const $frontCard = $cards.eq(0);
2119            const exitRotation = direction === 'next' ? -8 : 8;
2120            const exitX = direction === 'next' ? -60 : 60;
2121
2122            // Create timeline for smooth card swap
2123            const tl = gsap.timeline();
2124
2125            // Soft exit animation
2126            tl.to($frontCard, {
2127                rotation: exitRotation,
2128                x: exitX,
2129                opacity: 0,
2130                scale: 0.9,
2131                duration: 0.4,
2132                ease: 'power2.inOut'
2133            });
2134
2135            // Update content while card is transitioning
2136            tl.call(() => {
2137                $cards.each(function(cardIndex) {
2138                    const $card = $(this);
2139                    const termIndex = (currentTermIndex + cardIndex) % totalTerms;
2140                    const term = terms[termIndex];
2141
2142                    if (term) {
2143                        $card.find('.glossary-card-title').text(term.term);
2144                        $card.find('.glossary-card-description').text(term.description);
2145                        var $link = $card.find('.glossary-card-link');
2146                        if (term.link && term.link !== '/') {
2147                            $link.attr('href', term.link).show();
2148                        } else {
2149                            $link.hide();
2150                        }
2151                    }
2152                });
2153            });
2154
2155            // Reset front card for soft entry
2156            tl.set($frontCard, {
2157                rotation: direction === 'next' ? 8 : -8,
2158                x: direction === 'next' ? 60 : -60,
2159                scale: 0.9
2160            });
2161
2162            // Soft entry animation
2163            tl.to($frontCard, {
2164                rotation: 0,
2165                x: 0,
2166                opacity: 1,
2167                scale: 1,
2168                zIndex: 30,
2169                duration: 0.5,
2170                ease: 'power3.out'
2171            });
2172
2173            // Animate back cards smoothly
2174            tl.to($cards.eq(1), {
2175                scale: 0.97,
2176                rotation: 3,
2177                x: 8,
2178                opacity: 0.6,
2179                zIndex: 20,
2180                duration: 0.5,
2181                ease: 'power2.out'
2182            }, '-=0.4');
2183
2184            tl.to($cards.eq(2), {
2185                scale: 0.94,
2186                rotation: -3,
2187                x: -8,
2188                opacity: 0.4,
2189                zIndex: 10,
2190                duration: 0.5,
2191                ease: 'power2.out'
2192            }, '-=0.4');
2193        }
2194
2195        /**
2196         * Initial card setup (no animation, just position)
2197         */
2198        function initializeCards() {
2199            const terms = getCurrentTerms();
2200            const totalTerms = terms.length;
2201
2202            $cards.each(function(cardIndex) {
2203                const $card = $(this);
2204                const termIndex = (currentTermIndex + cardIndex) % totalTerms;
2205                const term = terms[termIndex];
2206
2207                if (term) {
2208                    $card.find('.glossary-card-title').text(term.term);
2209                    $card.find('.glossary-card-description').text(term.description);
2210                    var $link = $card.find('.glossary-card-link');
2211                    if (term.link && term.link !== '/') {
2212                        $link.attr('href', term.link).show();
2213                    } else {
2214                        $link.hide();
2215                    }
2216                }
2217            });
2218
2219            animateCards();
2220        }
2221
2222        /**
2223         * Animate card positions (Poker Card Style)
2224         */
2225        function animateCards() {
2226            $cards.each(function(index) {
2227                const $card = $(this);
2228                const position = index + 1;
2229
2230                // Reset classes
2231                $card.removeClass('glossary-card-active glossary-card-back-1 glossary-card-back-2');
2232
2233                if (position === 1) {
2234                    // Front card - no rotation, full size
2235                    $card.addClass('glossary-card-active');
2236                    gsap.to($card, {
2237                        scale: 1,
2238                        rotation: 0,
2239                        x: 0,
2240                        opacity: 1,
2241                        zIndex: 30,
2242                        duration: 0.5,
2243                        ease: 'back.out(1.2)'
2244                    });
2245                } else if (position === 2) {
2246                    // Back card 1 - rotated right (+5deg)
2247                    $card.addClass('glossary-card-back-1');
2248                    gsap.to($card, {
2249                        scale: 0.95,
2250                        rotation: 5,
2251                        x: 15,
2252                        opacity: 0.7,
2253                        zIndex: 20,
2254                        duration: 0.5,
2255                        ease: 'power2.out'
2256                    });
2257                } else {
2258                    // Back card 2 - rotated left (-5deg)
2259                    $card.addClass('glossary-card-back-2');
2260                    gsap.to($card, {
2261                        scale: 0.9,
2262                        rotation: -5,
2263                        x: -15,
2264                        opacity: 0.5,
2265                        zIndex: 10,
2266                        duration: 0.5,
2267                        ease: 'power2.out'
2268                    });
2269                }
2270            });
2271        }
2272
2273        /**
2274         * Update active states for word buttons
2275         */
2276        function updateWordActiveState() {
2277            $wordList.find('.btn-cybersecurity-glossary-word')
2278                .removeClass('btn-cybersecurity-glossary-word-active');
2279            $wordList.find(`[data-index="${currentTermIndex}"]`)
2280                .addClass('btn-cybersecurity-glossary-word-active');
2281        }
2282
2283        /**
2284         * Handle letter button click
2285         */
2286        function handleLetterClick(e) {
2287            const $btn = $(e.currentTarget);
2288            if ($btn.prop('disabled')) return;
2289
2290            const letter = $btn.data('letter');
2291            if (letter === currentLetter) return;
2292
2293            // Update active state
2294            $alphabetButtons.removeClass('btn-cybersecurity-glossary-active');
2295            $btn.addClass('btn-cybersecurity-glossary-active');
2296
2297            // Update current letter
2298            currentLetter = letter;
2299            currentTermIndex = 0;
2300
2301            // Update displays
2302            $currentLetterDisplay.text(letter);
2303            updateWordList();
2304            initializeCards(); // Use initialize for letter change (no swap animation)
2305
2306            // Update word dropdown text to first term of new letter
2307            const terms = getCurrentTerms();
2308            if (terms.length > 0) {
2309                updateWordDropdownText(terms[0].term);
2310            }
2311        }
2312
2313        /**
2314         * Handle word button click
2315         */
2316        function handleWordClick(e) {
2317            const $btn = $(e.currentTarget);
2318            const index = parseInt($btn.data('index'), 10);
2319
2320            if (index === currentTermIndex) {
2321                closeWordDropdown();
2322                return;
2323            }
2324
2325            const direction = index > currentTermIndex ? 'next' : 'prev';
2326            currentTermIndex = index;
2327            updateWordActiveState();
2328            updateCards(direction);
2329
2330            // Update dropdown text and close
2331            const terms = getCurrentTerms();
2332            if (terms[index]) {
2333                updateWordDropdownText(terms[index].term);
2334            }
2335            closeWordDropdown();
2336        }
2337
2338        /**
2339         * Handle previous button click
2340         */
2341        function handlePrevClick() {
2342            const terms = getCurrentTerms();
2343            if (terms.length === 0) return;
2344
2345            currentTermIndex = (currentTermIndex - 1 + terms.length) % terms.length;
2346            updateWordActiveState();
2347            updateCards('prev');
2348            updateWordDropdownText(terms[currentTermIndex].term);
2349        }
2350
2351        /**
2352         * Handle next button click
2353         */
2354        function handleNextClick() {
2355            const terms = getCurrentTerms();
2356            if (terms.length === 0) return;
2357
2358            currentTermIndex = (currentTermIndex + 1) % terms.length;
2359            updateWordActiveState();
2360            updateCards('next');
2361            updateWordDropdownText(terms[currentTermIndex].term);
2362        }
2363
2364        /**
2365         * Bind word click events (called after word list update)
2366         */
2367        function bindWordClickEvents() {
2368            $wordList.find('.btn-cybersecurity-glossary-word').on('click', handleWordClick);
2369        }
2370
2371        /**
2372         * Toggle word dropdown
2373         */
2374        function toggleWordDropdown() {
2375            $wordDropdownToggle.toggleClass('active');
2376            $wordList.toggleClass('open');
2377        }
2378
2379        /**
2380         * Close word dropdown
2381         */
2382        function closeWordDropdown() {
2383            $wordDropdownToggle.removeClass('active');
2384            $wordList.removeClass('open');
2385        }
2386
2387        /**
2388         * Update word dropdown toggle text
2389         */
2390        function updateWordDropdownText(termName) {
2391            $wordDropdownToggleText.text(termName);
2392        }
2393
2394        // Initialize event listeners
2395        $alphabetButtons.on('click', handleLetterClick);
2396        $prevBtn.on('click', handlePrevClick);
2397        $nextBtn.on('click', handleNextClick);
2398        $wordDropdownToggle.on('click', toggleWordDropdown);
2399        bindWordClickEvents();
2400
2401        // Initialize card positions (no animation on page load)
2402        initializeCards();
2403    }
2404
2405    /**
2406     * References Infinite Carousel
2407     * JS-based smooth infinite scroll with exact pixel calculation
2408     */
2409    function initReferencesCarousel() {
2410        var $track = $('.references-track');
2411        if (!$track.length) return;
2412
2413        var $imgs = $track.find('img');
2414        var totalImages = $imgs.length;
2415        var halfCount = totalImages / 2;
2416
2417        // Wait for images to load to get correct widths
2418        var loaded = 0;
2419        $imgs.each(function () {
2420            if (this.complete) {
2421                loaded++;
2422            } else {
2423                $(this).on('load', function () {
2424                    loaded++;
2425                    if (loaded >= totalImages) startScroll();
2426                });
2427            }
2428        });
2429        if (loaded >= totalImages) startScroll();
2430
2431        function startScroll() {
2432            if (!$imgs[halfCount]) return;
2433            // Measure the first-half width from the rendered layout instead
2434            // of summing widths + gaps — offsetLeft reflects the browser's
2435            // actual sub-pixel positioning, so the seamless loop point matches
2436            // the visible content even when logos have varying intrinsic sizes.
2437            var setWidth = $imgs[halfCount].offsetLeft - $imgs[0].offsetLeft;
2438
2439            // Re-measure on resize so the loop stays seamless when the layout
2440            // (e.g. responsive gap or wrapper width) changes.
2441            $(window).on('resize.referencesCarousel', function () {
2442                setWidth = $imgs[halfCount].offsetLeft - $imgs[0].offsetLeft;
2443            });
2444
2445            var pos = 0;
2446            var speed = 1; // pixels per frame
2447
2448            function animate() {
2449                pos -= speed;
2450                if (Math.abs(pos) >= setWidth) {
2451                    pos += setWidth;
2452                }
2453                $track[0].style.transform = 'translateX(' + pos + 'px)';
2454                requestAnimationFrame(animate);
2455            }
2456            requestAnimationFrame(animate);
2457        }
2458    }
2459
2460    /**
2461     * Customer Stories Vertical Swiper
2462     */
2463    function initCustomerStoriesSwiper() {
2464        if (!$('.customer-stories-swiper').length) return;
2465
2466        var $swiperEl = $('.customer-stories-swiper');
2467        var slideHeight = $swiperEl.find('.swiper-slide').first().outerHeight(true);
2468        $swiperEl.css('height', slideHeight + 'px');
2469
2470        new Swiper('.customer-stories-swiper', {
2471            direction: 'horizontal',
2472            slidesPerView: 1,
2473            spaceBetween: 0,
2474            loop: true,
2475            autoplay: {
2476                delay: 3000,
2477                disableOnInteraction: false,
2478            },
2479            navigation: {
2480                prevEl: '.customer-stories-prev',
2481                nextEl: '.customer-stories-next',
2482            },
2483        });
2484    }
2485
2486    /**
2487     * Partner Benefits Vertical Swiper
2488     */
2489    function initPartnerBenefitsSwiper() {
2490        if (!$('.partner-benefits-swiper').length) return;
2491
2492        $('.partner-benefits-swiper').each(function () {
2493            var $swiper = $(this);
2494            var swiperClass = $swiper.attr('class').split(' ').find(function (c) {
2495                return c.endsWith('-swiper');
2496            }) || 'partner-benefits-swiper';
2497
2498            new Swiper(this, {
2499                direction: 'horizontal',
2500                slidesPerView: 1,
2501                spaceBetween: 20,
2502                loop: true,
2503                autoplay: {
2504                    delay: 3000,
2505                    disableOnInteraction: false,
2506                },
2507                navigation: {
2508                    nextEl: '.' + swiperClass + '-next',
2509                    prevEl: '.' + swiperClass + '-prev',
2510                },
2511            });
2512        });
2513    }
2514
2515    /**
2516     * About Purpose Text Swiper
2517     */
2518    function initAboutPurposeSwiper() {
2519        if (!$('.about-purpose-swiper').length) return;
2520
2521        new Swiper('.about-purpose-swiper', {
2522            direction: 'horizontal',
2523            slidesPerView: 1,
2524            spaceBetween: 20,
2525            loop: true,
2526            navigation: {
2527                nextEl: '.about-purpose-swiper-next',
2528                prevEl: '.about-purpose-swiper-prev',
2529            },
2530        });
2531    }
2532
2533    /**
2534     * FAQ Accordion
2535     */
2536    function initFaqAccordion() {
2537        var $items = $('[data-faq-accordion]');
2538        if (!$items.length) return;
2539
2540        $items.find('[data-faq-accordion-toggle]').on('click', function() {
2541            var $currentItem = $(this).closest('[data-faq-accordion]');
2542            var isActive = $currentItem.hasClass('active');
2543
2544            if (isActive) return;
2545
2546            // Close all items
2547            $items.each(function() {
2548                $(this).removeClass('active');
2549                $(this).find('.faq-accordion-content').slideUp(300);
2550                $(this).find('.faq-accordion-icon-open').addClass('hidden');
2551                $(this).find('.faq-accordion-icon-closed').removeClass('hidden');
2552                $(this).find('h6').removeClass('text-tart-orange').addClass('text-navy-black');
2553            });
2554
2555            // Open clicked item
2556            $currentItem.addClass('active');
2557            $currentItem.find('.faq-accordion-content').slideDown(300);
2558            $currentItem.find('.faq-accordion-icon-open').removeClass('hidden');
2559            $currentItem.find('.faq-accordion-icon-closed').addClass('hidden');
2560            $currentItem.find('h6').removeClass('text-navy-black').addClass('text-tart-orange');
2561        });
2562    }
2563
2564    /**
2565     * Accordion
2566     */
2567    function initAccordion() {
2568        var $items = $('[data-accordion]');
2569        if (!$items.length) return;
2570
2571        $items.find('[data-accordion-toggle]').on('click', function() {
2572            var $currentItem = $(this).closest('[data-accordion]');
2573            var isActive = $currentItem.hasClass('active');
2574
2575            // Don't close if already active — one must always stay open
2576            if (isActive) return;
2577
2578            // Close all items in the same accordion group
2579            var $siblings = $currentItem.parent().find('[data-accordion]');
2580            $siblings.each(function() {
2581                $(this).removeClass('active');
2582                $(this).find('.accordion-content').slideUp(300, function() {
2583                    $(this).addClass('hidden');
2584                });
2585                $(this).find('.accordion-icon-open').addClass('hidden');
2586                $(this).find('.accordion-icon-closed').removeClass('hidden');
2587            });
2588
2589            // Open clicked item
2590            $currentItem.addClass('active');
2591            $currentItem.find('.accordion-content').removeClass('hidden').hide().slideDown(300);
2592            $currentItem.find('.accordion-icon-open').removeClass('hidden');
2593            $currentItem.find('.accordion-icon-closed').addClass('hidden');
2594        });
2595    }
2596
2597    /**
2598     * Signal Steps hover
2599     */
2600    function initSignalSteps() {
2601        $('.signal-steps').each(function() {
2602            var $wrapper = $(this);
2603            var $steps = $wrapper.find('.signal-step');
2604
2605            $steps.on('mouseenter', function() {
2606                $steps.removeClass('active');
2607                $(this).addClass('active');
2608            });
2609        });
2610    }
2611
2612    initSignalSteps();
2613
2614})(jQuery);
2615
2616// Incident Banner Close
2617(function () {
2618    document.addEventListener('DOMContentLoaded', function () {
2619        var bannerClose = document.getElementById('incident-banner-close');
2620        var banner = document.getElementById('incident-banner');
2621
2622        if (bannerClose && banner) {
2623            bannerClose.addEventListener('click', function () {
2624                banner.classList.add('hidden');
2625                var hero = document.querySelector('.bg-home-hero');
2626                if (hero) {
2627                    hero.style.marginTop = window.innerWidth >= 1024 ? '100px' : '88px';
2628                }
2629            });
2630        }
2631    });
2632}());
2633
2634// Incident Report Popup
2635(function () {
2636    document.addEventListener('DOMContentLoaded', function () {
2637        var trigger = document.getElementById('incident-report-trigger');
2638        var popup = document.getElementById('incident-report-popup');
2639        var backdrop = document.getElementById('incident-report-backdrop');
2640        var closeBtn = document.getElementById('incident-report-close');
2641
2642        function openPopup() {
2643            popup.classList.remove('hidden');
2644            popup.classList.add('flex');
2645            document.body.style.overflow = 'hidden';
2646        }
2647
2648        function closePopup() {
2649            popup.classList.add('hidden');
2650            popup.classList.remove('flex');
2651            document.body.style.overflow = '';
2652        }
2653
2654        if (trigger) trigger.addEventListener('click', openPopup);
2655        if (closeBtn) closeBtn.addEventListener('click', closePopup);
2656        if (backdrop) backdrop.addEventListener('click', closePopup);
2657
2658        document.addEventListener('keydown', function (e) {
2659            if (e.key === 'Escape') closePopup();
2660        });
2661    });
2662
2663    // Product Header Radar Effect
2664    function initProductRadar() {
2665        const container = document.querySelector('.product-header');
2666        if (!container) return;
2667
2668        // Remove old dots on resize
2669        const oldDots = container.querySelectorAll('.product-radar-dot');
2670        oldDots.forEach(dot => dot.remove());
2671
2672        const width = container.clientWidth;
2673        const height = container.clientHeight;
2674        const GRID_SIZE = 70;
2675
2676        const cols = Math.floor(width / GRID_SIZE);
2677        const rows = Math.floor(height / GRID_SIZE);
2678
2679        const dots = [];
2680
2681        for (let i = 0; i <= cols; i++) {
2682            for (let j = 0; j <= rows; j++) {
2683                const dot = document.createElement('div');
2684                dot.className = 'product-radar-dot';
2685                dot.style.left = (i * GRID_SIZE) + 'px';
2686                dot.style.top = (j * GRID_SIZE) + 'px';
2687                dot.style.transitionDelay = (Math.random() * 0.3).toFixed(2) + 's';
2688                container.appendChild(dot);
2689                dots.push(dot);
2690            }
2691        }
2692
2693        const exactThird = Math.max(1, Math.floor(dots.length / 3));
2694
2695        // Randomize array
2696        const shuffled = [...dots].sort(() => 0.5 - Math.random());
2697        let activeDots = shuffled.slice(0, exactThird);
2698        let inactiveDots = shuffled.slice(exactThird);
2699
2700        activeDots.forEach(d => d.classList.add('active'));
2701
2702        // Twinkle effect interval
2703        if (window.productRadarInterval) clearInterval(window.productRadarInterval);
2704
2705        window.productRadarInterval = setInterval(() => {
2706            const swapCount = Math.max(1, Math.floor(activeDots.length * 0.25));
2707
2708            // Pick random active dots to sleep
2709            activeDots.sort(() => 0.5 - Math.random());
2710            const toDeactivate = activeDots.splice(0, swapCount);
2711
2712            // Pick random inactive dots to wake up
2713            inactiveDots.sort(() => 0.5 - Math.random());
2714            const toActivate = inactiveDots.splice(0, swapCount);
2715
2716            toDeactivate.forEach(d => {
2717                d.classList.remove('active');
2718                inactiveDots.push(d);
2719            });
2720
2721            toActivate.forEach(d => {
2722                d.classList.add('active');
2723                activeDots.push(d);
2724            });
2725
2726        }, 1500);
2727    }
2728
2729    if (document.readyState === 'complete') {
2730        initProductRadar();
2731    } else {
2732        window.addEventListener('load', initProductRadar);
2733    }
2734
2735    let productRadarResizeTimer;
2736    window.addEventListener('resize', () => {
2737        clearTimeout(productRadarResizeTimer);
2738        productRadarResizeTimer = setTimeout(initProductRadar, 400);
2739    });
2740
2741    // EASM Radar Animation
2742    var easmRadarState = null;
2743
2744    function initEasmRadar() {
2745        const canvas = document.getElementById('easmRadarCanvas');
2746        if (!canvas) return;
2747
2748        const container = document.getElementById('easmRadarContainer');
2749        const ctx = canvas.getContext('2d');
2750
2751        const colors = {
2752            bg: '#000313',
2753            red: '#FF5247',
2754            pinkDark: '#F3716E',
2755            pinkLight: '#F8B5AC',
2756            grid: 'rgba(248, 181, 172, 0.5)'
2757        };
2758
2759        const scanSpeed = 0.01;
2760
2761        // Preserve state across resizes
2762        if (!easmRadarState) {
2763            easmRadarState = {
2764                scanAngle: 0,
2765                pointRatios: []
2766            };
2767            const pointCount = 15;
2768            for (let i = 0; i < pointCount; i++) {
2769                easmRadarState.pointRatios.push({
2770                    rRatio: Math.random() * 0.8 + 0.1,
2771                    theta: Math.random() * Math.PI * 2,
2772                    alpha: 0,
2773                    sizeBase: 3 + (Math.random() * 1)
2774                });
2775            }
2776        }
2777
2778        function handleResize() {
2779            const dpr = window.devicePixelRatio || 1;
2780            const size = container.offsetWidth || 280;
2781
2782            canvas.width = size * dpr;
2783            canvas.height = size * dpr;
2784            ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
2785        }
2786
2787        handleResize();
2788
2789        function getSize() {
2790            return container.offsetWidth || 280;
2791        }
2792
2793        function drawRadarGrid() {
2794            const size = getSize();
2795            const cx = size / 2;
2796            const cy = size / 2;
2797            const radarRadius = (size / 2) - 10;
2798
2799            ctx.fillStyle = colors.bg;
2800            ctx.fillRect(0, 0, size, size);
2801
2802            ctx.strokeStyle = colors.grid;
2803            ctx.lineWidth = 1;
2804            for (let i = 1; i <= 4; i++) {
2805                ctx.beginPath();
2806                ctx.arc(cx, cy, (radarRadius / 4) * i, 0, Math.PI * 2);
2807                ctx.stroke();
2808            }
2809
2810            ctx.beginPath();
2811            ctx.moveTo(cx, cy - radarRadius);
2812            ctx.lineTo(cx, cy + radarRadius);
2813            ctx.moveTo(cx - radarRadius, cy);
2814            ctx.lineTo(cx + radarRadius, cy);
2815            ctx.stroke();
2816        }
2817
2818        function drawPoints() {
2819            const size = getSize();
2820            const cx = size / 2;
2821            const cy = size / 2;
2822            const radarRadius = (size / 2) - 10;
2823
2824            easmRadarState.pointRatios.forEach(p => {
2825                const r = p.rRatio * (radarRadius - 20) + 10;
2826                let normalizedScanAngle = easmRadarState.scanAngle % (Math.PI * 2);
2827                if (normalizedScanAngle < 0) normalizedScanAngle += Math.PI * 2;
2828
2829                let diff = normalizedScanAngle - p.theta;
2830
2831                if (diff > 0 && diff < scanSpeed * 2) {
2832                    p.alpha = 1;
2833                } else if (diff < -Math.PI * 1.8) {
2834                    p.alpha = 1;
2835                }
2836
2837                p.alpha *= 0.975;
2838
2839                const x = cx + r * Math.cos(p.theta);
2840                const y = cy + r * Math.sin(p.theta);
2841
2842                ctx.save();
2843
2844                if (p.alpha > 0.1) {
2845                    ctx.beginPath();
2846                    ctx.arc(x, y, p.sizeBase, 0, Math.PI * 2);
2847                    ctx.fillStyle = 'rgba(255, 82, 71, ' + p.alpha + ')';
2848                    ctx.shadowBlur = 0;
2849                    ctx.shadowColor = 'rgba(243, 113, 110, ' + (p.alpha * 0.8) + ')';
2850                    ctx.fill();
2851                } else {
2852                    ctx.beginPath();
2853                    ctx.arc(x, y, p.sizeBase - 1, 0, Math.PI * 2);
2854                    ctx.fillStyle = 'rgba(248, 181, 172, 0.3)';
2855                    ctx.shadowBlur = 0;
2856                    ctx.fill();
2857                }
2858
2859                ctx.restore();
2860            });
2861        }
2862
2863        function drawScanner() {
2864            const size = getSize();
2865            const cx = size / 2;
2866            const cy = size / 2;
2867            const radarRadius = (size / 2) - 10;
2868
2869            const tailAngle = Math.PI / 2;
2870            const grad = ctx.createConicGradient(easmRadarState.scanAngle - tailAngle, cx, cy);
2871
2872            grad.addColorStop(0, 'rgba(255, 82, 71, 0)');
2873            grad.addColorStop(0.25, 'rgba(255, 82, 71, 0.4)');
2874            grad.addColorStop(0.2501, 'rgba(0, 0, 0, 0)');
2875            grad.addColorStop(1, 'rgba(0, 0, 0, 0)');
2876
2877            ctx.beginPath();
2878            ctx.arc(cx, cy, radarRadius, 0, Math.PI * 2);
2879            ctx.fillStyle = grad;
2880            ctx.fill();
2881
2882            ctx.beginPath();
2883            ctx.moveTo(cx, cy);
2884            const pointerX = cx + Math.cos(easmRadarState.scanAngle) * radarRadius;
2885            const pointerY = cy + Math.sin(easmRadarState.scanAngle) * radarRadius;
2886            ctx.lineTo(pointerX, pointerY);
2887            ctx.strokeStyle = colors.red;
2888            ctx.lineWidth = 2;
2889            ctx.stroke();
2890        }
2891
2892        function animate() {
2893            drawRadarGrid();
2894            drawPoints();
2895            drawScanner();
2896            easmRadarState.scanAngle += scanSpeed;
2897            easmRadarState.animFrameId = requestAnimationFrame(animate);
2898        }
2899
2900        // Cancel any previous animation loop
2901        if (easmRadarState.animFrameId) {
2902            cancelAnimationFrame(easmRadarState.animFrameId);
2903        }
2904
2905        animate();
2906
2907        if (!easmRadarState.resizeAttached) {
2908            let easmResizeTimer;
2909            window.addEventListener('resize', () => {
2910                clearTimeout(easmResizeTimer);
2911                easmResizeTimer = setTimeout(() => {
2912                    handleResize();
2913                }, 400);
2914            });
2915            easmRadarState.resizeAttached = true;
2916        }
2917    }
2918
2919    if (document.readyState === 'complete') {
2920        initEasmRadar();
2921    } else {
2922        window.addEventListener('load', initEasmRadar);
2923    }
2924
2925    // CTI Header Animation
2926    function initCtiHeader() {
2927        var animationContainer = document.getElementById('ctiAnimationContainer');
2928        if (!animationContainer) return;
2929
2930        var canvas = document.createElement('canvas');
2931        canvas.style.position = 'absolute';
2932        canvas.style.top = '0';
2933        canvas.style.left = '0';
2934        canvas.style.width = '100%';
2935        canvas.style.height = '100%';
2936        animationContainer.appendChild(canvas);
2937        var ctx = canvas.getContext('2d');
2938
2939        var waveCanvas = document.getElementById('ctiWaveCanvas');
2940        var waveCtx = waveCanvas.getContext('2d');
2941        var waveWidth, waveHeight;
2942
2943        var cWidth, cHeight;
2944        function handleResize() {
2945            var dpr = window.devicePixelRatio || 1;
2946
2947            cWidth = animationContainer.clientWidth;
2948            cHeight = animationContainer.clientHeight;
2949            canvas.width = cWidth * dpr;
2950            canvas.height = cHeight * dpr;
2951            ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
2952
2953            waveWidth = waveCanvas.parentElement.clientWidth;
2954            waveHeight = waveCanvas.parentElement.clientHeight;
2955            waveCanvas.width = waveWidth * dpr;
2956            waveCanvas.height = waveHeight * dpr;
2957            waveCtx.setTransform(dpr, 0, 0, dpr, 0, 0);
2958        }
2959
2960        window.addEventListener('resize', handleResize);
2961        handleResize();
2962
2963        var baseSize = 676;
2964        var ringRatios = [
2965            { ratio: 237 / baseSize, stroke: '#53281D' },
2966            { ratio: 385 / baseSize, stroke: '#351C16' },
2967            { ratio: 529 / baseSize, stroke: '#1D1110' },
2968            { ratio: 676 / baseSize, stroke: '#150E0F' }
2969        ];
2970
2971        var lineCount = 12;
2972        var r3Ratio = ringRatios[2].ratio / 2;
2973
2974        var nodes = [];
2975        for (var i = 0; i < lineCount; i++) {
2976            var angle = i * ((Math.PI * 2) / lineCount);
2977            nodes.push({
2978                angle: angle,
2979                offsetRatio: 0,
2980                targetOffsetRatio: 0,
2981                speed: 0,
2982                movingIn: true,
2983                delay: 0,
2984                initAnimation: function () {
2985                    this.targetOffsetRatio = Math.random() * (25 / baseSize);
2986                    this.speed = (0.05 + Math.random() * 0.15) / baseSize;
2987                    this.movingIn = true;
2988                    this.delay = Math.floor(Math.random() * 60 * 2.5);
2989                },
2990                update: function () {
2991                    if (this.delay > 0) {
2992                        this.delay--;
2993                        return;
2994                    }
2995                    if (this.movingIn) {
2996                        this.offsetRatio += this.speed;
2997                        if (this.offsetRatio >= this.targetOffsetRatio) {
2998                            this.offsetRatio = this.targetOffsetRatio;
2999                            this.movingIn = false;
3000                        }
3001                    } else {
3002                        this.offsetRatio -= this.speed;
3003                        if (this.offsetRatio <= 0) {
3004                            this.offsetRatio = 0;
3005                            this.initAnimation();
3006                        }
3007                    }
3008                }
3009            });
3010            nodes[i].initAnimation();
3011        }
3012
3013        var time = 0;
3014        var waveLinesConfig = [
3015            { yOffset: -45, phase: 0, amp: 35, speed: 0.004, waveLength: 1500 },
3016            { yOffset: 0, phase: 2, amp: 45, speed: 0.003, waveLength: 1800 },
3017            { yOffset: 45, phase: 4, amp: 30, speed: 0.005, waveLength: 1300 }
3018        ];
3019
3020        function animateLoop() {
3021            time += 1;
3022
3023            // Wave background
3024            waveCtx.clearRect(0, 0, waveWidth, waveHeight);
3025            var waveCenterY = waveHeight / 2;
3026            waveCtx.lineWidth = 2;
3027            waveCtx.strokeStyle = '#1D1210';
3028
3029            waveLinesConfig.forEach(function (line) {
3030                waveCtx.beginPath();
3031                for (var x = 0; x <= waveWidth; x += 15) {
3032                    var a = (x / line.waveLength) * Math.PI * 2 + line.phase + (time * line.speed);
3033                    var y = waveCenterY + line.yOffset + Math.sin(a) * line.amp;
3034                    if (x === 0) waveCtx.moveTo(x, y);
3035                    else waveCtx.lineTo(x, y);
3036                }
3037                waveCtx.stroke();
3038            });
3039
3040            // Circular layers
3041            ctx.clearRect(0, 0, cWidth, cHeight);
3042            var centerX = cWidth / 2;
3043            var centerY = cHeight / 2;
3044            var currentSize = Math.min(cWidth, cHeight);
3045
3046            ctx.lineWidth = 2;
3047            ringRatios.forEach(function (ring) {
3048                ctx.beginPath();
3049                ctx.arc(centerX, centerY, (ring.ratio * currentSize) / 2, 0, Math.PI * 2);
3050                ctx.strokeStyle = ring.stroke;
3051                ctx.stroke();
3052            });
3053
3054            var r3px = r3Ratio * currentSize;
3055            ctx.lineWidth = 2;
3056            ctx.strokeStyle = '#1E0C11';
3057            for (var i = 0; i < lineCount; i++) {
3058                var a = i * ((Math.PI * 2) / lineCount);
3059                var endX = centerX + Math.cos(a) * r3px;
3060                var endY = centerY + Math.sin(a) * r3px;
3061                ctx.beginPath();
3062                ctx.moveTo(centerX, centerY);
3063                ctx.lineTo(endX, endY);
3064                ctx.stroke();
3065            }
3066
3067            var nodeSizePx = Math.max(4, (15 / baseSize) * currentSize) / 2;
3068            nodes.forEach(function (node) {
3069                node.update();
3070                var nodeRadius = (r3Ratio - node.offsetRatio) * currentSize;
3071                var nx = centerX + Math.cos(node.angle) * nodeRadius;
3072                var ny = centerY + Math.sin(node.angle) * nodeRadius;
3073                ctx.beginPath();
3074                ctx.arc(nx, ny, nodeSizePx, 0, Math.PI * 2);
3075                ctx.fillStyle = '#CC3F40';
3076                ctx.fill();
3077            });
3078
3079            var centerDotPx = Math.max(8, (33 / baseSize) * currentSize) / 2;
3080            ctx.beginPath();
3081            ctx.arc(centerX, centerY, centerDotPx, 0, Math.PI * 2);
3082            ctx.fillStyle = '#EC5E45';
3083            ctx.fill();
3084
3085            requestAnimationFrame(animateLoop);
3086        }
3087
3088        animateLoop();
3089    }
3090
3091    if (document.readyState === 'complete') {
3092        initCtiHeader();
3093    } else {
3094        window.addEventListener('load', initCtiHeader);
3095    }
3096
3097    // DRPS Header Radar Animation
3098    var drpsRadarState = null;
3099
3100    function initDrpsRadar() {
3101        var canvas = document.getElementById('drpsRadarCanvas');
3102        if (!canvas) return;
3103
3104        var radarContainer = document.getElementById('drpsRadarContainer');
3105        var ctx = canvas.getContext('2d');
3106
3107        var colors = {
3108            red: '#FF4D4D',
3109            darkRed: 'rgba(202, 62, 63, 0.5)',
3110            grid: '#260D13'
3111        };
3112
3113        var scanSpeed = 0.01;
3114
3115        // Preserve state across resizes
3116        if (!drpsRadarState) {
3117            drpsRadarState = {
3118                scanAngle: 0,
3119                // Store point positions as ratios (0-1) so they survive resize
3120                pointRatios: []
3121            };
3122            var pointCount = 8;
3123            for (var i = 0; i < pointCount; i++) {
3124                drpsRadarState.pointRatios.push({
3125                    rRatio: Math.random() * 0.8 + 0.1,
3126                    theta: Math.random() * Math.PI * 2,
3127                    alpha: 0
3128                });
3129            }
3130        }
3131
3132        function handleResize() {
3133            var dpr = window.devicePixelRatio || 1;
3134            var size = radarContainer.offsetWidth || 280;
3135
3136            canvas.width = size * dpr;
3137            canvas.height = size * dpr;
3138            ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
3139        }
3140
3141        handleResize();
3142
3143        function getSize() {
3144            return radarContainer.offsetWidth || 280;
3145        }
3146
3147        function drawRadarGrid() {
3148            var size = getSize();
3149            var cx = size / 2;
3150            var cy = size / 2;
3151            var radarRadius = (size / 2) - 10;
3152
3153            ctx.clearRect(0, 0, size, size);
3154
3155            ctx.strokeStyle = colors.grid;
3156            ctx.lineWidth = 1;
3157            for (var i = 1; i <= 4; i++) {
3158                ctx.beginPath();
3159                ctx.arc(cx, cy, (radarRadius / 4) * i, 0, Math.PI * 2);
3160                ctx.stroke();
3161            }
3162
3163            ctx.beginPath();
3164            ctx.moveTo(cx, cy - radarRadius);
3165            ctx.lineTo(cx, cy + radarRadius);
3166            ctx.moveTo(cx - radarRadius, cy);
3167            ctx.lineTo(cx + radarRadius, cy);
3168            ctx.stroke();
3169        }
3170
3171        function drawPoints() {
3172            var size = getSize();
3173            var cx = size / 2;
3174            var cy = size / 2;
3175            var radarRadius = (size / 2) - 10;
3176            var pointSize = Math.max(3, 5 * (size / 500));
3177
3178            drpsRadarState.pointRatios.forEach(function (p) {
3179                var r = p.rRatio * (radarRadius - 20) + 10;
3180                var normalizedScanAngle = drpsRadarState.scanAngle % (Math.PI * 2);
3181                if (normalizedScanAngle < 0) normalizedScanAngle += Math.PI * 2;
3182
3183                var diff = normalizedScanAngle - p.theta;
3184
3185                if (diff > 0 && diff < scanSpeed * 2.5) {
3186                    p.alpha = 1;
3187                } else if (diff < -Math.PI * 1.8) {
3188                    p.alpha = 1;
3189                }
3190
3191                p.alpha *= 0.96;
3192
3193                var x = cx + r * Math.cos(p.theta);
3194                var y = cy + r * Math.sin(p.theta);
3195
3196                ctx.beginPath();
3197                ctx.arc(x, y, pointSize, 0, Math.PI * 2);
3198                ctx.fillStyle = colors.darkRed;
3199                ctx.fill();
3200
3201                if (p.alpha > 0.05) {
3202                    ctx.save();
3203                    ctx.beginPath();
3204                    ctx.arc(x, y, pointSize, 0, Math.PI * 2);
3205                    ctx.fillStyle = 'rgba(255, 77, 77, ' + p.alpha + ')';
3206                    ctx.shadowBlur = 10;
3207                    ctx.shadowColor = 'rgba(255, 77, 77, ' + p.alpha + ')';
3208                    ctx.fill();
3209                    ctx.restore();
3210                }
3211            });
3212        }
3213
3214        function drawScanner() {
3215            var size = getSize();
3216            var cx = size / 2;
3217            var cy = size / 2;
3218            var radarRadius = (size / 2) - 10;
3219
3220            var tailAngle = Math.PI / 2;
3221            var grad = ctx.createConicGradient(drpsRadarState.scanAngle - tailAngle, cx, cy);
3222
3223            grad.addColorStop(0, 'rgba(255, 77, 77, 0)');
3224            grad.addColorStop(0.25, 'rgba(255, 77, 77, 0.2)');
3225            grad.addColorStop(0.2501, 'rgba(0, 0, 0, 0)');
3226            grad.addColorStop(1, 'rgba(0, 0, 0, 0)');
3227
3228            ctx.beginPath();
3229            ctx.arc(cx, cy, radarRadius, 0, Math.PI * 2);
3230            ctx.fillStyle = grad;
3231            ctx.fill();
3232
3233            ctx.beginPath();
3234            ctx.moveTo(cx, cy);
3235            var pointerX = cx + Math.cos(drpsRadarState.scanAngle) * radarRadius;
3236            var pointerY = cy + Math.sin(drpsRadarState.scanAngle) * radarRadius;
3237            ctx.lineTo(pointerX, pointerY);
3238            ctx.strokeStyle = colors.red;
3239            ctx.lineWidth = 2;
3240            ctx.stroke();
3241        }
3242
3243        function drawCenterPoint() {
3244            var size = getSize();
3245            var cx = size / 2;
3246            var cy = size / 2;
3247            var centerSize = Math.max(5, 8 * (size / 500));
3248
3249            ctx.beginPath();
3250            ctx.arc(cx, cy, centerSize, 0, Math.PI * 2);
3251            ctx.fillStyle = colors.red;
3252            ctx.fill();
3253        }
3254
3255        function animate() {
3256            drawRadarGrid();
3257            drawPoints();
3258            drawScanner();
3259            drawCenterPoint();
3260            drpsRadarState.scanAngle += scanSpeed;
3261            drpsRadarState.animFrameId = requestAnimationFrame(animate);
3262        }
3263
3264        // Cancel any previous animation loop to avoid stacking
3265        if (drpsRadarState.animFrameId) {
3266            cancelAnimationFrame(drpsRadarState.animFrameId);
3267        }
3268
3269        animate();
3270
3271        // Store resize handler ref to avoid duplicate listeners
3272        if (!drpsRadarState.resizeAttached) {
3273            var drpsResizeTimer;
3274            window.addEventListener('resize', function () {
3275                clearTimeout(drpsResizeTimer);
3276                drpsResizeTimer = setTimeout(function () {
3277                    handleResize();
3278                }, 400);
3279            });
3280            drpsRadarState.resizeAttached = true;
3281        }
3282    }
3283
3284    if (document.readyState === 'complete') {
3285        initDrpsRadar();
3286    } else {
3287        window.addEventListener('load', initDrpsRadar);
3288    }
3289}());
3290
3291/* Solutions Header — Matrix & Hero Canvas */
3292(function () {
3293    const mc = document.getElementById('matrix-canvas');
3294    if (!mc) return;
3295    const ctx = mc.getContext('2d');
3296    const parent = mc.parentElement;
3297    let W = parent.offsetWidth, H = parent.offsetHeight;
3298    mc.width = W;
3299    mc.height = H;
3300    const cols = Math.floor(W / 16), drops = Array(cols).fill(1);
3301    const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789@#$%^&*()_+-=[]{}|;:<>?./\\アイウエオカキクケコ';
3302    function dmx() {
3303        ctx.fillStyle = 'rgba(2,4,10,0.05)';
3304        ctx.fillRect(0, 0, W, H);
3305        ctx.fillStyle = '#ff4d4d';
3306        ctx.font = '12px monospace';
3307        drops.forEach((y, i) => {
3308            const c = chars[Math.floor(Math.random() * chars.length)];
3309            ctx.fillText(c, i * 16, y * 16);
3310            if (y * 16 > H && Math.random() > 0.975) drops[i] = 0;
3311            drops[i]++;
3312        });
3313    }
3314    setInterval(dmx, 50);
3315    window.addEventListener('resize', () => {
3316        W = parent.offsetWidth;
3317        H = parent.offsetHeight;
3318        mc.width = W;
3319        mc.height = H;
3320    });
3321})();
3322
3323(function () {
3324    const hc = document.getElementById('hero-canvas');
3325    if (!hc) return;
3326    const ctx = hc.getContext('2d');
3327    let W = hc.parentElement.offsetWidth, H = hc.parentElement.offsetHeight;
3328    hc.width = W;
3329    hc.height = H;
3330    const nodes = Array.from({ length: 40 }, () => ({
3331        x: Math.random() * W,
3332        y: Math.random() * H,
3333        vx: (Math.random() - .5) * .4,
3334        vy: (Math.random() - .5) * .4,
3335        r: Math.random() * 2 + 1
3336    }));
3337    function dh() {
3338        ctx.clearRect(0, 0, W, H);
3339        nodes.forEach(n => {
3340            n.x += n.vx;
3341            n.y += n.vy;
3342            if (n.x < 0 || n.x > W) n.vx *= -1;
3343            if (n.y < 0 || n.y > H) n.vy *= -1;
3344            ctx.beginPath();
3345            ctx.arc(n.x, n.y, n.r, 0, Math.PI * 2);
3346            ctx.fillStyle = 'rgba(255,77,77,.6)';
3347            ctx.fill();
3348        });
3349        for (let i = 0; i < nodes.length; i++)
3350            for (let j = i + 1; j < nodes.length; j++) {
3351                const dx = nodes[i].x - nodes[j].x,
3352                    dy = nodes[i].y - nodes[j].y,
3353                    d = Math.sqrt(dx * dx + dy * dy);
3354                if (d < 120) {
3355                    ctx.beginPath();
3356                    ctx.moveTo(nodes[i].x, nodes[i].y);
3357                    ctx.lineTo(nodes[j].x, nodes[j].y);
3358                    ctx.strokeStyle = `rgba(255,77,77,${.15 * (1 - d / 120)})`;
3359                    ctx.lineWidth = .5;
3360                    ctx.stroke();
3361                }
3362            }
3363        requestAnimationFrame(dh);
3364    }
3365    dh();
3366    window.addEventListener('resize', () => {
3367        W = hc.parentElement.offsetWidth;
3368        H = hc.parentElement.offsetHeight;
3369        hc.width = W;
3370        hc.height = H;
3371    });
3372})();
3373
3374/* Takedown Engine Log Cycling */
3375(function () {
3376    var e1 = document.getElementById('td-line1');
3377    var e2 = document.getElementById('td-line2');
3378    var e3 = document.getElementById('td-line3');
3379    if (!e1 || !e2 || !e3) return;
3380    var tdLogs = [
3381        { l1: 'case_open: phishing domain brandefense-login[.]com', l2: 'registrar_notified: namecheap.com — abuse report filed', l3: 'status: DOMAIN_SUSPENDED in 2.4h' },
3382        { l1: 'case_open: fake_app brandefense-mobile[.]apk detected', l2: 'store_notified: Google Play — removal requested', l3: 'status: APP_REMOVED in 1.1h' },
3383        { l1: 'case_open: impersonation @brandefense_support on X', l2: 'platform_escalated: X Trust & Safety — report filed', l3: 'status: ACCOUNT_SUSPENDED in 3.7h' },
3384        { l1: 'case_open: BIN_4532** listed on dark marketplace', l2: 'fraud_feed_distributed: payment processor notified', l3: 'status: CARDS_BLOCKED in 0.8h' }
3385    ];
3386    var tdIdx = 0;
3387    setInterval(function () {
3388        tdIdx = (tdIdx + 1) % tdLogs.length;
3389        var d = tdLogs[tdIdx];
3390        e1.textContent = '> ' + d.l1;
3391        e2.textContent = '> ' + d.l2;
3392        e3.textContent = '> ' + d.l3;
3393    }, 4000);
3394})();
3395
3396/* Hero Terminal Fade-in (anime.js) */
3397(function () {
3398    if (typeof anime === 'undefined') return;
3399    if (!document.getElementById('hero-term')) return;
3400    anime({ targets: '#hero-term', opacity: [0, 1], translateY: [10, 0], duration: 500, easing: 'easeOutExpo' });
3401    ['#tl1', '#tl2', '#tl3', '#tl4', '#tl5', '#tl6'].forEach((id, i) => {
3402        const el = document.querySelector(id);
3403        if (!el) return;
3404        setTimeout(() => {
3405            anime({ targets: el, opacity: [0, 1], duration: 300 });
3406        }, 900 + i * 500);
3407    });
3408})();
3409
3410/* Reports Page filtering (dropdown, search, categories) is handled by initReportsFilter() via AJAX. */
3411
3412/* Hash anchor scroll fix — re-scroll after full page load */
3413(function () {
3414    if (!window.location.hash) return;
3415    window.addEventListener('load', function () {
3416        var el = document.querySelector(window.location.hash);
3417        if (el) {
3418            setTimeout(function () {
3419                el.scrollIntoView({ behavior: 'smooth', block: 'start' });
3420            }, 300);
3421        }
3422    });
3423})();
3424
3425
3426// Hero Float Animation
3427(function() {
3428    if (typeof gsap === "undefined") return;
3429    document.querySelectorAll(".hero-float").forEach(function(el) {
3430        gsap.to(el, {
3431            y: -15,
3432            duration: 3,
3433            ease: "sine.inOut",
3434            repeat: -1,
3435            yoyo: true
3436        });
3437    });
3438})();
3439
3440// GSAP Reveal Animations
3441document.addEventListener("DOMContentLoaded", function() {
3442    if (typeof gsap === "undefined") return;
3443
3444    // Set initial state
3445    gsap.set(".reveal", { opacity: 0, y: 90});
3446
3447    // Adaptive trigger: short elements still wait until they're 50% visible
3448    // (so the motion lands inside the viewport), but elements taller than the
3449    // viewport (typical on mobile for cards/images) fire as soon as ~40% of
3450    // the viewport is filled by the element. Otherwise tall items wait until
3451    // they're fully on-screen, which on mobile means scrolling past them.
3452    var observer = new IntersectionObserver(function(entries) {
3453        entries.forEach(function(entry) {
3454            if (!entry.isIntersecting) return;
3455            var target = entry.target;
3456            var elHeight = target.getBoundingClientRect().height;
3457            var vh = window.innerHeight || document.documentElement.clientHeight;
3458            var required = elHeight > 0 ? Math.min(0.5, (vh * 0.4) / elHeight) : 0.5;
3459            if (entry.intersectionRatio >= required) {
3460                gsap.to(target, {
3461                    opacity: 1,
3462                    y: 0,
3463                    duration: .9,
3464                    ease: "power2.out"
3465                });
3466                observer.unobserve(target);
3467            }
3468        });
3469    }, { threshold: [0, 0.1, 0.2, 0.3, 0.4, 0.5] });
3470
3471    document.querySelectorAll(".reveal").forEach(function(el) {
3472        observer.observe(el);
3473    });
3474});

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.