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.