// ============================================================================= // Stony Brook Animations // Contains both fixes/tweaks and section-specific animations for the site // ============================================================================= // The below code logs the selected element to the console. // Add this at a global scope in your JS file, or within a DOMContentLoaded listener if preferred. // For simplicity, adding it globally for now. /* document.addEventListener('focusin', function(event) { console.log('Focused element:', event.target); // You can also log specific attributes if helpful: // console.log('Focused element ID:', event.target.id); // console.log('Focused element class list:', event.target.classList); }, true); // Using capture phase to catch it early, though bubbling (false) is usually fine too. */ // Check for reduced motion preference function prefersReducedMotion() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; } // ----------------------------------------------------------------------------- // FIXES & TWEAKS // Small adjustments to ensure smooth animation behavior // ----------------------------------------------------------------------------- // Fix: Prime the News-Corner-Arrow so the first roll-over animates smoothly // Without this fix, the first hover would snap instead of transition smoothly ;(function(){ function primeNewsArrows() { document.querySelectorAll('.news-corner-arrow').forEach(el => { // 1) Turn *off* the transition inline so the browser snaps to 50×50 el.style.transition = 'none'; // 2) Force a layout/read so it "sees" the 50×50 immediately void el.offsetWidth; // 3) Remove our inline override so the next width/height change uses CSS again el.style.removeProperty('transition'); }); } if (document.readyState === 'complete') { primeNewsArrows(); } else { window.addEventListener('load', primeNewsArrows); } })(); // ----------------------------------------------------------------------------- // SECTION-SPECIFIC ANIMATIONS // Custom animations for individual sections across the site // ----------------------------------------------------------------------------- // Animation: Belong Section Number Counters // Animates statistics to count up when scrolled into view ;(function(){ function handleBelongCounters() { // Pre-process all stat numbers to store their original text const statElements = document.querySelectorAll('.belong-stat .stat-number'); statElements.forEach(element => { // Store the original text as a data attribute element.setAttribute('data-original', element.textContent); }); function animateCounter(element, target, duration = 2000) { const start = 0; const increment = target / (duration / 16); // 60fps let current = start; // Get original text and extract parts const originalText = element.getAttribute('data-original'); const numberMatch = originalText.match(/[\d,]+/); if (!numberMatch) return; const prefix = originalText.substring(0, numberMatch.index); const suffix = originalText.substring(numberMatch.index + numberMatch[0].length); // Set initial state to 0 requestAnimationFrame(() => { element.textContent = `${prefix}0${suffix}`; // Start the animation in the next frame requestAnimationFrame(() => { const timer = setInterval(() => { current += increment; if (current >= target) { clearInterval(timer); current = target; element.textContent = originalText; // Ensure we end with exact original text return; } const formattedNumber = Math.floor(current).toLocaleString(); element.textContent = `${prefix}${formattedNumber}${suffix}`; }, 16); }); }); } // Create a new observer for each stat box function createObserver(statBox) { const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const statNumber = entry.target.querySelector('.stat-number'); if (statNumber) { const targetValue = parseInt(statNumber.getAttribute('data-target')); animateCounter(statNumber, targetValue); } observer.unobserve(entry.target); } }); }, { threshold: 0.5 }); observer.observe(statBox); } // Initialize counters document.querySelectorAll('.belong-stat').forEach(statBox => { createObserver(statBox); }); } if (document.readyState === 'complete') { handleBelongCounters(); } else { window.addEventListener('load', handleBelongCounters); } })(); // Animation: Path Section Scroll Animations // Handles fade and slide animations for elements in the Path section ;(function(){ function handlePathAnimations() { // Skip animations if user prefers reduced motion if (prefersReducedMotion()) { document.querySelectorAll('.animate-on-scroll').forEach(element => { element.classList.add('is-visible'); }); return; } const animatedElements = document.querySelectorAll('.animate-on-scroll'); function checkVisibility() { animatedElements.forEach(element => { const rect = element.getBoundingClientRect(); const delay = element.getAttribute('data-delay') || 0; // Element is considered visible when its top edge is 80% of the way up the viewport const isVisible = rect.top <= window.innerHeight * 0.8; if (isVisible && !element.classList.contains('is-visible')) { setTimeout(() => { element.classList.add('is-visible'); }, delay); } }); } // Check visibility on scroll window.addEventListener('scroll', () => { window.requestAnimationFrame(checkVisibility); }, { passive: true }); // Initial check checkVisibility(); } if (document.readyState === 'complete') { handlePathAnimations(); } else { window.addEventListener('load', handlePathAnimations); } })(); // Animation: Homepage Carousel Configuration // This section configures the Slick carousel for the homepage with responsive settings // - Implements left/right navigation arrows and dot indicators // - Configures responsive breakpoints for different screen sizes // - Handles accessibility with ARIA labels // - Adjusts navigation controls based on viewport width: // * Desktop (1280px+): Shows both arrows and dots // * Tablet/Mobile (992px-1279px): Shows only dots // * Small Tablet/Mobile (<992px): Shows only dots // Homepage Carousel document.addEventListener("DOMContentLoaded", function () { const $carousel = $('.slick-carousel'); function equalizeCarouselContentHeights($sliderInstance) { const $contentElements = $sliderInstance.find('.sb-hp-carousel-content'); if (!$contentElements.length) return; let maxHeight = 0; $contentElements.css('height', 'auto'); // Reset height to auto to get natural height $contentElements.each(function() { const currentHeight = $(this).outerHeight(); if (currentHeight > maxHeight) { maxHeight = currentHeight; } }); if (maxHeight > 0) { $contentElements.css('height', maxHeight + 'px'); } } $carousel.on('init setPosition breakpoint', function (event, slick) { equalizeCarouselContentHeights($(this)); }); $carousel.slick({ arrows: true, // Enable arrows by default dots: true, infinite: true, speed: 500, slidesToShow: 1, slidesToScroll: 1, prevArrow: '', nextArrow: '', adaptiveHeight: false, // Set to false to maintain consistent slider height autoplay: false, responsive: [ { breakpoint: 1280, // Settings for viewports *below* 1280px settings: { arrows: false, // Disable arrows for viewports narrower than 1280px dots: true } }, // For viewports 992px and below, arrows are already covered by the 1280 breakpoint setting arrows: false. // We can simplify by removing the redundant arrows: false here if it's simply inheriting. // However, explicitly setting it doesn't harm and makes intent clear for this specific breakpoint if other settings were different. { breakpoint: 992, settings: { arrows: false, dots: true } }, { breakpoint: 576, settings: { arrows: false, dots: true } } ] }); }); // Calendar Countdown Timer // - Displays a countdown to a target date (June 21, 2025) // - Updates every second with days, hours, minutes, seconds remaining // - Uses padded 2-digit numbers (e.g. "05" instead of "5") // - Handles countdown completion by stopping at 0 // - Requires HTML elements with class: // * .calendar-section__countdown - Container // * .number - Four elements for days/hours/mins/secs // Animation: Calendar Countdown Timer (function() { // Set your target date here (YYYY-MM-DDTHH:MM:SS format or any valid Date string) const d = new Date(); const y = d.getFullYear(); if(document.getElementsByClassName('date-text').length > 0){ var date = document.getElementsByClassName('date-text')[0].innerHTML.split('-')[0]; var targetDate = new Date(`${date}, ${y}`); } // Cache countdown number elements var countdown = document.querySelector('.calendar-section__countdown'); if (!countdown) return; var numbers = countdown.querySelectorAll('.number'); function updateCountdown() { var now = new Date(); var diff = targetDate - now; if (diff < 0) diff = 0; var days = Math.floor(diff / (1000 * 60 * 60 * 24)); var hours = Math.floor((diff / (1000 * 60 * 60)) % 24); var mins = Math.floor((diff / (1000 * 60)) % 60); var secs = Math.floor((diff / 1000) % 60); if (numbers.length >= 4) { numbers[0].textContent = String(days).padStart(2, '0'); numbers[1].textContent = String(hours).padStart(2, '0'); numbers[2].textContent = String(mins).padStart(2, '0'); numbers[3].textContent = String(secs).padStart(2, '0'); } } updateCountdown(); setInterval(updateCountdown, 1000); })(); // Animation: Scrolling Promo Boxes // Initializes and manages a Slick Slider for the '.scrolling-promo-slider' element. // Features: // - Single slide display with infinite looping, navigation arrows, and custom dots. // - Differentiated hover state management for arrows on touch vs. non-touch devices: // - Non-touch (Desktop): Hover states (.slick-arrow-hovered) are managed via mouseenter/mouseleave // and re-evaluated after slide changes based on mouse position. // - Touch: Arrow pointer-events are disabled during slide transitions and hover classes removed // after slide changes to prevent sticky hover states. // - Accessibility Enhancements: // * ARIA labels on navigation buttons (arrows and custom dots). // * Dynamic 'aria-live="polite"' attribute on the current slide for screen reader announcements. // * Custom paging function generates ARIA labels for dots using slide headings. // - Responsive: Adapts to window width (though specific responsive settings for arrows/dots are in SCSS/HTML structure). // - Smooth 500ms slide transition speed. // - `adaptiveHeight: false` to maintain consistent arrow appearance. // - `focusOnChange: false` to prevent unexpected focus jumps. // Required HTML structure: // * Container with class '.scrolling-promo-slider'. // * Individual slides with class '.promo-slide'. // * Slide headings with class '.promo-slide-heading' (used for custom dot ARIA labels). $(document).ready(function () { const $slider = $('.scrolling-promo-slider'); // Detect if the device primarily uses touch input. const isTouchDevice = window.matchMedia('(pointer: coarse)').matches; // Store current mouse coordinates globally within this scope for hover checks on desktop. let mouseX = 0; let mouseY = 0; $(document).on('mousemove', function(e) { mouseX = e.clientX; mouseY = e.clientY; }); // For touch devices, disable pointer events on arrows during slide transition // to prevent accidental clicks or missed taps while the slide is moving. if (isTouchDevice) { $slider.on('beforeChange', function(event, slick, currentSlide, nextSlide){ if (slick.$prevArrow) slick.$prevArrow.css('pointer-events', 'none'); if (slick.$nextArrow) slick.$nextArrow.css('pointer-events', 'none'); }); } // After a slide change, handle arrow states and ARIA attributes. $slider.on('afterChange', function(event, slick, currentSlide){ if (isTouchDevice) { // FOR TOUCH DEVICES ONLY: // Use a short timeout to ensure operations run after the slide transition completes. setTimeout(function() { // Re-enable pointer events on arrows. if (slick.$prevArrow) slick.$prevArrow.css('pointer-events', 'auto'); if (slick.$nextArrow) slick.$nextArrow.css('pointer-events', 'auto'); // Explicitly remove any JS-controlled hover class after a tap or slide // to avoid sticky hover states on touch devices. if (slick.$prevArrow) slick.$prevArrow.removeClass('slick-arrow-hovered'); if (slick.$nextArrow) slick.$nextArrow.removeClass('slick-arrow-hovered'); }, 50); // 50ms delay. } else { // FOR NON-TOUCH (DESKTOP) DEVICES: // After a slide change, re-check if the mouse is currently over an arrow // and apply/remove the .slick-arrow-hovered class accordingly. // This handles cases where the slide changes while the mouse is stationary over an arrow. setTimeout(function() { function checkAndApplyHover($arrow) { if ($arrow && $arrow.length) { $arrow.removeClass('slick-arrow-hovered'); // Ensure a clean state before checking. const rect = $arrow[0].getBoundingClientRect(); // Check if the last recorded mouse coordinates are within the arrow's bounds. if (mouseX >= rect.left && mouseX <= rect.right && mouseY >= rect.top && mouseY <= rect.bottom) { $arrow.addClass('slick-arrow-hovered'); } } } checkAndApplyHover(slick.$prevArrow); checkAndApplyHover(slick.$nextArrow); }, 50); // 50ms delay, allows DOM to settle and mouse position to be current. } // Accessibility: Update aria-live attribute for screen readers. // Remove from all slides first, then add to the newly current slide. $('.promo-slide').removeAttr('aria-live'); if (currentSlide !== undefined && currentSlide !== null && slick.$slides && currentSlide < slick.$slides.length) { $(slick.$slides[currentSlide]).attr('aria-live', 'polite'); } }); // On slider initialization. $slider.on('init', function(event, slick){ // For non-touch devices, set up mouseenter/mouseleave events to manage // the .slick-arrow-hovered class for custom styling. // This is not used for touch devices to avoid sticky hover states. if (!isTouchDevice) { function addHoverClass() { $(this).addClass('slick-arrow-hovered'); } function removeHoverClass() { $(this).removeClass('slick-arrow-hovered'); } if (slick.$prevArrow) { slick.$prevArrow.on('mouseenter', addHoverClass).on('mouseleave', removeHoverClass); } if (slick.$nextArrow) { slick.$nextArrow.on('mouseenter', addHoverClass).on('mouseleave', removeHoverClass); } } }); // Initialize the Slick slider with specified options. $slider.slick({ slidesToShow: 1, // Show one slide at a time. slidesToScroll: 1, // Scroll one slide at a time. dots: true, // Show dot indicators. arrows: true, // Show navigation arrows. infinite: true, // Loop slides infinitely. speed: 500, // Animation speed in milliseconds. adaptiveHeight: false, // IMPORTANT: Kept false to ensure consistent height for arrow positioning. // If true, slider height adjusts to each slide, which can make full-height arrows problematic. accessibility: true, // Enable Slick's built-in accessibility features. focusOnChange: false, // IMPORTANT: Prevents the slider from automatically focusing on the slide // after a change, which can be disorienting. Manual focus management is preferred if needed. respondTo: 'window', // Base responsiveness calculations on the window, not the slider container. prevArrow: '', // Custom previous arrow HTML. nextArrow: '', // Custom next arrow HTML. customPaging: function (slider, i) { // Function to generate custom HTML for dot navigation. // Get the heading text of the slide to use in the dot's ARIA label for better accessibility. const title = $(slider.$slides[i]).find('.promo-slide-heading').text(); return ``; } }); // Accessibility: Add initial aria-live="polite" to the first (current) slide on load. // This ensures the screen reader announces the first slide content when the page loads. if ($slider.hasClass('slick-initialized') && $slider.slick('getSlick').$slides.length > 0) { const initialSlideIndex = $slider.slick('slickCurrentSlide'); $($slider.slick('getSlick').$slides[initialSlideIndex]).attr('aria-live', 'polite'); } }); // Animation: Flip Cards // ============================================================================= // Animation: Flip Cards // ============================================================================= // This script handles the flip animation for cards with the class '.flip-card'. // It adds an 'is-flipped' class to the '.flip-card-inner' element on mouseenter // or focusin, and removes it on mouseleave or focusout. // The actual flip animation (rotation) is defined in the SCSS/CSS using the // 'is-flipped' class. ;(function() { function handleFlipCards() { const flipCards = document.querySelectorAll('.flip-card'); flipCards.forEach(card => { const innerCard = card.querySelector('.flip-card-inner'); if (!innerCard) { // If there's no inner card, skip this card return; } // More robust priming for the animation const computedStyle = window.getComputedStyle(innerCard); const originalTransition = computedStyle.transition; // Temporarily disable transitions to set initial state without animating innerCard.style.transition = 'none'; // Force reflow to apply the "no transition" style void innerCard.offsetWidth; // Explicitly set the transition to what it should be from CSS innerCard.style.transition = originalTransition; // Force reflow again to make the browser acknowledge the transition property void innerCard.offsetWidth; // In the next frame, remove the inline style so CSS takes full control requestAnimationFrame(() => { innerCard.style.removeProperty('transition'); }); // Function to add the 'is-flipped' class const flip = () => innerCard.classList.add('is-flipped'); // Function to remove the 'is-flipped' class const unflip = () => innerCard.classList.remove('is-flipped'); // Event listeners for hover (mouseenter/mouseleave) card.addEventListener('mouseenter', flip); card.addEventListener('mouseleave', unflip); // Event listeners for focus (focusin/focusout) // focusin bubbles, so it captures focus on children too card.addEventListener('focusin', flip); card.addEventListener('focusout', unflip); }); } // Run the function after all resources (including CSS) are loaded if (document.readyState === 'complete') { // If already loaded (e.g., script added dynamically after page load) handleFlipCards(); } else { window.addEventListener('load', handleFlipCards); } })(); // ============================================================================= // Nudge Flip Card Layout (to fix height percentage issues on resize) // ============================================================================= ;(function() { function nudgeFlipCardLayout() { // console.log('[NudgeLayout] Attempting to nudge .flip-card-inner layouts...'); document.querySelectorAll('.flip-card-inner').forEach(inner => { const originalInlineDisplay = inner.style.display; inner.style.display = 'none'; void inner.offsetHeight; // Force reflow inner.style.display = originalInlineDisplay; // Restore original inline display or remove if none was set // If originalInlineDisplay was empty, setting to empty string effectively removes the inline style, // allowing CSS-defined display (e.g., from Bootstrap or your own styles) to take effect. }); } // Expose this function to be callable by the height equalization logic if needed, // or integrate into its load/resize handlers directly. // For now, let's integrate directly into the handlers of equalizeCardTitleHeights module. // This function needs to be defined *before* the module that calls it if not namespaced/exported. // To keep things clean, let's make sure the equalize module calls it. // Or, better yet, ensure it runs in the same sequence. // For simplicity, this new module will also handle its own load/resize listeners // and then the equalizeCardTitleHeights module will run after. // This will be restructured slightly below. window.nudgeFlipCardLayout = nudgeFlipCardLayout; // Make it globally available for the next module })(); // ============================================================================= // Equalize Flip Card Front Title Heights // ============================================================================= // This script ensures that all .flip-card-front .card-title elements // that are visually in the same row have the same height. This handles // cases where text wrapping causes titles to have different natural heights. ;(function() { function getItemsPerRow() { const width = window.innerWidth; if (width >= 992) return 4; // Based on col-lg-3 (4 items: 12/3) if (width >= 768) return 2; // Based on col-md-6 (2 items: 12/6) return 1; // Based on col-12 (1 item) } function equalizeCardTitleHeights() { // console.log('[EQHeights] Attempting to equalize card title heights...'); const titles = document.querySelectorAll('.flip-card-front .card-title'); // console.log('[EQHeights] Found ' + titles.length + ' card titles.'); if (!titles.length) { return; } // Reset heights to auto to get natural heights first titles.forEach(title => { title.style.height = 'auto'; }); const itemsPerRow = getItemsPerRow(); // console.log('[EQHeights] Items per row based on viewport width (' + window.innerWidth + 'px): ' + itemsPerRow); const titlesByRow = {}; for (let i = 0; i < titles.length; i++) { const rowIndex = Math.floor(i / itemsPerRow); const rowKey = 'row_' + rowIndex; if (!titlesByRow[rowKey]) { titlesByRow[rowKey] = []; } titlesByRow[rowKey].push(titles[i]); } // console.log('[EQHeights] Titles grouped by calculated rows:', JSON.parse(JSON.stringify(titlesByRow))); // Log a deep copy for inspection // Equalize heights within each calculated row for (const rowKey in titlesByRow) { const rowTitles = titlesByRow[rowKey]; let maxHeight = 0; let isRowReliable = true; // Assume reliable until proven otherwise if (rowTitles.length > 0) { rowTitles.forEach(title => { const titleStyles = window.getComputedStyle(title); if (titleStyles.display === 'none') { // If explicitly display:none, it won't have a meaningful height for equalization // but doesn't necessarily make the whole row unreliable if others are visible. // However, for simplicity in this pass, let's consider it a sign of unreliability. isRowReliable = false; return; // Early exit from this forEach iteration } if (title.offsetHeight === 0) { isRowReliable = false; // Mark row as unreliable if any item has 0 height return; // Early exit from this forEach iteration } if (title.offsetHeight > maxHeight) { maxHeight = title.offsetHeight; } }); if (!isRowReliable) { // console.log('[EQHeights] Row ' + rowKey + ': Contains items with 0 offsetHeight or display:none. Removing inline height style from all items in this row.'); rowTitles.forEach(title => { title.style.removeProperty('height'); }); } else if (maxHeight > 0) { if (rowTitles.length > 1) { // console.log('[EQHeights] Row ' + rowKey + ': Max height = ' + maxHeight + 'px. Applying to ' + rowTitles.length + ' titles.'); rowTitles.forEach(title => { title.style.height = maxHeight + 'px'; }); } else { // console.log('[EQHeights] Row ' + rowKey + ': Single reliable item, natural height = ' + maxHeight + 'px.'); // For single reliable item rows, their height was reset to auto, so natural height is fine. } } else { // This case (maxHeight is 0 but row was deemed reliable) should ideally not happen. // If it does, it means all items had >0 height but somehow maxHeight ended up 0, which is odd. // Default to removing height to be safe. // console.log('[EQHeights] Row ' + rowKey + ': Max height is 0 despite items appearing reliable. Removing inline height styles as a fallback.'); rowTitles.forEach(title => { title.style.removeProperty('height'); }); } } } } // Run on load and on resize with debouncing if (document.readyState === 'complete') { setTimeout(() => { if (window.nudgeFlipCardLayout) window.nudgeFlipCardLayout(); setTimeout(equalizeCardTitleHeights, 10); // Run equalization after a brief pause for nudge }, 50); } else { window.addEventListener('load', () => { setTimeout(() => { if (window.nudgeFlipCardLayout) window.nudgeFlipCardLayout(); setTimeout(equalizeCardTitleHeights, 10); }, 50); }); } let resizeTimeout; window.addEventListener('resize', () => { clearTimeout(resizeTimeout); resizeTimeout = setTimeout(() => { if (window.nudgeFlipCardLayout) window.nudgeFlipCardLayout(); setTimeout(equalizeCardTitleHeights, 10); }, 250); }); })(); // ============================================================================= // Animation: Prime Card News 4 Column Hover Effects // ============================================================================= // This script "primes" the animations for the cards-news-4-column component // to prevent the "first hover snap" issue. It forces the browser to // acknowledge the initial state and transition properties of the elements // before the first interaction. ;(function() { function primeCardNewsAnimations() { const elementsToPrime = document.querySelectorAll( '.cards-news-4-column .card-img-top, .cards-news-4-column .card-title' ); elementsToPrime.forEach(el => { // Get the computed transition style from CSS const computedStyle = window.getComputedStyle(el); const originalTransition = computedStyle.transition; // 1. Temporarily disable transitions inline to set initial state without animating el.style.transition = 'none'; // 2. Force a reflow/layout read so the browser "sees" the current state immediately // and acknowledges the "no transition" style. void el.offsetWidth; // 3. Explicitly re-apply the transition property that was computed from the CSS. // This makes the browser aware of the transition it *should* use. el.style.transition = originalTransition; // 4. Force another reflow/layout read to make the browser acknowledge the transition property. void el.offsetWidth; // 5. In the next animation frame, remove the inline transition style. // This allows the CSS-defined transitions to take full control for subsequent interactions. // Using requestAnimationFrame helps ensure this runs after the browser has processed the above. requestAnimationFrame(() => { el.style.removeProperty('transition'); }); }); } // Run the priming function after the DOM is fully loaded and parsed. if (document.readyState === 'complete' || document.readyState === 'interactive') { // If DOM is already ready (e.g., script loaded asynchronously or deferred) setTimeout(primeCardNewsAnimations, 0); // Use setTimeout to allow browser to finish current tasks } else { document.addEventListener('DOMContentLoaded', primeCardNewsAnimations); } })(); // ============================================================================= // Animation: Interior Carousel Cards Full Width Configuration // ============================================================================= // This section handles the initialization and custom behavior for the // "Interior Carousel Cards Full Width" Slick Slider component. // // Key functionalities include: // 1. Responsive Setup: Configures the number of slides shown, and the // visibility of navigation arrows and dots based on viewport width breakpoints. // 2. Equal Card Heights: Implements a JavaScript function (`equalizeInteriorCardHeights`) // that dynamically calculates and sets a uniform height for all cards within // the carousel, ensuring visual consistency regardless of varying text content. // 3. Accessibility Enhancements: // - Keyboard Navigation: A specific function (`ensureSlidesNotFocusable`) // modifies the default Slick Slider behavior by removing slide `
` // wrappers from the keyboard tab order. This ensures that users navigating // via keyboard tab directly to the interactive card links (`` tags) // without an intermediate stop on the slide container itself. // - ARIA Attributes: Uses Slick's `customPaging` to generate descriptive // `aria-label` attributes for dot navigation, improving usability for // screen reader users. Arrow buttons also have `aria-label`s. // - Reduced Motion: Checks the user's `prefers-reduced-motion` setting // and adjusts the slide transition speed accordingly (e.g., to 0 for // instant transitions if reduced motion is preferred). // // Overall, this configuration aims to provide a robust, responsive, visually // consistent, and accessible carousel experience for the "Interior Carousel // Cards Full Width" section. // // Target HTML structure: // document.addEventListener("DOMContentLoaded", function () { const $interiorCarousel = $('.interior-carousel-cards-full-width .cards-slick-slider'); if ($interiorCarousel.length > 0) { // Function to ensure all cards in the carousel have the same height. // Iterates through all cards, finds the tallest, and applies that height to all. function equalizeInteriorCardHeights($sliderInstance) { const $allSlides = $sliderInstance.find('.slick-slide'); if (!$allSlides.length) return; const $cardItems = $allSlides.find('.card-item'); const $textAreas = $allSlides.find('.card-item-text-area'); // Reset heights before measurement to get natural content height. $cardItems.css('height', 'auto'); $textAreas.css('height', 'auto'); let maxCardItemHeight = 0; $cardItems.each(function() { const currentHeight = $(this).outerHeight(); if (currentHeight > maxCardItemHeight) { maxCardItemHeight = currentHeight; } }); if (maxCardItemHeight > 0) { $cardItems.css('height', maxCardItemHeight + 'px'); } } // WCAG/Keyboard Navigation Fix: // Function to prevent Slick's main slide
wrappers from being focusable via keyboard. // By default, Slick Slider can add tabindex="0" to slide
s, making them part of the tab order. // This can lead to a "double tab" behavior where the slide wrapper gets focus before the interactive content (e.g., a link) within the slide. // This function explicitly sets tabindex="-1" on these slide
s to remove them from the natural keyboard navigation flow, // ensuring that tabbing goes directly to the intended interactive elements within each card. // Removing the 'role' attribute is an additional measure, as certain roles can implicitly influence focusability for assistive technologies. function ensureSlidesNotFocusable(slickInstance) { if (slickInstance && slickInstance.$slides) { slickInstance.$slides.each(function() { $(this).attr('tabindex', '-1').removeAttr('role'); }); // Also target direct children of the slide track, as these were specifically observed to receive focus in previous debugging. if (slickInstance.$slideTrack) { slickInstance.$slideTrack.children().attr('tabindex', '-1').removeAttr('role'); } } } // Apply height equalization and ensure slides are not focusable on various Slick events. // This ensures visual consistency and correct keyboard navigation during initialization, // when responsive breakpoints change, and after slide transitions. $interiorCarousel.on('init setPosition breakpoint afterChange', function (event, slick) { setTimeout(function() { let currentSlickInstance = null; // Determine the correct Slick instance to operate on. if (slick && slick.$slider) { currentSlickInstance = slick; } else if ($interiorCarousel.data('slick')) { // Fallback to get instance from data attribute if not passed in event. currentSlickInstance = $interiorCarousel.data('slick'); } if (currentSlickInstance) { equalizeInteriorCardHeights($(currentSlickInstance.$slider)); ensureSlidesNotFocusable(currentSlickInstance); } }, 150); // Delay to allow Slick to complete its DOM manipulations before our functions run. }); // Explicitly call ensureSlidesNotFocusable on the initial 'init' event for the slider. // This is a primary step to correct tab order as soon as the slider is ready. $interiorCarousel.on('init', function(event, slick) { if (slick) { // Ensure the slick object is available from the event. ensureSlidesNotFocusable(slick); } }); // Determine slide transition speed, respecting user's preference for reduced motion. const slickSpeed = prefersReducedMotion() ? 0 : 500; // Initialize Slick Slider with all configurations. $interiorCarousel.slick({ arrows: false, // Base setting: Arrows initially off, controlled by responsive settings. dots: false, // Base setting: Dots initially off, controlled by responsive settings. infinite: false, speed: slickSpeed, // Apply motion-sensitive speed. slidesToShow: 6, // Base number of slides for widest view. slidesToScroll: 6, prevArrow: '', nextArrow: '', adaptiveHeight: false, // Manual height equalization is used instead. focusOnSelect: false, // Prevents slide itself from gaining focus on click. customPaging: function(slider, i) { // For accessible dot navigation labels. const $slide = $(slider.$slides[i]); const headlineText = $slide.find('.card-item-headline').text().trim(); let shortDesc = headlineText.substring(0, 30); if (headlineText.length > 30) shortDesc += "..."; return ''; }, responsive: [ { breakpoint: 1400, settings: { slidesToShow: 5, slidesToScroll: 5, arrows: true, dots: false } }, { breakpoint: 1281, settings: { slidesToShow: 4, slidesToScroll: 4, arrows: true, dots: false } }, { breakpoint: 992, settings: { slidesToShow: 3, slidesToScroll: 3, arrows: false, dots: true } }, { breakpoint: 768, settings: { slidesToShow: 2, slidesToScroll: 2, arrows: false, dots: true } }, { breakpoint: 576, settings: { slidesToShow: 1, slidesToScroll: 1, arrows: false, dots: true } } ] }); } }); // ============================================================================= // Link Banner Button Width Equalization // ============================================================================= // This section handles making all buttons in the link banner component have // uniform widths for visual consistency. // // Key functionalities: // 1. Finds all link banner button containers on the page // 2. For each container: // - Measures the natural width of each button // - Determines the widest button's width // - Sets all buttons to match the widest width // // Target HTML structure: // // // The script runs on window load to ensure all fonts and resources that might // affect button width are fully loaded before measurements are taken. // Script for Link Banner Button Uniform Widths function setUniformButtonWidths() { // Select all wrappers that might contain a set of buttons to be equalized const buttonContainers = document.querySelectorAll('.sb-link-banner__buttons, .sb-link-banner--two-button-variant .sb-link-banner__item-layout-wrapper'); buttonContainers.forEach(container => { // Find direct .sb-link-banner__button children or those within .sb-link-banner__item-column for the two-button variant let buttons; if (container.classList.contains('sb-link-banner__item-layout-wrapper')) { // For the two-button variant, buttons are inside item-columns which are inside the layout-wrapper buttons = container.querySelectorAll(".sb-link-banner__item-column .sb-link-banner__button"); } else { // For the default multi-button banner buttons = container.querySelectorAll(".sb-link-banner__button"); } if (!buttons || buttons.length === 0) return; let maxWidth = 0; buttons.forEach(btn => { btn.style.width = "auto"; // Reset width to measure natural width const width = btn.offsetWidth; if (width > maxWidth) { maxWidth = width; } }); buttons.forEach(btn => { btn.style.width = `${maxWidth}px`; }); }); } // Ensure the function runs after all resources are loaded window.addEventListener("load", setUniformButtonWidths); // Optional: Recalculate on resize (consider debouncing for performance if enabled) // window.addEventListener('resize', setUniformButtonWidths); // ============================================================================= // Animation: Prime Cards 4 Column v2 Static Hover Effects // ============================================================================= // This script "primes" the animations for the cards-4-column-v2-static component // to prevent the "first hover snap" issue. It targets the image and button visual. ;(function() { function primeCards4ColumnV2StaticAnimations() { const elementsToPrime = document.querySelectorAll( '.cards-4-column-v2-static .cards-4-col-v2-static-img', '.cards-4-column-v2-static .cards-4-col-v2-static-btn-visual' ); elementsToPrime.forEach(el => { const computedStyle = window.getComputedStyle(el); const originalTransition = computedStyle.transition; // Only prime if there's an actual transition defined, other than the default "all 0s ease 0s" if (originalTransition && originalTransition !== 'all 0s ease 0s') { el.style.transition = 'none'; void el.offsetWidth; // Force reflow el.style.transition = originalTransition; void el.offsetWidth; // Force reflow again requestAnimationFrame(() => { el.style.removeProperty('transition'); }); } }); } // Run the priming function after the DOM is fully loaded and parsed. if (document.readyState === 'complete' || document.readyState === 'interactive') { // If DOM is already ready (e.g., script loaded asynchronously or deferred) setTimeout(primeCards4ColumnV2StaticAnimations, 0); // Use setTimeout to allow browser to finish current tasks } else { document.addEventListener('DOMContentLoaded', primeCards4ColumnV2StaticAnimations); } })();