PageSourceSearch

https://www.accademiamoda.it/wp-content/themes/iuad-theme/src/js/custom/animations.js?ver=1.0.0

js accademiamoda.it collected 2026-10-04 13:23:40 UTC 104,417 bytes, 2,846 lines download raw bytes

1/**
2 * Force Scroll to Top on Page Load/Reload
3 * Assicura che ogni reload parta dall'inizio della pagina
4 * Previene problemi con animazioni ScrollTrigger e storico scroll
5 */
6(function() {
7    'use strict';
8
9    // Funzione helper per controllare se c'è un anchor nell'URL
10    function hasAnchorInUrl() {
11        return window.location.hash && window.location.hash.length > 1;
12    }
13
14    function normalizeHash(hash) {
15        if (!hash) return '';
16        return hash.charAt(0) === '#' ? hash : ('#' + hash);
17    }
18
19    function isElementActuallyVisible(el) {
20        if (!el || !(el instanceof Element)) return false;
21        const style = window.getComputedStyle(el);
22        if (style.display === 'none' || style.visibility === 'hidden') return false;
23        // offsetParent è null anche per position:fixed in alcuni casi, quindi aggiungiamo un check su rect
24        const rect = el.getBoundingClientRect();
25        if ((rect.width === 0 && rect.height === 0) && el.offsetParent === null) return false;
26        // Evita anchor dentro wrapper responsive nascosti
27        const mobileWrapper = el.closest('.responsive-template-mobile');
28        if (mobileWrapper && window.getComputedStyle(mobileWrapper).display === 'none') return false;
29        const desktopWrapper = el.closest('.responsive-template-desktop');
30        if (desktopWrapper && window.getComputedStyle(desktopWrapper).display === 'none') return false;
31        return true;
32    }
33
34    /**
35     * Quando usiamo Lenis + inizializzazioni GSAP/ScrollTrigger,
36     * il jump nativo verso l'anchor può non avvenire (o essere annullato).
37     * Qui forziamo lo scroll all'anchor dopo il load, con retry.
38     */
39    function scrollToInitialAnchor() {
40        if (!hasAnchorInUrl()) return;
41
42        // Se lo scroll è ancora bloccato dal loader, non ha senso tentare.
43        if (document.body && document.body.classList.contains('loading')) {
44            return;
45        }
46
47        const hash = normalizeHash(window.location.hash);
48        if (!hash || hash === '#') return;
49
50        // decode per gestire id con caratteri speciali
51        let selector = hash;
52        try {
53            selector = decodeURIComponent(hash);
54        } catch (e) {}
55
56        // Serve essere "tolleranti": su alcune pagine i blocchi arrivano tardi (Gutenberg, immagini, init JS).
57        const maxAttempts = 80; // ~8s con delay 100ms
58        const attemptDelayMs = 100;
59        let attempt = 0;
60
61        const tryScroll = () => {
62            attempt += 1;
63
64            let el = null;
65            try {
66                // Se ci fossero più occorrenze, preferiamo la prima visibile
67                const all = document.querySelectorAll(selector);
68                if (all && all.length) {
69                    el = Array.from(all).find(isElementActuallyVisible) || all[0];
70                }
71            } catch (e) {
72                // selector non valido (es. hash con caratteri non escapati)
73                el = document.getElementById(selector.replace(/^#/, ''));
74            }
75
76            if (!el || !isElementActuallyVisible(el)) {
77                if (attempt < maxAttempts) {
78                    setTimeout(tryScroll, attemptDelayMs);
79                }
80                return;
81            }
82
83            // 1) Prima fai il jump nativo (è il più affidabile per hash deep-link).
84            // 2) Poi (se c'è Lenis) riallinea la posizione con un secondo pass.
85            el.scrollIntoView({ behavior: 'auto', block: 'start' });
86
87            const lenisScroll = () => {
88                if (window.lenis && typeof window.lenis.scrollTo === 'function') {
89                    window.lenis.scrollTo(el, { immediate: true, force: true });
90                }
91            };
92
93            requestAnimationFrame(lenisScroll);
94            setTimeout(lenisScroll, 50);
95        };
96
97        tryScroll();
98    }
99
100    // Espone la funzione: serve al page loader che sblocca lo scroll dopo la sua animazione.
101    window.iuadScrollToInitialAnchor = scrollToInitialAnchor;
102
103    // Observer: se l'anchor arriva dopo (blocchi inseriti tardi / template responsive),
104    // proviamo non appena compare nel DOM.
105    let anchorObserver = null;
106    function ensureAnchorObserver() {
107        if (!hasAnchorInUrl()) return;
108        if (anchorObserver) return;
109        if (typeof MutationObserver === 'undefined') return;
110
111        anchorObserver = new MutationObserver(() => {
112            // appena lo scroll è libero e l'elemento esiste, scrolla e poi spegne l'observer
113            if (document.body && document.body.classList.contains('loading')) return;
114            scrollToInitialAnchor();
115
116            // Se dopo lo scroll l'elemento è visibile, possiamo fermarci
117            const hash = normalizeHash(window.location.hash);
118            if (!hash || hash === '#') return;
119            try {
120                const candidates = document.querySelectorAll(decodeURIComponent(hash));
121                const visible = candidates && candidates.length ? Array.from(candidates).find(isElementActuallyVisible) : null;
122                if (visible) {
123                    anchorObserver.disconnect();
124                    anchorObserver = null;
125                }
126            } catch (e) {}
127        });
128
129        anchorObserver.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });
130    }
131
132    // Se l'utente inizia a interagire (scroll/wheel/touch/keys) prima del "load",
133    // non forziamo più lo scroll-top a fine caricamento.
134    var userHasInteractedWithScroll = false;
135    function markUserInteraction() {
136        userHasInteractedWithScroll = true;
137    }
138    window.addEventListener('scroll', markUserInteraction, { passive: true });
139    window.addEventListener('wheel', markUserInteraction, { passive: true });
140    window.addEventListener('touchmove', markUserInteraction, { passive: true });
141    window.addEventListener('keydown', function(e) {
142        // tasti tipici di scroll
143        var keys = ['ArrowDown', 'ArrowUp', 'PageDown', 'PageUp', 'Home', 'End', ' '];
144        if (keys.indexOf(e.key) !== -1) {
145            markUserInteraction();
146        }
147    }, { passive: true });
148
149    // 1. Disabilita completamente il comportamento nativo di scroll restoration
150    // MA solo se non c'è un anchor nell'URL
151    if ('scrollRestoration' in history && !hasAnchorInUrl()) {
152        history.scrollRestoration = 'manual';
153    }
154
155    // 2. Forza scroll a 0 PRIMA che il DOM sia renderizzato
156    // MA solo se non c'è un anchor nell'URL (per permettere lo scroll automatico all'anchor)
157    if (!hasAnchorInUrl()) {
158        window.scrollTo(0, 0);
159        document.documentElement.scrollTop = 0;
160        document.body.scrollTop = 0;
161    }
162
163    // 3. Assicura scroll a 0 anche dopo il caricamento completo
164    // MA solo se non c'è un anchor nell'URL
165    window.addEventListener('load', function() {
166        if (hasAnchorInUrl()) {
167            // Se l'utente ha già iniziato a scrollare, non interveniamo.
168            if (!userHasInteractedWithScroll) {
169                scrollToInitialAnchor();
170                // Secondo pass: utile quando plugin/scroll management ricalcolano dopo il load.
171                setTimeout(() => {
172                    if (!userHasInteractedWithScroll) {
173                        scrollToInitialAnchor();
174                    }
175                }, 500);
176            }
177            ensureAnchorObserver();
178            return;
179        }
180
181        if (!hasAnchorInUrl() && !userHasInteractedWithScroll) {
182            // Usa Lenis se disponibile, altrimenti scroll nativo
183            if (window.lenis) {
184                window.lenis.scrollTo(0, { immediate: true, force: true });
185            } else {
186                window.scrollTo({ top: 0, left: 0, behavior: 'instant' });
187                document.documentElement.scrollTop = 0;
188                document.body.scrollTop = 0;
189            }
190        }
191    });
192
193    // Se l'hash cambia (o viene "riapplicato" dal browser), riallineiamo lo scroll.
194    window.addEventListener('hashchange', function() {
195        if (!userHasInteractedWithScroll) {
196            scrollToInitialAnchor();
197        }
198        ensureAnchorObserver();
199    });
200
201    // 4. Previeni il salvataggio della posizione prima dell'unload
202    window.addEventListener('beforeunload', function() {
203        if (window.lenis) {
204            window.lenis.scrollTo(0, { immediate: true });
205        } else {
206            window.scrollTo(0, 0);
207        }
208    });
209
210    // 5. Gestisci anche la navigazione con pulsante back/forward
211    window.addEventListener('pageshow', function(event) {
212        // Se la pagina è stata caricata dalla cache (bfcache)
213        if (event.persisted) {
214            // Non forzare lo scroll a 0 se c'è un anchor nell'URL
215            if (!hasAnchorInUrl()) {
216                if (window.lenis) {
217                    window.lenis.scrollTo(0, { immediate: true, force: true });
218                } else {
219                    window.scrollTo({ top: 0, left: 0, behavior: 'instant' });
220                }
221            }
222            
223            // Refresh ScrollTrigger se presente
224            if (typeof ScrollTrigger !== 'undefined') {
225                ScrollTrigger.refresh();
226            }
227        }
228    });
229
230})();
231
232
233/**
234 * Lenis Smooth Scroll Initialization
235 * Integrates Lenis with GSAP ScrollTrigger
236 */
237
238(function() {
239    'use strict';
240
241    /**
242     * Esclude dalla smooth-scroll Lenis zone con scroll interno (es. webchat Botpress).
243     * Senza questo, la rotella tende a muovere la pagina invece della lista messaggi.
244     * Cfr. Lenis: opzione prevent(node) e data-lenis-prevent-wheel.
245     */
246    function iuadLenisPreventWheelOnNode(node) {
247        if (!(node instanceof HTMLElement)) {
248            return false;
249        }
250        if (node.hasAttribute('data-lenis-prevent') || node.hasAttribute('data-lenis-prevent-wheel')) {
251            return true;
252        }
253        var selectors = [
254            '[data-botpress-webchat]',
255            '[id*="botpress"]',
256            '[class*="bpWebchat"]',
257            '[class*="bpw-"]',
258            '[class*="bpMessageList"]'
259        ].join(', ');
260        if (node.matches && node.matches(selectors)) {
261            return true;
262        }
263        if (typeof node.closest === 'function' && node.closest(selectors)) {
264            return true;
265        }
266        if (node.tagName === 'IFRAME' && node.src) {
267            try {
268                var src = node.src;
269                if (/botpress|bpcontent\.cloud/i.test(src)) {
270                    return true;
271                }
272            } catch (e) {}
273        }
274        return false;
275    }
276
277    // Initialize a new Lenis instance for smooth scrolling
278    const lenis = new Lenis({
279        duration: 1.2,
280        easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
281        orientation: 'vertical',
282        gestureOrientation: 'vertical',
283        smoothWheel: true,
284        wheelMultiplier: 1,
285        smoothTouch: false,
286        touchMultiplier: 2,
287        infinite: false,
288        allowNestedScroll: true,
289        prevent: iuadLenisPreventWheelOnNode,
290    });
291
292    // Synchronize Lenis scrolling with GSAP's ScrollTrigger plugin
293    lenis.on('scroll', ScrollTrigger.update);
294
295    // Add Lenis's requestAnimationFrame (raf) method to GSAP's ticker
296    // This ensures Lenis's smooth scroll animation updates on each GSAP tick
297    gsap.ticker.add((time) => {
298        lenis.raf(time * 1000); // Convert time from seconds to milliseconds
299    });
300
301    // Disable lag smoothing in GSAP to prevent any delay in scroll animations
302    gsap.ticker.lagSmoothing(0);
303
304    // Optional: Stop/Start smooth scroll based on user preference
305    // Uncomment if you want to respect users' motion preferences
306    /*
307    const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
308    if (prefersReducedMotion.matches) {
309        lenis.stop();
310    }
311    */
312
313    // Expose lenis globally for potential use in other scripts
314    window.lenis = lenis;
315
316})();
317
318/**
319 * Scroll lock when portfolio lightboxes are open
320 * - Image/video lightbox adds: body.portfolio-lightbox-open
321 * - PDF lightbox adds: body.portfolio-pdf-lightbox-open
322 *
323 * We lock both html/body (and stop Lenis) to avoid background scroll on all browsers.
324 */
325(function() {
326    'use strict';
327
328    function isLightboxOpen() {
329        return document.body.classList.contains('portfolio-lightbox-open')
330            || document.body.classList.contains('portfolio-pdf-lightbox-open');
331    }
332
333    function lockScroll() {
334        if (document.body.dataset.iuadScrollLock === '1') return;
335        document.body.dataset.iuadScrollLock = '1';
336
337        const scrollY = window.scrollY || window.pageYOffset || 0;
338        document.body.dataset.iuadScrollLockY = String(scrollY);
339
340        // preserve previous inline styles (only inline, CSS stays intact)
341        document.documentElement.dataset.iuadPrevOverflow = document.documentElement.style.overflow || '';
342        document.body.dataset.iuadPrevOverflow = document.body.style.overflow || '';
343        document.body.dataset.iuadPrevPosition = document.body.style.position || '';
344        document.body.dataset.iuadPrevTop = document.body.style.top || '';
345        document.body.dataset.iuadPrevWidth = document.body.style.width || '';
346
347        document.documentElement.style.overflow = 'hidden';
348        document.body.style.overflow = 'hidden';
349        document.body.style.position = 'fixed';
350        document.body.style.top = (-scrollY) + 'px';
351        document.body.style.width = '100%';
352
353        if (window.lenis && typeof window.lenis.stop === 'function') {
354            window.lenis.stop();
355        }
356    }
357
358    function unlockScroll() {
359        if (document.body.dataset.iuadScrollLock !== '1') return;
360        document.body.dataset.iuadScrollLock = '0';
361
362        const scrollY = parseInt(document.body.dataset.iuadScrollLockY || '0', 10) || 0;
363
364        document.documentElement.style.overflow = document.documentElement.dataset.iuadPrevOverflow || '';
365        document.body.style.overflow = document.body.dataset.iuadPrevOverflow || '';
366        document.body.style.position = document.body.dataset.iuadPrevPosition || '';
367        document.body.style.top = document.body.dataset.iuadPrevTop || '';
368        document.body.style.width = document.body.dataset.iuadPrevWidth || '';
369
370        // restore scroll position
371        window.scrollTo(0, scrollY);
372
373        if (window.lenis && typeof window.lenis.start === 'function') {
374            window.lenis.start();
375        }
376    }
377
378    function sync() {
379        if (isLightboxOpen()) lockScroll();
380        else unlockScroll();
381    }
382
383    // Initial sync + observe class changes
384    if (document.readyState === 'loading') {
385        document.addEventListener('DOMContentLoaded', sync);
386    } else {
387        sync();
388    }
389
390    const observer = new MutationObserver(sync);
391    observer.observe(document.body, { attributes: true, attributeFilter: ['class'] });
392})();
393
394/**
395 * SplitText Animation for Percent Blocks
396 * Animates text from bottom when block enters viewport
397 */
398(function() {
399    'use strict';
400
401    // Register SplitText plugin
402    gsap.registerPlugin(SplitText);
403
404    // Wait for fonts to be ready
405    /*
406    document.fonts.ready.then(() => {
407        // Find all percent blocks
408        const percentBlocks = document.querySelectorAll('.percent-block');
409        
410        if (percentBlocks.length === 0) return;
411
412        percentBlocks.forEach((block) => {
413            // Find title and text elements within each block
414            const title = block.querySelector('.percent-block-title');
415            const text = block.querySelector('.percent-block-text');
416            
417            // Create array of elements to animate
418            const elements = [];
419            if (title) elements.push(title);
420            if (text) elements.push(text);
421            
422            if (elements.length === 0) return;
423
424            // Set initial opacity to 1 (will be controlled by animation)
425            gsap.set(elements, { opacity: 1 });
426
427            // Apply SplitText to each element
428            elements.forEach((element) => {
429                const split = new SplitText(element, {
430                    type: "words,lines",
431                    linesClass: "line",
432                });
433
434                // Create the animation timeline
435                const tl = gsap.timeline({
436                    scrollTrigger: {
437                        trigger: block,
438                        start: "top 80%", // Start when block is 80% from top of viewport
439                        end: "top 20%",
440                        toggleActions: "play none none none", // Play once when entering
441                        // markers: true, // Uncomment for debugging
442                    }
443                });
444
445                // Animate lines from bottom
446                tl.from(split.lines, {
447                    duration: 1,
448                    yPercent: 100,
449                    opacity: 0,
450                    stagger: 0.15,
451                    ease: "power3.out",
452                });
453            });
454        });
455    });
456    */
457
458})();
459
460/**
461 * Percent Block Circles Animation
462 * Slide animation with opposite directions when entering viewport
463 */
464(function() {
465    'use strict';
466
467    // Wait for DOM to be ready
468    document.addEventListener('DOMContentLoaded', () => {
469        // Find all percent block containers
470        const containers = document.querySelectorAll('.percent-block-container-circle');
471        
472        if (containers.length === 0) return;
473
474        containers.forEach((container) => {
475            const blockOne = container.querySelector('.percent-block-one');
476            const blockTwo = container.querySelector('.percent-block-two');
477            
478            if (!blockOne || !blockTwo) return;
479
480            // Set initial state
481            gsap.set(blockOne, {
482                x: 0,
483                zIndex: 1
484            });
485            
486            gsap.set(blockTwo, {
487                x: 0,
488                zIndex: 2,
489                scale: 1
490            });
491
492            // Create the animation timeline
493            const tl = gsap.timeline({
494                scrollTrigger: {
495                    trigger: container,
496                    start: "center bottom", // Start when 50% of container is visible in viewport
497                    end: "bottom top",
498                    toggleActions: "play none none none",
499                    // markers: true, // Uncomment for debugging
500                }
501            });
502
503            // Animate both blocks
504            tl.to(blockOne, {
505                x: -250, // Slide left
506                duration: 1.2,
507                ease: "power3.out"
508            }, 0) // Start at time 0
509            .to(blockTwo, {
510                x: 250, // Slide right
511                width: "+=100px", // Increase width by 20px
512                height: "+=100px", // Increase height by 20px to keep proportion
513                duration: 1.2,
514                ease: "power3.out"
515            }, 0); // Start at time 0 (same time as blockOne)
516        });
517    });
518
519})();
520
521
522/**
523 * Vertical Card Carousel with Stack Effect
524 * Carousel verticale con effetto mazzo di carte
525 */
526(function() {
527    'use strict';
528
529    document.addEventListener('DOMContentLoaded', () => {
530        const carousel = document.querySelector('.vertical-card-carousel');
531        
532        if (!carousel) return;
533
534        const cards = carousel.querySelectorAll('.carousel-card');
535        const dots = carousel.querySelectorAll('.carousel-dot');
536        
537        if (cards.length === 0 || dots.length === 0) return;
538
539        let currentIndex = 0;
540        let previousIndex = -1;
541        let isAnimating = false;
542
543        // Funzione per calcolare l'indice della slide precedente
544        function getPreviousSlideIndex(activeIndex) {
545            return activeIndex > 0 ? activeIndex - 1 : -1;
546        }
547
548        // Funzione per animare il cambio di slide
549        function goToSlide(index) {
550            if (isAnimating || index === currentIndex || index < 0 || index >= cards.length) {
551                return;
552            }
553
554            isAnimating = true;
555
556            const currentCard = cards[currentIndex];
557            const nextCard = cards[index];
558            const currentDot = dots[currentIndex];
559            const nextDot = dots[index];
560            
561            // Calcola nuova slide precedente
562            const newPreviousIndex = getPreviousSlideIndex(index);
563            const newPreviousCard = newPreviousIndex >= 0 ? cards[newPreviousIndex] : null;
564
565            // Determina se siamo su mobile (stesso breakpoint dello swipe)
566            const isMobile = window.innerWidth <= 1024;
567
568            // Timeline GSAP per l'animazione
569            const tl = gsap.timeline({
570                onComplete: () => {
571                    isAnimating = false;
572                    currentIndex = index;
573                    previousIndex = newPreviousIndex;
574                }
575            });
576
577            // Determina la direzione (avanti o indietro)
578            const isGoingForward = index > currentIndex;
579
580            if (isMobile) {
581                // ANIMAZIONE ORIZZONTALE SU MOBILE CON EFFETTO SOVRAPPOSIZIONE
582                // La card previous rimane a x: 0 (stessa posizione della card attiva)
583                if (isGoingForward) {
584                    // NAVIGAZIONE IN AVANTI (swipe left)
585                    
586                    // La vecchia slide previous scompare al centro senza movimento (se esiste e non è quella che diventerà attiva)
587                    if (previousIndex >= 0 && previousIndex !== index) {
588                        const oldPreviousCard = cards[previousIndex];
589                        tl.to(oldPreviousCard, {
590                            x: 0,
591                            y: 0,
592                            opacity: 0,
593                            duration: 0.4,
594                            ease: "power2.in",
595                            onComplete: () => {
596                                oldPreviousCard.classList.remove('previous');
597                            }
598                        }, 0);
599                    }
600
601                    // La card corrente diventa previous (rimane a x: 0, solo cambia classe)
602                    tl.to(currentCard, {
603                        x: 0,
604                        y: 0,
605                        opacity: 1,
606                        duration: 0.5,
607                        ease: "power2.out",
608                        onStart: () => {
609                            currentCard.classList.remove('active');
610                            currentCard.classList.add('previous');
611                        }
612                    }, 0);
613
614                    // La nuova card attiva entra da destra con bounce
615                    tl.fromTo(nextCard, 
616                        {
617                            x: 100,
618                            y: 0,
619                            opacity: 0
620                        },
621                        {
622                            x: 0,
623                            y: 0,
624                            opacity: 1,
625                            duration: 0.8,
626                            ease: "back.out(1.7)",
627                            onStart: () => {
628                                nextCard.classList.remove('previous');
629                                nextCard.classList.add('active');
630                                startCardMaskIntro(nextCard);
631                            }
632                        }, 0.2);
633
634                } else {
635                    // NAVIGAZIONE ALL'INDIETRO (swipe right)
636                    
637                    // La card corrente scompare completamente a destra
638                    tl.to(currentCard, {
639                        x: 100,
640                        y: 0,
641                        opacity: 0,
642                        duration: 0.5,
643                        ease: "power2.in",
644                        onComplete: () => {
645                            currentCard.classList.remove('active');
646                            gsap.set(currentCard, { x: 100, y: 0, opacity: 0 });
647                        }
648                    }, 0);
649
650                    // Assicura che la nextCard sia nella posizione previous (x: 0) prima di animarla
651                    // Se non ha già la classe previous, imposta la posizione iniziale
652                    if (!nextCard.classList.contains('previous')) {
653                        gsap.set(nextCard, { x: 0, y: 0, opacity: 1 });
654                        nextCard.classList.add('previous');
655                    }
656
657                    // La vecchia previous (che è la nuova attiva) rimane a x: 0, solo cambia classe
658                    tl.to(nextCard, {
659                        x: 0,
660                        y: 0,
661                        opacity: 1,
662                        duration: 0.8,
663                        ease: "back.out(1.7)",
664                        onStart: () => {
665                            nextCard.classList.remove('previous');
666                            nextCard.classList.add('active');
667                            startCardMaskIntro(nextCard);
668                        }
669                    }, 0.2);
670
671                    // La vecchia "old previous" scompare al centro senza movimento se non è quella che diventa la nuova previous
672                    if (previousIndex >= 0 && previousIndex !== newPreviousIndex && previousIndex !== index) {
673                        const oldPreviousCard = cards[previousIndex];
674                        tl.to(oldPreviousCard, {
675                            x: 0,
676                            y: 0,
677                            opacity: 0,
678                            duration: 0.4,
679                            ease: "power2.in",
680                            onComplete: () => {
681                                oldPreviousCard.classList.remove('previous');
682                            }
683                        }, 0);
684                    }
685                }
686
687                // Mostra la nuova slide previous (se esiste e non è già visibile) - solo su mobile
688                if (newPreviousCard && newPreviousIndex !== previousIndex && newPreviousIndex !== currentIndex) {
689                    tl.fromTo(newPreviousCard,
690                        {
691                            x: 100,
692                            y: 0,
693                            opacity: 0
694                        },
695                        {
696                            x: 0,
697                            y: 0,
698                            opacity: 1,
699                            duration: 0.6,
700                            ease: "power2.out",
701                            onStart: () => {
702                                newPreviousCard.classList.add('previous');
703                            }
704                        }, 0.1);
705                }
706            } else {
707                // ANIMAZIONE VERTICALE SU DESKTOP
708                if (isGoingForward) {
709                    // NAVIGAZIONE IN AVANTI
710                    
711                    // La vecchia slide previous scompare (se esiste e non è quella che diventerà attiva)
712                    if (previousIndex >= 0 && previousIndex !== index) {
713                        const oldPreviousCard = cards[previousIndex];
714                        tl.to(oldPreviousCard, {
715                            y: 100,
716                            opacity: 0,
717                            duration: 0.4,
718                            ease: "power2.in",
719                            onComplete: () => {
720                                oldPreviousCard.classList.remove('previous');
721                            }
722                        }, 0);
723                    }
724
725                    // La card corrente diventa previous (scivola 
725sotto)
726                    tl.to(currentCard, {
727                        y: 30,
728                        opacity: 1,
729                        duration: 0.5,
730                        ease: "power2.out",
731                        onStart: () => {
732                            currentCard.classList.remove('active');
733                            currentCard.classList.add('previous');
734                        }
735                    }, 0);
736
737                    // La nuova card attiva appare dal basso con bounce
738                    tl.fromTo(nextCard, 
739                        {
740                            y: 100,
741                            opacity: 0
742                        },
743                        {
744                            y: 0,
745                            opacity: 1,
746                            duration: 0.8,
747                            ease: "back.out(1.7)",
748                            onStart: () => {
749                                nextCard.classList.remove('previous');
750                                nextCard.classList.add('active');
751                                startCardMaskIntro(nextCard);
752                            }
753                        }, 0.2);
754
755                } else {
756                    // NAVIGAZIONE ALL'INDIETRO
757                    
758                    // La card corrente scompare verso il basso
759                    tl.to(currentCard, {
760                        y: 100,
761                        opacity: 0,
762                        duration: 0.5,
763                        ease: "power2.in",
764                        onComplete: () => {
765                            currentCard.classList.remove('active');
766                            gsap.set(currentCard, { y: 100, opacity: 0 });
767                        }
768                    }, 0);
769
770                    // La vecchia previous (che è la nuova attiva) sale con bounce
771                    tl.to(nextCard, {
772                        y: 0,
773                        opacity: 1,
774                        duration: 0.8,
775                        ease: "back.out(1.7)",
776                        onStart: () => {
777                            nextCard.classList.remove('previous');
778                            nextCard.classList.add('active');
779                            startCardMaskIntro(nextCard);
780                        }
781                    }, 0.2);
782
783                    // La vecchia "old previous" scompare se non è quella che diventa la nuova previous
784                    if (previousIndex >= 0 && previousIndex !== newPreviousIndex && previousIndex !== index) {
785                        const oldPreviousCard = cards[previousIndex];
786                        tl.to(oldPreviousCard, {
787                            y: 100,
788                            opacity: 0,
789                            duration: 0.4,
790                            ease: "power2.in",
791                            onComplete: () => {
792                                oldPreviousCard.classList.remove('previous');
793                            }
794                        }, 0);
795                    }
796                }
797
798                // Mostra la nuova slide previous (solo su desktop)
799                if (newPreviousCard && newPreviousIndex !== previousIndex && newPreviousIndex !== currentIndex) {
800                    tl.fromTo(newPreviousCard,
801                        {
802                            y: 100,
803                            opacity: 0
804                        },
805                        {
806                            y: 30,
807                            opacity: 1,
808                            duration: 0.6,
809                            ease: "power2.out",
810                            onStart: () => {
811                                newPreviousCard.classList.add('previous');
812                            }
813                        }, 0.1);
814                }
815            }
816
817            // Aggiorna i dots
818            currentDot.classList.remove('active');
819            nextDot.classList.add('active');
820
821            // Animazione del dot che perde l'active (torna alla scala originale)
822            /*
823            gsap.to(currentDot, {
824                scale: 1,
825                duration: 0.3,
826                ease: "power2.out"
827            });
828
829            // Animazione del dot attivo (scala leggermente più grande)
830            gsap.fromTo(nextDot,
831                { scale: 1 },
832                { 
833                    scale: 0.8, 
834                    duration: 0.3,
835                    ease: "back.out(2)"
836                }
837            );
838            */
839        }
840
841        // Event listener per i dots
842        dots.forEach((dot, index) => {
843            dot.addEventListener('click', () => {
844                goToSlide(index);
845            });
846        });
847
848        // Swipe su mobile: abilita next/prev con gesture orizzontale
849        if (window.innerWidth <= 1024) {
850            let touchStartX = 0;
851            let touchCurrentX = 0;
852            const swipeThreshold = 50; // px
853
854            const onTouchStart = (e) => {
855                const touch = e.touches[0];
856                touchStartX = touch.clientX;
857                touchCurrentX = touchStartX;
858            };
859
860            const onTouchMove = (e) => {
861                const touch = e.touches[0];
862                touchCurrentX = touch.clientX;
863            };
864
865            const onTouchEnd = () => {
866                const deltaX = touchCurrentX - touchStartX;
867                if (Math.abs(deltaX) < swipeThreshold) return;
868
869                if (deltaX < 0) {
870                    // swipe left -> next
871                    const nextIndex = (currentIndex + 1) % cards.length;
872                    goToSlide(nextIndex);
873                } else {
874                    // swipe right -> prev
875                    const prevIndex = (currentIndex - 1 + cards.length) % cards.length;
876                    goToSlide(prevIndex);
877                }
878            };
879
880            carousel.addEventListener('touchstart', onTouchStart, { passive: true });
881            carousel.addEventListener('touchmove', onTouchMove, { passive: true });
882            carousel.addEventListener('touchend', onTouchEnd);
883        }
884
885        // Navigazione da tastiera (frecce su/giù)
886        document.addEventListener('keydown', (e) => {
887            if (e.key === 'ArrowDown') {
888                e.preventDefault();
889                const nextIndex = (currentIndex + 1) % cards.length;
890                goToSlide(nextIndex);
891            } else if (e.key === 'ArrowUp') {
892                e.preventDefault();
893                const prevIndex = (currentIndex - 1 + cards.length) % cards.length;
894                goToSlide(prevIndex);
895            }
896        });
897
898        // Navigazione con scroll del mouse (solo desktop)
899        if (window.innerWidth > 1024) {
900            const wrapper = carousel.querySelector('.carousel-wrapper') || carousel;
901            let wheelTimeout = null;
902
903            const onWheel = (e) => {
904                const deltaY = e.deltaY || 0;
905                if (deltaY === 0) {
906                    return;
907                }
908
909                // Calcola quanto del wrapper è effettivamente visibile in viewport
910                const rect = wrapper.getBoundingClientRect();
911                const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
912                const visibleTop = Math.max(rect.top, 0);
913                const visibleBottom = Math.min(rect.bottom, viewportHeight);
914                const visibleHeight = Math.max(0, visibleBottom - visibleTop);
915                const visibilityRatio = rect.height > 0 ? visibleHeight / rect.height : 0;
916
917                // Se il wrapper non è almeno al 60% visibile, non intercettiamo lo scroll
918                if (visibilityRatio < 0.85) {
919                    return;
920                }
921
922                // Determina l'indice di destinazione SENZA loop
923                let targetIndex = currentIndex;
924                if (deltaY > 0 && currentIndex < cards.length - 1) {
925                    // Scroll verso il basso -> slide successiva (se esiste)
926                    targetIndex = currentIndex + 1;
927                } else if (deltaY < 0 && currentIndex > 0) {
928                    // Scroll verso l'alto -> slide precedente (se esiste)
929                    targetIndex = currentIndex - 1;
930                }
931
932                // Se non cambierebbe slide, lascia scorrere la pagina normalmente
933                if (targetIndex === currentIndex) {
934                    return;
935                }
936
937                // A questo punto intercettiamo lo scroll per controllare il carousel
938                e.preventDefault();
939                e.stopPropagation();
940
941                // Semplice throttling: evita troppi cambi slide in una singola "raffica"
942                if (wheelTimeout) {
943                    return;
944                }
945                wheelTimeout = setTimeout(() => {
946                    wheelTimeout = null;
947                }, 150);
948
949                goToSlide(targetIndex);
950            };
951
952            wrapper.addEventListener('wheel', onWheel, { passive: false });
953        }
954
955        // Inizializza lo stato iniziale (senza avviare la maschera sulla prima slide)
956        const isMobileInit = window.innerWidth <= 1024;
957        cards.forEach((card, index) => {
958            if (index === 0) {
959                if (isMobileInit) {
960                    // Su mobile: solo x, y deve essere 0
961                    gsap.set(card, { x: 0, y: 0, opacity: 1 });
962                } else {
963                    // Su desktop: solo y, x deve essere 0
964                    gsap.set(card, { x: 0, y: 0, opacity: 1 });
965                }
966            } else {
967                if (isMobileInit) {
968                    // Su mobile: solo x, y deve essere 0
969                    gsap.set(card, { x: 100, y: 0, opacity: 0 });
970                } else {
971                    // Su desktop: solo y, x deve essere 0
972                    gsap.set(card, { x: 0, y: 100, opacity: 0 });
973                }
974            }
975        });
976
977        // Funzione per avviare l'animazione sulla card attiva
978        const triggerMaskAnimation = () => {
979            const activeCard = carousel.querySelector('.carousel-card.active');
980            if (activeCard) {
981                // Verifica che la card abbia effettivamente la maschera SVG
982                const hasMask = activeCard.querySelector('.card-image .masked-svg');
983                if (hasMask) {
984                    //console.log('Trovata maschera su slide attiva, avvio animazione');
985                    
986                    // PRIMA: ferma TUTTE le animazioni su TUTTE le card
987                    const allCards = carousel.querySelectorAll('.carousel-card');
988                    allCards.forEach(card => {
989                        const cardSvg = card.querySelector('.card-image .masked-svg');
990                        if (cardSvg) {
991                            const cardBlobs = cardSvg.querySelectorAll('.blob');
992                            cardBlobs.forEach(blob => {
993                                gsap.killTweensOf(blob);
994                                gsap.set(blob, { clearProps: "all" });
995                            });
996                        }
997                    });
998                    
999                    // POI: avvia solo sulla card attiva
1000                    setTimeout(() => {
1001                        startCardMaskIntro(activeCard);
1002                    }, 50);
1003                } else {
1004                    //console.log('Nessuna maschera trovata su questa slide');
1005                }
1006            }
1007        };
1008
1009        // Avvia l'animazione quando il carousel entra in viewport (solo per la prima volta)
1010        let hasEnteredViewport = false;
1011        if ('IntersectionObserver' in window) {
1012            const observer = new IntersectionObserver((entries, obs) => {
1013                entries.forEach((entry) => {
1014                    if (entry.isIntersecting && !hasEnteredViewport) {
1015                        hasEnteredViewport = true;
1016                        triggerMaskAnimation();
1017                        obs.disconnect();
1018                    }
1019                });
1020            }, { root: null, threshold: 0.2 });
1021            observer.observe(carousel);
1022        } else {
1023            // Fallback: delay dopo load
1024            setTimeout(() => {
1025                if (!hasEnteredViewport) {
1026                    hasEnteredViewport = true;
1027                    triggerMaskAnimation();
1028                }
1029            }, 300);
1030        }
1031
1032        // Hook diretto nella funzione goToSlide per intercettare i cambi
1033        const originalGoToSlide = goToSlide;
1034        goToSlide = function(index) {
1035            const result = originalGoToSlide.call(this, index);
1036            // Trigger l'animazione dopo che la slide è cambiata
1037            setTimeout(() => {
1038                const activeCard = carousel.querySelector('.carousel-card.active');
1039                if (activeCard) {
1040                    triggerMaskAnimation();
1041                }
1042            }, 100);
1043            return result;
1044        };
1045    });
1046
1047})();
1048
1049// --- Utility: animazione maschera bolle per card attiva ---
1050function startCardMaskIntro(card) {
1051    if (!card) return;
1052    
1053    // Ottieni l'index della card corrente
1054    const cardIndex = card.dataset.index;
1055    //console.log('Avvio animazione per card index:', cardIndex);
1056    
1057    // Kill eventuali animazioni precedenti per evitare conflitti
1058    const svg = card.querySelector('.card-image .masked-svg');
1059    if (!svg) {
1060        //console.log('SVG non trovato in questa card');
1061        return;
1062    }
1063    
1064    // Kill solo i tween di questa specifica card e resetta tutti gli elementi
1065    const blobs = svg.querySelectorAll('.blob');
1066    blobs.forEach(blob => {
1067        gsap.killTweensOf(blob);
1068        // Forza il reset immediato
1069        blob.style.transform = '';
1070    });
1071    
1072    // Cerca i blob SOLO con l'index specifico di questa card
1073    const cBig = svg.querySelector(`.blob-big.blob-index-${cardIndex}`);
1074    const cUR  = svg.querySelector(`.blob-small-ur.blob-index-${cardIndex}`);
1075    const cBL  = svg.querySelector(`.blob-small-bl.blob-index-${cardIndex}`);
1076    if (!cBig || !cUR || !cBL) {
1077        //console.log('Blob non trovati per index:', cardIndex);
1078        return;
1079    }
1080    
1081    //console.log('Blob trovati per index', cardIndex, '- avvio animazione');
1082    //console.log('Big:', cBig, 'UR:', cUR, 'BL:', cBL);
1083
1084    // Valori finali di riferimento (come in card_carousel.php)
1085    const finalCircles = [
1086        { key: 'big',  cx: 500, cy: 500, r: 480 },
1087        { key: 'ur',   cx: 780, cy: 240, r: 200 },
1088        { key: 'bl',   cx: 240, cy: 780, r: 200 },
1089    ];
1090
1091    // Usa i valori finali definiti in "finalCircles"
1092    const targets = [
1093        { el: cBig, cx: finalCircles[0].cx, cy: finalCircles[0].cy, r: finalCircles[0].r },
1094        { el: cUR,  cx: finalCircles[1].cx, cy: finalCircles[1].cy, r: finalCircles[1].r },
1095        { el: cBL,  cx: finalCircles[2].cx, cy: finalCircles[2].cy, r: finalCircles[2].r },
1096    ];
1097
1098    // Imposta stato iniziale: tutti dal centro r=0 (solo per questa card)
1099    targets.forEach(({ el }) => {
1100        //console.log('Reset blob:', el);
1101        // Forza il reset con gsap.set per essere sicuri
1102        gsap.set(el, {
1103            attr: { cx: 500, cy: 500, r: 0 },
1104            clearProps: "transform"
1105        });
1106    });
1107
1108    // Helper: movimento continuo random nell'intera area del contenitore
1109    const wander = (el, pad, isBig = false, baseR = 200) => {
1110        const next = () => {
1111            // Per il cerchio big, genera PRIMA il nuovo raggio
1112            let nextRadius = baseR;
1113            if (isBig) {
1114                nextRadius = gsap.utils.random(baseR * 0.38, baseR * 1);
1115            }
1116            
1117            // Calcola limiti basandosi sul raggio che il cerchio AVRÀ nel prossimo stato
1118            // In questo modo il cerchio non esce mai dai bordi
1119            const min = nextRadius;
1120            const max = 1000 - nextRadius;
1121            
1122            // Movimento randomico entro i limiti calcolati
1123            const nx = Math.floor(gsap.utils.random(min, max, 1));
1124            const ny = Math.floor(gsap.utils.random(min, max, 1));
1125            
1126            // Durata randomica per tutti i cerchi
1127            const duration = gsap.utils.random(2.5, 4.5);
1128            
1129            // Costruisci gli attributi da animare
1130            const attr = { cx: nx, cy: ny };
1131            if (isBig) {
1132                attr.r = nextRadius; // Usa il raggio già calcolato
1133            }
1134            
1135            gsap.to(el, {
1136                duration: duration,
1137                ease: 'sine.inOut',
1138                attr: attr,
1139                onComplete: next
1140            });
1141        };
1142        next();
1143    };
1144
1145    // Animazioni sequenziali: ur -> bl -> big
1146    const tl = gsap.timeline();
1147
1148    // 1) UR parte subito: crescita + spostamento verso angolo alto-destra (3 secondi)
1149    tl.to(targets[1].el, {
1150        duration: 3,
1151        ease: 'power2.out',
1152        attr: { 
1153            cx: targets[1].cx, 
1154            cy: targets[1].cy, 
1155            r: targets[1].r 
1156        }
1157    }, 0);
1158
1159    // 2) BL parte dopo 2 secondi: crescita + spostamento verso angolo basso-sinistra (3 secondi)
1160    tl.to(targets[2].el, {
1161        duration: 3,
1162        ease: 'power2.out',
1163        attr: { 
1164            cx: targets[2].cx, 
1165            cy: targets[2].cy, 
1166            r: targets[2].r 
1167        }
1168    }, 0.7);
1169
1170    // 3) BIG parte dopo altri 2 secondi: crescita + spostamento verso centro (4 secondi)
1171    tl.to(targets[0].el, {
1172        duration: 4,
1173        ease: 'power2.out',
1174        attr: { 
1175            cx: targets[0].cx, 
1176            cy: targets[0].cy, 
1177            r: targets[0].r 
1178        }
1179    }, 2);
1180
1181    // 4) Avvia il movimento continuo di TUTTI i blob contemporaneamente
1182    tl.add(() => {
1183        // Tutti i blob iniziano il movimento continuo insieme
1184        targets.forEach(({ el, r }, i) => {
1185            const isBig = (i === 0);
1186            //wander(el, null, isBig, r);
1187        });
1188    }, 3.7); // Parte a 3.7s, verso la fine delle animazioni iniziali
1189
1190    // 5) NON segnare come animata per permettere riavvio
1191    // tl.call(() => { card.dataset.maskAnimated = 'true'; });
1192}
1193
1194/**
1195 * Rettangolo in coordinate utente del viewBox effettivamente visibile nel layout.
1196 * Con xMidYMid slice la viewport mostra solo la parte centrale del viewBox: i limiti 0–1000
1197 * non coincidono con l'area visibile e le bolle risultano tagliate se non si usa questo rettangolo.
1198 */
1199function iuadSvgSliceVisibleUserRect(svg) {
1200    if (!svg || !svg.viewBox || !svg.viewBox.baseVal) {
1201        return { minX: 0, maxX: 1000, minY: 0, maxY: 1000 };
1202    }
1203    const vb = svg.viewBox.baseVal;
1204    const vbx = vb.x;
1205    const vby = vb.y;
1206    const vbw = vb.width;
1207    const vbh = vb.height;
1208    const W = svg.clientWidth;
1209    const H = svg.clientHeight;
1210    if (!(W > 0) || !(H > 0) || !vbw || !vbh) {
1211        return { minX: vbx, maxX: vbx + vbw, minY: vby, maxY: vby + vbh };
1212    }
1213    const raw = (svg.getAttribute('preserveAspectRatio') || '').trim();
1214    const tokens = raw.split(/\s+/).filter(function (t) {
1215        return t && String(t).toLowerCase() !== 'defer';
1216    });
1217    const meetOrSlice = (tokens[1] || 'meet').toLowerCase();
1218    if (meetOrSlice !== 'slice') {
1219        return { minX: vbx, maxX: vbx + vbw, minY: vby, maxY: vby + vbh };
1220    }
1221    const s = Math.max(W / vbw, H / vbh);
1222    const ox = (W - vbw * s) / 2;
1223    const oy = (H - vbh * s) / 2;
1224    const uxLo = vbx - ox / s;
1225    const uxHi = vbx + (W - ox) / s;
1226    const uyLo = vby - oy / s;
1227    const uyHi = vby + (H - oy) / s;
1228    return {
1229        minX: Math.max(vbx, uxLo),
1230        maxX: Math.min(vbx + vbw, uxHi),
1231        minY: Math.max(vby, uyLo),
1232        maxY: Math.min(vby + vbh, uyHi)
1233    };
1234}
1235
1236function iuadHeroMaskCircleCenterBounds(vis, r, gooeyPad) {
1237    const g = typeof gooeyPad === 'number' ? gooeyPad : 0;
1238    const rad = typeof r === 'number' && r > 0 ? r : 0;
1239    let minCx = vis.minX + rad + g;
1240    let maxCx = vis.maxX - rad - g;
1241    let minCy = vis.minY + rad + g;
1242    let maxCy = vis.maxY - rad - g;
1243    if (maxCx < minCx) {
1244        const m = (vis.minX + vis.maxX) / 2;
1245        minCx = m;
1246        maxCx = m;
1247    }
1248    if (maxCy < minCy) {
1249        const m = (vis.minY + vis.maxY) / 2;
1250        minCy = m;
1251        maxCy = m;
1252    }
1253    return { minCx: minCx, maxCx: maxCx, minCy: minCy, maxCy: maxCy };
1254}
1255
1256function iuadClampCircleCenterToHeroMaskBounds(cx, cy, bounds) {
1257    return {
1258        cx: Math.min(bounds.maxCx, Math.max(bounds.minCx, cx)),
1259        cy: Math.min(bounds.maxCy, Math.max(bounds.minCy, cy))
1260    };
1261}
1262
1263
1264/**
1265 * Hero Mask Animation - Generic Function
1266 * Funzione generica per animare maschere SVG con blob su hero sections
1267 * 
1268 * @param {Object} config - Configurazione dell'animazione
1269 * @param {string} config.wrapperSelector - Selettore CSS del wrapper dell'hero
1270 * @param {string} config.svgSelector - Selettore CSS dell'SVG (default: '.hero-masked-svg')
1271 * @param {Object} config.blobConfig - Configurazione dei blob finali
1272 * @param {Object} config.initialState - Stato iniziale dei blob (default: centro con r=0)
1273 * @param {number} config.observerThreshold - Threshold per IntersectionObserver (default: 0.2)
1274 * @param {number} config.wanderGooeyPad - Margine in unità utente per dilatazione/blur della mask gooey (default: 48)
1275 */
1276function initHeroMaskAnimation(config) {
1277    'use strict';
1278
1279    const {
1280        wrapperSelector,
1281        svgSelector = '.hero-masked-svg',
1282        blobConfig = {
1283            big: { cx: 500, cy: 500, r: 430 },
1284            ur: { cx: 780, cy: 240, r: 180 },
1285            bl: { cx: 240, cy: 780, r: 180 }
1286        },
1287        initialState = {
1288            cx: 500,
1289            cy: 500,
1290            r: 0
1291        },
1292        observerThreshold = 0.2,
1293        wanderGooeyPad = 48
1294    } = config;
1295
1296    const heroWrapper = document.querySelector(wrapperSelector);
1297    
1298    if (!heroWrapper) return;
1299
1300    const svg = heroWrapper.querySelector(svgSelector);
1301    if (!svg) return;
1302
1303    const cBig = svg.querySelector('.hero-blob-big');
1304    const cUR = svg.querySelector('.hero-blob-small-ur');
1305    const cBL = svg.querySelector('.hero-blob-small-bl');
1306
1307    if (!cBig || !cUR || !cBL) {
1308        //console.log('Hero blob non trovati in', wrapperSelector);
1309        return;
1310    }
1311
1312    const startHeroMaskAnimation = () => {
1313        let layoutFrames = 0;
1314        const runWhenSvgHasLayout = () => {
1315            layoutFrames += 1;
1316            if ((svg.clientWidth > 0 && svg.clientHeight > 0) || layoutFrames >= 72) {
1317                startHeroMaskAnimationAfterLayout();
1318            } else {
1319                requestAnimationFrame(runWhenSvgHasLayout);
1320            }
1321        };
1322        requestAnimationFrame(runWhenSvgHasLayout);
1323    };
1324
1325    const startHeroMaskAnimationAfterLayout = () => {
1326        const finalCircles = [
1327            { key: 'big', ...blobConfig.big },
1328            { key: 'ur', ...blobConfig.ur },
1329            { key: 'bl', ...blobConfig.bl },
1330        ];
1331
1332        const targets = [
1333            { el: cBig, cx: finalCircles[0].cx, cy: finalCircles[0].cy, r: finalCircles[0].r },
1334            { el: cUR, cx: finalCircles[1].cx, cy: finalCircles[1].cy, r: finalCircles[1].r },
1335            { el: cBL, cx: finalCircles[2].cx, cy: finalCircles[2].cy, r: finalCircles[2].r },
1336        ];
1337
1338        const visIntro = iuadSvgSliceVisibleUserRect(svg);
1339        targets.forEach(function (t) {
1340            const b = iuadHeroMaskCircleCenterBounds(visIntro, t.r, wanderGooeyPad);
1341            const clamped = iuadClampCircleCenterToHeroMaskBounds(t.cx, t.cy, b);
1342            t.cx = clamped.cx;
1343            t.cy = clamped.cy;
1344        });
1345
1346        // Imposta stato iniziale personalizzabile
1347        targets.forEach(({ el }) => {
1348            gsap.set(el, {
1349                attr: { 
1350                    cx: initialState.cx, 
1351                    cy: initialState.cy, 
1352                    r: initialState.r 
1353                },
1354                clearProps: "transform"
1355            });
1356        });
1357
1358        const wander = (el, pad, isBig = false, baseR = 200) => {
1359            const next = () => {
1360                let nextRadius = baseR;
1361                if (isBig) {
1362                    nextRadius = gsap.utils.random(baseR * 0.38, baseR * 1);
1363                }
1364
1365                const vis = iuadSvgSliceVisibleUserRect(svg);
1366                const wb = iuadHeroMaskCircleCenterBounds(vis, nextRadius, wanderGooeyPad);
1367
1368                const nx = Math.floor(gsap.utils.random(wb.minCx, wb.maxCx, 1));
1369                const ny = Math.floor(gsap.utils.random(wb.minCy, wb.maxCy, 1));
1370                
1371                // Durata randomica per tutti i cerchi
1372                const duration = gsap.utils.random(2.5, 4.5);
1373                
1374                // Costruisci gli attributi da animare
1375                const attr = { cx: nx, cy: ny };
1376                if (isBig) {
1377                    attr.r = nextRadius; // Usa il raggio già calcolato
1378                }
1379                
1380                gsap.to(el, {
1381                    duration: duration,
1382                    ease: 'sine.inOut',
1383                    attr: attr,
1384                    onComplete: next
1385                });
1386            };
1387            next();
1388        };
1389
1390        // Animazioni sequenziali: ur -> bl -> big
1391        const tl = gsap.timeline();
1392
1393        // 1) UR parte subito: crescita + spostamento verso angolo alto-destra
1394        tl.to(targets[1].el, {
1395            duration: 3,
1396            ease: 'power2.out',
1397            attr: { 
1398                cx: targets[1].cx, 
1399                cy: targets[1].cy, 
1400                r: targets[1].r 
1401            }
1402        }, 0);
1403
1404        // 2) BL parte dopo 0.7s: crescita + spostamento verso angolo basso-sinistra
1405        tl.to(targets[2].el, {
1406            duration: 3,
1407            ease: 'power2.out',
1408            attr: { 
1409                cx: targets[2].cx, 
1410                cy: targets[2].cy, 
1411                r: targets[2].r 
1412            }
1413        }, 0.7);
1414
1415        // 3) BIG parte dopo 2s: crescita + spostamento verso centro (4 secondi)
1416        tl.to(targets[0].el, {
1417            duration: 4,
1418            ease: 'power2.out',
1419            attr: { 
1420                cx: targets[0].cx, 
1421                cy: targets[0].cy, 
1422                r: targets[0].r 
1423            }
1424        }, 2);
1425
1426        // 4) Avvia il movimento continuo di TUTTI i blob contemporaneamente
1427        tl.add(() => {
1428            // Tutti i blob iniziano il movimento continuo insieme
1429            targets.forEach(({ el, r }, i) => {
1430                const isBig = (i === 0);
1431                wander(el, null, isBig, r);
1432            });
1433        }, 3.7); // Parte a 3.7s, verso la fine delle animazioni iniziali
1434    };
1435
1436    // Avvia l'animazione quando l'hero entra in viewport
1437    let hasEnteredViewport = false;
1438    if ('IntersectionObserver' in window) {
1439        const observer = new IntersectionObserver((entries, obs) => {
1440            entries.forEach((entry) => {
1441                if (entry.isIntersecting && !hasEnteredViewport) {
1442                    hasEnteredViewport = true;
1443                    startHeroMaskAnimation();
1444                    obs.disconnect();
1445                }
1446            });
1447        }, { root: null, threshold: observerThreshold });
1448        observer.observe(heroWrapper);
1449    } else {
1450        // Fallback: delay dopo load
1451        setTimeout(() => {
1452            if (!hasEnteredViewport) {
1453                hasEnteredViewport = true;
1454                startHeroMaskAnimation();
1455            }
1456        }, 300);
1457    }
1458}
1459
1460
1461/**
1462 * Hero Corsi - Animazione maschera SVG
1463 * Inizializza l'animazione per l'hero dei corsi
1464*/
1465(function() {
1466    'use strict';
1467
1468    document.addEventListener('DOMContentLoaded', () => {
1469        initHeroMaskAnimation({
1470            wrapperSelector: '.hero_corsi_wrapper',
1471            blobConfig: {
1472                big: { cx: 500, cy: 500, r: 430 },
1473                ur: { cx: 780, cy: 240, r: 180 },
1474                bl: { cx: 240, cy: 780, r: 180 }
1475            },
1476            initialState: {
1477                cx: 500,
1478                cy: 500,
1479                r: 0
1480            }
1481        });
1482    });
1483
1484})();
1485
1486
1487/**
1488 * Hero Sedi - Animazione maschera SVG
1489 * Inizializza l'animazione per l'hero delle sedi
1490*/
1491(function() {
1492    'use strict';
1493
1494    document.addEventListener('DOMContentLoaded', () => {
1495        initHeroMaskAnimation({
1496            wrapperSelector: '.hero-sedi_wrapper',
1497            blobConfig: {
1498                big: { cx: 500, cy: 500, r: 380 },
1499                ur: { cx: 730, cy: 230, r: 130 },
1500                bl: { cx: 230, cy: 730, r: 130 }
1501            },
1502            initialState: {
1503                cx: 500,
1504                cy: 500,
1505                r: 0
1506            }
1507        });
1508    });
1509
1510})();
1511/**
1512 * Hero News - Animazione maschera SVG
1513 * Inizializza l'animazione per l'hero delle news
1514*/
1515(function() {
1516    'use strict';
1517
1518    document.addEventListener('DOMContentLoaded', () => {
1519        initHeroMaskAnimation({
1520            wrapperSelector: '.hero-news_wrapper',
1521            blobConfig: {
1522                big: { cx: 500, cy: 500, r: 380 },
1523                ur: { cx: 730, cy: 230, r: 130 },
1524                bl: { cx: 230, cy: 730, r: 130 }
1525            },
1526            initialState: {
1527                cx: 500,
1528                cy: 500,
1529                r: 0
1530            }
1531        });
1532    });
1533
1534})();
1535
1536
1537/**
1538 * Page Loader Animation
1539 * Animazione stile Tripletta Pizza: div impilati che appaiono dal basso
1540 * e poi scorrono tutti insieme verso l'alto come un sipario
1541 */
1542(function() {
1543    'use strict';
1544
1545    // Avvia appena il DOM è pronto (non aspettare il 'load' completo)
1546    document.addEventListener('DOMContentLoaded', () => {
1547        const loader = document.getElementById('pageLoader');
1548        
1549        if (!loader) return;
1550
1551        const container = loader.querySelector('.loader-container');
1552        const slides = loader.querySelectorAll('.loader-slide');
1553        
1554        if (slides.length === 0) return;
1555
1556        // Se la URL contiene un anchor, NON blocchiamo lo scroll:
1557        // il jump nativo verso l'anchor deve poter avvenire liberamente.
1558        const shouldLockScroll = !(window.location.hash && window.location.hash.length > 1);
1559
1560        // Blocca lo scroll finché il loader non è scomparso del tutto.
1561        // Nota: usiamo overflow su html/body e fermiamo Lenis se presente.
1562        const prevHtmlOverflow = document.documentElement.style.overflow;
1563        const prevBodyOverflow = document.body.style.overflow;
1564        const prevBodyPosition = document.body.style.position;
1565        const prevBodyWidth = document.body.style.width;
1566        const scrollYAtLock = window.scrollY || window.pageYOffset || 0;
1567        let scrollLocked = false;
1568
1569        function lockScroll() {
1570            if (!shouldLockScroll) return;
1571            if (scrollLocked) return;
1572            scrollLocked = true;
1573            document.body.classList.add('loading');
1574            document.documentElement.style.overflow = 'hidden';
1575            document.body.style.overflow = 'hidden';
1576            // iOS/Safari: prevenzione ulteriore
1577            document.body.style.position = 'fixed';
1578            document.body.style.top = (-scrollYAtLock) + 'px';
1579            document.body.style.width = '100%';
1580            if (window.lenis && typeof window.lenis.stop === 'function') {
1581                window.lenis.stop();
1582            }
1583        }
1584
1585        function unlockScroll() {
1586            if (!shouldLockScroll) return;
1587            if (!scrollLocked) return;
1588            scrollLocked = false;
1589            document.body.classList.remove('loading');
1590            document.documentElement.style.overflow = prevHtmlOverflow;
1591            document.body.style.overflow = prevBodyOverflow;
1592            document.body.style.position = prevBodyPosition;
1593            document.body.style.width = prevBodyWidth;
1594            document.body.style.top = '';
1595            if (window.lenis && typeof window.lenis.start === 'function') {
1596                window.lenis.start();
1597            }
1598
1599            // Se siamo entrati con un anchor (#...), NON ripristiniamo lo scroll pre-loader,
1600            // altrimenti annulliamo il jump verso l'anchor. In quel caso lasciamo gestire
1601            // lo scroll alla logica anchor post-unlock.
1602            if (window.location.hash && window.location.hash.length > 1) {
1603                if (typeof window.iuadScrollToInitialAnchor === 'function') {
1604                    window.iuadScrollToInitialAnchor();
1605                    setTimeout(() => window.iuadScrollToInitialAnchor(), 100);
1606                }
1607            } else {
1608                window.scrollTo(0, scrollYAtLock);
1609            }
1610        }
1611
1612        lockScroll();
1613
1614        // Imposta i colori di background e testo per ogni slide
1615        slides.forEach(slide => {
1616            const bgColor = slide.getAttribute('data-bg');
1617            const textColor = slide.getAttribute('data-color');
1618            slide.style.backgroundColor = bgColor;
1619            slide.style.color = textColor;
1620        });
1621
1622        const HERO_VIDEO_BREAKPOINT = 767;
1623
1624        function setHeroVideoForViewport() {
1625            const hero = document.getElementById('heroHome');
1626            const video = document.getElementById('heroVideo');
1627            if (!hero || !video) return;
1628            const isMobile = window.matchMedia('(max-width: ' + HERO_VIDEO_BREAKPOINT + 'px)').matches;
1629            const url = isMobile ? hero.getAttribute('data-video-mobile') : hero.getAttribute('data-video-desktop');
1630            const type = isMobile ? (hero.getAttribute('data-video-mobile-type') || 'video/mp4') : (hero.getAttribute('data-video-desktop-type') || 'video/mp4');
1631            if (!url) return;
1632            video.pause();
1633            video.innerHTML = '<source src="' + url + '" type="' + type + '">';
1634            video.load();
1635            video.classList.add('loaded');
1636            video.play().catch(err => {
1637                //console.log('Autoplay prevented:', err);
1638            });
1639        }
1640
1641        let heroVideoViewportMobile = null;
1642        function onHeroVideoResize() {
1643            const isMobile = window.matchMedia('(max-width: ' + HERO_VIDEO_BREAKPOINT + 'px)').matches;
1644            if (heroVideoViewportMobile !== isMobile) {
1645                heroVideoViewportMobile = isMobile;
1646                setHeroVideoForViewport();
1647            }
1648        }
1649        window.addEventListener('resize', onHeroVideoResize);
1650
1651        const mainTimeline = gsap.timeline({
1652            delay: 0.2,
1653            onComplete: () => {
1654                loader.classList.add('loaded');
1655                loader.style.display = 'none';
1656                unlockScroll();
1657
1658                setHeroVideoForViewport();
1659                heroVideoViewportMobile = window.matchMedia('(max-width: ' + HERO_VIDEO_BREAKPOINT + 'px)').matches;
1660
1661                triggerHomeEntryAnimations();
1662            }
1663        });
1664
1665        // Animazione di ogni slide che appare (senza movimento, solo opacity)
1666        slides.forEach((slide, index) => {
1667            const isFinal = slide.classList.contains('final');
1668            const displayDuration = isFinal ? 0.8 : 0.4; // Quanto tempo resta visibile
1669
1670            // Per la prima slide, è già visibile, quindi solo aspetta
1671            if (index === 0) {
1672                mainTimeline.to({}, { duration: displayDuration });
1673                mainTimeline.add(() => {
1674                    slide.classList.remove('active');
1675                });
1676            } else {
1677                // Mostra la slide (aggiunge classe active)
1678                mainTimeline.add(() => {
1679                    slide.classList.add('active');
1680                });
1681
1682                // Pausa mentre la slide è visibile
1683                mainTimeline.to({}, { duration: displayDuration });
1684
1685                // Nascondi la slide (rimuove classe active) - solo se non è l'ultima
1686                if (index < slides.length - 1) {
1687                    mainTimeline.add(() => {
1688                        slide.classList.remove('active');
1689                    });
1690                }
1691            }
1692        });
1693
1694        // Prima dell'animazione del sipario, mostra il sito dietro
1695        mainTimeline.add(() => {
1696            // Mostra il contenuto dietro il loader
1697            gsap.set('#wrapper-navbar, .site-content, .hero-home', {
1698                opacity: 1,
1699                visibility: 'visible'
1700            });
1701        });
1702
1703        // Animazione finale: tutto il container scorre verso l'alto (sipario che si alza)
1704        // Ora mentre sale, si vede il sito sotto
1705        mainTimeline.to(container, {
1706            y: '-100%',
1707            duration: 1.2,
1708            ease: 'power3.inOut'
1709        });
1710    });
1711
1712})();
1713
1714
1715/**
1716 * Home Entry Animations
1717 * Animazioni bounce per header e scroll button dopo il loader
1718 * Solo sulla home page
1719 */
1720function triggerHomeEntryAnimations() {
1721    'use strict';
1722    
1723    // Verifica se siamo in home page
1724    const isHome = document.body.classList.contains('home') || 
1725                   document.body.classList.contains('page-template-template-home');
1726    
1727    if (!isHome) return;
1728    
1729    const header = document.querySelector('.nd-desktop-header');
1730    const scrollBtn = document.querySelector('.hero-home__scroll-btn');
1731    
1732    // Animazione header: bounce dall'alto
1733    if (header) {
1734        gsap.fromTo(header, 
1735            {
1736                y: -100,
1737                opacity: 0
1738            },
1739            {
1740                y: 0,
1741                opacity: 1,
1742                duration: 0.8,
1743                ease: 'back.out(1.7)',
1744                delay: 0.3
1745            }
1746        );
1747    }
1748    
1749    // Animazione scroll button: bounce dal basso
1750    if (scrollBtn) {
1751        gsap.fromTo(scrollBtn, 
1752            {
1753                y: 100,
1754                opacity: 0
1755            },
1756            {
1757                y: 0,
1758                opacity: 1,
1759                duration: 0.8,
1760                ease: 'back.out(1.7)',
1761                delay: 0.5
1762            }
1763        );
1764    }
1765}
1766
1767
1768/**
1769 * Background Change Animation Trigger
1770 * Attiva l'animazione di cambio colore background sincronizzata su tutti gli elementi
1771 * quando il primo elemento entra in viewport
1772 * Uso: aggiungi la classe 'bg-change-trigger' a qualsiasi elemento
1773 */
1774(function() {
1775    'use strict';
1776
1777    // Flag globale per tracciare se l'animazione è già stata avviata
1778    let hasAnimated = false;
1779    let observer = null;
1780
1781    /**
1782     * Funzione helper per verificare se un elemento è visibile
1783     * (non dentro un template responsive nascosto)
1784     */
1785    function isElementVisible(element) {
1786        const computedStyle = window.getComputedStyle(element);
1787        if (computedStyle.display === 'none' || computedStyle.visibility === 'hidden') {
1788            return false;
1789        }
1790        
1791        // Verifica se è dentro un template responsive nascosto
1792        const mobileWrapper = element.closest('.responsive-template-mobile');
1793        if (mobileWrapper && window.getComputedStyle(mobileWrapper).display === 'none') {
1794            return false;
1795        }
1796        
1797        const desktopWrapper = element.closest('.responsive-template-desktop');
1798        if (desktopWrapper && window.getComputedStyle(desktopWrapper).display === 'none') {
1799            return false;
1800        }
1801        
1802        return true;
1803    }
1804
1805    /**
1806     * Funzione per inizializzare l'observer
1807     */
1808    function initBgChangeAnimation() {
1809        // Se l'animazione è già stata avviata, non reinizializzare
1810        if (hasAnimated) return;
1811
1812        // Trova tutti gli elementi con classe bg-change-trigger
1813        const allTriggers = document.querySelectorAll('.bg-change-trigger');
1814        
1815        if (allTriggers.length === 0) return;
1816
1817        // Filtra solo gli elementi visibili (non dentro template nascosti)
1818        const triggers = Array.from(allTriggers).filter(trigger => isElementVisible(trigger));
1819        
1820        if (triggers.length === 0) return;
1821
1822        // Se c'è già un observer attivo, disconnettilo
1823        if (observer) {
1824            observer.disconnect();
1825        }
1826
1827        // Trova il primo trigger visibile
1828        const firstTrigger = triggers[0];
1829
1830        // Crea un IntersectionObserver per il primo elemento
1831        observer = new IntersectionObserver((entries) => {
1832            entries.forEach((entry) => {
1833                // Quando il primo elemento entra in viewport e non è ancora stato animato
1834                if (entry.isIntersecting && !hasAnimated) {
1835                    // Trova di nuovo tutti i trigger (potrebbero essere cambiati)
1836                    const allTriggersNow = document.querySelectorAll('.bg-change-trigger');
1837                    const visibleTriggers = Array.from(allTriggersNow).filter(trigger => isElementVisible(trigger));
1838                    
1839                    // Aggiungi la classe a TUTTI gli elementi con bg-change-trigger visibili contemporaneamente
1840                    visibleTriggers.forEach((trigger) => {
1841                        trigger.classList.add('bg-change-animation');
1842                    });
1843
1844                    if (document.body.classList.contains('bg-change-sync-target')) {
1845                        document.body.classList.remove('bg-change-animation');
1846                        void document.body.offsetWidth;
1847                        document.body.classList.add('bg-change-animation');
1848                    }
1849                    
1850                    hasAnimated = true;
1851                    
1852                    // Disconnetti l'observer dopo aver aggiunto la classe
1853                    // L'animazione continuerà in loop infinito su tutti gli elementi
1854                    if (observer) {
1855                        observer.disconnect();
1856                    }
1857                    
1858                    //console.log(`Background animation started synchronously for ${visibleTriggers.length} elements`);
1859                }
1860            });
1861        }, {
1862            root: null, // viewport
1863            threshold: 0.2, // 20% dell'elemento deve essere visibile
1864            rootMargin: '0px'
1865        });
1866
1867        // Osserva solo il primo elemento visibile
1868        observer.observe(firstTrigger);
1869    }
1870
1871    // Inizializza al DOMContentLoaded
1872    document.addEventListener('DOMContentLoaded', () => {
1873        initBgChangeAnimation();
1874    });
1875
1876    // Reinizializza quando i template responsive cambiano visibilità
1877    // Usa un MutationObserver per rilevare cambiamenti di display
1878    if (typeof MutationObserver !== 'undefined') {
1879        const mutationObserver = new MutationObserver(() => {
1880            // Se l'animazione non è ancora partita, riprova
1881            if (!hasAnimated) {
1882                setTimeout(() => {
1883                    initBgChangeAnimation();
1884                }, 100);
1885            }
1886        });
1887
1888        // Osserva cambiamenti nel DOM
1889        mutationObserver.observe(document.body, {
1890            childList: true,
1891            subtree: true,
1892            attributes: true,
1893            attributeFilter: ['style', 'class']
1894        });
1895    }
1896
1897    // Reinizializza anche su resize (per template responsive)
1898    let resizeTimeout;
1899    window.addEventListener('resize', () => {
1900        clearTimeout(resizeTimeout);
1901        resizeTimeout = setTimeout(() => {
1902            if (!hasAnimated) {
1903                initBgChangeAnimation();
1904            }
1905        }, 200);
1906    });
1907
1908})();
1909
1910
1911/**
1912 * Background and Text Change Animation Trigger
1913 * Attiva l'animazione di cambio colore background E testo sincronizzata su tutti gli elementi
1914 * quando il primo elemento entra in viewport
1915 * Uso: aggiungi la classe 'bg-text-change-trigger' all'elemento contenitore
1916 *      e 'text-color-change' agli elementi di testo al suo interno
1917 */
1918(function() {
1919    'use strict';
1920
1921    // Flag globale per tracciare se l'animazione è già stata avviata
1922    let hasAnimated = false;
1923    let observer = null;
1924
1925    /**
1926     * Funzione helper per verificare se un elemento è visibile
1927     * (non dentro un template responsive nascosto)
1928     */
1929    function isElementVisible(element) {
1930        const computedStyle = window.getComputedStyle(element);
1931        if (computedStyle.display === 'none' || computedStyle.visibility === 'hidden') {
1932            return false;
1933        }
1934        
1935        // Verifica se è dentro un template responsive nascosto
1936        const mobileWrapper = element.closest('.responsive-template-mobile');
1937        if (mobileWrapper && window.getComputedStyle(mobileWrapper).display === 'none') {
1938            return false;
1939        }
1940        
1941        const desktopWrapper = element.closest('.responsive-template-desktop');
1942        if (desktopWrapper && window.getComputedStyle(desktopWrapper).display === 'none') {
1943            return false;
1944        }
1945        
1946        return true;
1947    }
1948
1949    /**
1950     * Funzione per inizializzare l'observer
1951     */
1952    function initBgTextChangeAnimation() {
1953        // Se l'animazione è già stata avviata, non reinizializzare
1954        if (hasAnimated) return;
1955
1956        // Trova tutti gli elementi con classe bg-text-change-trigger
1957        const allTriggers = document.querySelectorAll('.bg-text-change-trigger');
1958        
1959        if (allTriggers.length === 0) return;
1960
1961        // Filtra solo gli elementi visibili (non dentro template nascosti)
1962        const triggers = Array.from(allTriggers).filter(trigger => isElementVisible(trigger));
1963        
1964        if (triggers.length === 0) return;
1965
1966        // Se c'è già un observer attivo, disconnettilo
1967        if (observer) {
1968            observer.disconnect();
1969        }
1970
1971        // Trova il primo trigger visibile
1972        const firstTrigger = triggers[0];
1973
1974        // Crea un IntersectionObserver per il primo elemento
1975        observer = new IntersectionObserver((entries) => {
1976            entries.forEach((entry) => {
1977                // Quando il primo elemento entra in viewport e non è ancora stato animato
1978                if (entry.isIntersecting && !hasAnimated) {
1979                    // Trova di nuovo tutti i trigger (potrebbero essere cambiati)
1980                    const allTriggersNow = document.querySelectorAll('.bg-text-change-trigger');
1981                    const visibleTriggers = Array.from(allTriggersNow).filter(trigger => isElementVisible(trigger));
1982                    
1983                    // Aggiungi la classe background a TUTTI gli elementi con bg-text-change-trigger visibili
1984                    visibleTriggers.forEach((trigger) => {
1985                        trigger.classList.add('bg-change-animation');
1986                        
1987                        // Trova gli elementi di testo all'interno e aggiungi l'animazione del colore
1988                        const textElements = trigger.querySelectorAll('.text-color-change');
1989                        textElements.forEach((textElement) => {
1990                            textElement.classList.add('text-color-change-animation');
1991                        });
1992                    });
1993
1994                    if (document.body.classList.contains('bg-change-sync-target')) {
1995                        document.body.classList.remove('bg-change-animation');
1996                        void document.body.offsetWidth;
1997                        document.body.classList.add('bg-change-animation');
1998                    }
1999                    
2000                    hasAnimated = true;
2001                    
2002                    // Disconnetti l'observer dopo aver aggiunto le classi
2003                    // Le animazioni continueranno in loop infinito sincronizzate
2004                    if (observer) {
2005                        observer.disconnect();
2006                    }
2007                    
2008                    //console.log(`Background and text animation started synchronously for ${visibleTriggers.length} elements`);
2009                }
2010            });
2011        }, {
2012            root: null, // viewport
2013            threshold: 0.2, // 20% dell'elemento deve essere visibile
2014            rootMargin: '0px'
2015        });
2016
2017        // Osserva solo il primo elemento visibile
2018        observer.observe(firstTrigger);
2019    }
2020
2021    // Inizializza al DOMContentLoaded
2022    document.addEventListener('DOMContentLoaded', () => {
2023        initBgTextChangeAnimation();
2024    });
2025
2026    // Reinizializza quando i template responsive cambiano visibilità
2027    // Usa un MutationObserver per rilevare cambiamenti di display
2028    if (typeof MutationObserver !== 'undefined') {
2029        const mutationObserver = new MutationObserver(() => {
2030            // Se l'animazione non è ancora partita, riprova
2031            if (!hasAnimated) {
2032                setTimeout(() => {
2033                    initBgTextChangeAnimation();
2034                }, 100);
2035            }
2036        });
2037
2038        // Osserva cambiamenti nel DOM
2039        mutationObserver.observe(document.body, {
2040            childList: true,
2041            subtree: true,
2042            attributes: true,
2043            attributeFilter: ['style', 'class']
2044        });
2045    }
2046
2047    // Reinizializza anche su resize (per template responsive)
2048    let resizeTimeout;
2049    window.addEventListener('resize', () => {
2050        clearTimeout(resizeTimeout);
2051        resizeTimeout = setTimeout(() => {
2052            if (!hasAnimated) {
2053                initBgTextChangeAnimation();
2054            }
2055        }, 200);
2056    });
2057
2058})();
2059
2060
2061/**
2062 * Dipartimenti Block - Scroll Pinned Animation
2063 * Animazione sincronizzata con lo scroll: i blocchi salgono dal basso mentre la sezione è pinnata
2064 * La pagina riprende a scrollare solo quando tutti i blocchi hanno raggiunto la loro posizione
2065 */
2066(function() {
2067    'use strict';
2068
2069    document.addEventListener('DOMContentLoaded', () => {
2070        const dipartimentiBlock = document.querySelector('.dipartimenti-block');
2071        
2072        if (!dipartimentiBlock) return;
2073        
2074        // Verifica che l'elemento sia visibile (non nascosto da display:none)
2075        const isVisible = window.getComputedStyle(dipartimentiBlock).display !== 'none';
2076        if (!isVisible) {
2077            // Se non è visibile, aspetta che diventi visibile (per template responsive dinamici)
2078            const checkVisibility = () => {
2079                const checkVisible = window.getComputedStyle(dipartimentiBlock).display !== 'none';
2080                if (checkVisible) {
2081                    initDipartimentiAnimation();
2082                } else {
2083                    setTimeout(checkVisibility, 100);
2084                }
2085            };
2086            checkVisibility();
2087            return;
2088        }
2089
2090        initDipartimentiAnimation();
2091    });
2092    
2093    function initDipartimentiAnimation() {
2094        const dipartimentiBlock = document.querySelector('.dipartimenti-block');
2095        if (!dipartimentiBlock) return;
2096        
2097        // Verifica nuovamente la visibilità prima di inizializzare
2098        if (window.getComputedStyle(dipartimentiBlock).display === 'none') {
2099            return;
2100        }
2101
2102        const cards = dipartimentiBlock.querySelectorAll('.dipartimento-card');
2103        
2104        if (cards.length === 0) return;
2105
2106        // Registra ScrollTrigger plugin
2107        gsap.registerPlugin(ScrollTrigger);
2108
2109        // Definisci gli offset iniziali per ogni blocco
2110        const initialOffsets = [0, 20, 10, 30]; // px per blocchi 1, 2, 3, 4
2111        
2112        // Controlla se siamo su mobile
2113        const isMobile = window.innerWidth <= 1024;
2114
2115        // Timeline principale per l'animazione
2116        const tl = gsap.timeline({
2117            scrollTrigger: {
2118                trigger: dipartimentiBlock,
2119                start: 'top top',
2120                end: '+=200%', // L'animazione dura per 200% dell'altezza del viewport
2121                pin: true, // Pinna la sezione durante l'animazione
2122                scrub: 1, // Sincronizza l'animazione con lo scroll (1 = smooth)
2123                anticipatePin: 1,
2124                // markers: true, // Decommentare per debug
2125            }
2126        });
2127
2128        // FASE 1: Card 1 e 3 salgono per prime (partenza anticipata)
2129        [0, 2].forEach((index) => {
2130            const card = cards[index];
2131            const compensation = isMobile ? 0 : -initialOffsets[index];
2132            
2133            tl.to(card, {
2134                y: compensation, // Arrivano alla posizione finale allineata
2135                duration: 0.6,
2136                ease: 'power2.out'
2137            }, 0); // Partono subito
2138        });
2139
2140        // FASE 2: Card 2 e 4 salgono dopo (partenza ritardata)
2141        [1, 3].forEach((index) => {
2142            const card = cards[index];
2143            const compensation = isMobile ? 0 : -initialOffsets[index];
2144            
2145            tl.to(card, {
2146                y: compensation, // Arrivano alla posizione finale allineata
2147                duration: 0.6,
2148                ease: 'power2.out'
2149            }, 0.1); // Partono 0.1s dopo (ridotto da 0.3s)
2150        });
2151
2152        // Aggiungi classe animated a tutte le card alla fine
2153        tl.call(() => {
2154            cards.forEach(card => card.classList.add('animated'));
2155        });
2156
2157        // Pausa finale prima di rilasciare il pin
2158        tl.to({}, { duration: 0.4 });
2159
2160        //console.log('Dipartimenti block scroll animation initialized');
2161    }
2162
2163})();
2164
2165
2166/**
2167 * Carousel Sedi - Infinite Loop Marquee
2168 * Animazione infinita continua senza stop per il carousel di sedi e dipartimenti
2169 */
2170(function() {
2171    'use strict';
2172
2173    document.addEventListener('DOMContentLoaded', () => {
2174        const carousel = document.querySelector('.carousel-sedi-block');
2175        
2176        if (!carousel) return;
2177        
2178        // Verifica che l'elemento sia visibile (non nascosto da display:none)
2179        const isVisible = window.getComputedStyle(carousel).display !== 'none';
2180        if (!isVisible) {
2181            // Se non è visibile, aspetta che diventi visibile (per template responsive dinamici)
2182            const checkVisibility = () => {
2183                const checkVisible = window.getComputedStyle(carousel).display !== 'none';
2184                if (checkVisible) {
2185                    initCarouselSediAnimation();
2186                } else {
2187                    setTimeout(checkVisibility, 100);
2188                }
2189            };
2190            checkVisibility();
2191            return;
2192        }
2193
2194        initCarouselSediAnimation();
2195    });
2196    
2197    function initCarouselSediAnimation() {
2198        const carousel = document.querySelector('.carousel-sedi-block');
2199        if (!carousel) return;
2200        
2201        // Verifica nuovamente la visibilità prima di inizializzare
2202        if (window.getComputedStyle(carousel).display === 'none') {
2203            return;
2204        }
2205
2206        const track = carousel.querySelector('.carousel-sedi-track');
2207        
2208        if (!track) return;
2209
2210        // Registra GSAP plugin
2211        gsap.registerPlugin(ScrollTrigger);
2212
2213        // Calcola la larghezza totale di un set di item (1/3 del totale perché abbiamo duplicato 3 volte)
2214        const items = track.querySelectorAll('.carousel-sedi-item');
2215        const totalItems = items.length;
2216        const itemsPerSet = totalItems / 3;
2217        
2218        // Calcola la larghezza di un singolo set
2219        let setWidth = 0;
2220        for (let i = 0; i < itemsPerSet; i++) {
2221            setWidth += items[i].offsetWidth;
2222            // Aggiungi anche il gap (tranne per l'ultimo)
2223            if (i < itemsPerSet - 1) {
2224                const gap = parseInt(window.getComputedStyle(track).gap) || 80;
2225                setWidth += gap;
2226            }
2227        }
2228
2229        // Durata dell'animazione (in secondi) - più basso = più veloce
2230        const duration = 40;
2231
2232        // Crea l'animazione infinita con GSAP
2233        gsap.to(track, {
2234            x: -setWidth,
2235            duration: duration,
2236            ease: 'none', // Linear per movimento costante
2237            repeat: -1, // Ripeti all'infinito
2238            modifiers: {
2239                x: gsap.utils.unitize(x => parseFloat(x) % setWidth) // Seamless loop
2240            }
2241        });
2242
2243        //console.log('Carousel sedi infinite loop initialized');
2244    }
2245
2246})();
2247
2248
2249/**
2250 * Exchange Program Block - Card Opening Animation
2251 * Le card si aprono ai lati rivelando il testo centrale durante lo scroll
2252 */
2253(function() {
2254    'use strict';
2255
2256    document.addEventListener('DOMContentLoaded', () => {
2257        const exchangeBlock = document.querySelector('.exchange-program-block');
2258        
2259        if (!exchangeBlock) return;
2260        
2261        // Verifica che l'elemento sia visibile (non nascosto da display:none)
2262        const isVisible = window.getComputedStyle(exchangeBlock).display !== 'none';
2263        if (!isVisible) {
2264            // Se non è visibile, aspetta che diventi visibile (per template responsive dinamici)
2265            const checkVisibility = () => {
2266                const checkVisible = window.getComputedStyle(exchangeBlock).display !== 'none';
2267                if (checkVisible) {
2268                    initExchangeProgramAnimation();
2269                } else {
2270                    setTimeout(checkVisibility, 100);
2271                }
2272            };
2273            checkVisibility();
2274            return;
2275        }
2276
2277        initExchangeProgramAnimation();
2278    });
2279    
2280    function initExchangeProgramAnimation() {
2281        const exchangeBlock = document.querySelector('.exchange-program-block');
2282        if (!exchangeBlock) return;
2283        
2284        // Verifica nuovamente la visibilità prima di inizializzare
2285        if (window.getComputedStyle(exchangeBlock).display === 'none') {
2286            return;
2287        }
2288
2289        const cards = exchangeBlock.querySelectorAll('.exchange-card');
2290        const content = exchangeBlock.querySelector('.exchange-program-content');
2291        
2292        if (cards.length === 0) return;
2293
2294        // Registra ScrollTrigger plugin
2295        gsap.registerPlugin(ScrollTrigger);
2296
2297        // Controlla se siamo su mobile
2298        const isMobile = window.innerWidth <= 1024;
2299
2300        if (isMobile) {
2301            // Su mobile non fare animazione
2302            return;
2303        }
2304
2305        // Il contenuto centrale è sempre visibile (nessuna animazione)
2306        gsap.set(content, { opacity: 1, scale: 1 });
2307
2308        // Timeline principale per l'animazione
2309        const tl = gsap.timeline({
2310            scrollTrigger: {
2311                trigger: exchangeBlock,
2312                start: 'top top',
2313                end: '+=150%',
2314                pin: true,
2315                scrub: 1,
2316                anticipatePin: 1,
2317                // markers: true, // Decommentare per debug
2318            }
2319        });
2320
2321        // Le card si aprono spostandosi verso le loro posizioni finali
2322        cards.forEach((card) => {
2323            const cardNumber = card.getAttribute('data-card');
2324            const computedStyle = getComputedStyle(card);
2325            
2326            // Leggi le posizioni finali dalle CSS variables
2327            const finalTop = computedStyle.getPropertyValue('--final-top').trim();
2328            const finalLeft = computedStyle.getPropertyValue('--final-left').trim();
2329            const finalRight = computedStyle.getPropertyValue('--final-right').trim();
2330            
2331            // Prepara l'oggetto di animazione
2332            const animationProps = {
2333                duration: 0.8,
2334                ease: 'power2.out',
2335                force3D: true // Mantiene accelerazione hardware
2336            };
2337            
2338            // Anima verso la posizione finale usando top e left/right
2339            if (finalTop) {
2340                animationProps.top = finalTop;
2341            }
2342            
2343            if (finalLeft) {
2344                animationProps.left = finalLeft;
2345            } else if (finalRight) {
2346                animationProps.right = finalRight;
2347            }
2348            
2349            tl.to(card, animationProps, 0); // Tutte le card si muovono insieme
2350        });
2351
2352        // Pausa finale
2353        tl.to({}, { duration: 0.4 });
2354
2355        //console.log('Exchange Program block animation initialized');
2356    }
2357
2358})();
2359
2360
2361/**
2362 * Portfolio Home Block - Vertical Scroll Animation
2363 * Le card appaiono dal basso e salgono durante lo scroll, come se passassero attraverso una finestra
2364 */
2365(function() {
2366    'use strict';
2367
2368    document.addEventListener('DOMContentLoaded', () => {
2369        const portfolioBlock = document.querySelector('.portfolio-home-block');
2370        
2371        if (!portfolioBlock) return;
2372        
2373        // Verifica che l'elemento sia visibile (non nascosto da display:none)
2374        const isVisible = window.getComputedStyle(portfolioBlock).display !== 'none';
2375        if (!isVisible) {
2376            // Se non è visibile, aspetta che diventi visibile (per template responsive dinamici)
2377            const checkVisibility = () => {
2378                const checkVisible = window.getComputedStyle(portfolioBlock).display !== 'none';
2379                if (checkVisible) {
2380                    initPortfolioHomeAnimation();
2381                } else {
2382                    setTimeout(checkVisibility, 100);
2383                }
2384            };
2385            checkVisibility();
2386            return;
2387        }
2388
2389        initPortfolioHomeAnimation();
2390    });
2391    
2392    function initPortfolioHomeAnimation() {
2393        const portfolioBlock = document.querySelector('.portfolio-home-block');
2394        if (!portfolioBlock) return;
2395        
2396        // Verifica nuovamente la visibilità prima di inizializzare
2397        if (window.getComputedStyle(portfolioBlock).display === 'none') {
2398            return;
2399        }
2400
2401        const cardsContainer = portfolioBlock.querySelector('.portfolio-cards-container');
2402        const viewport = portfolioBlock.querySelector('.portfolio-scroll-viewport');
2403        
2404        if (!cardsContainer || !viewport) return;
2405
2406        // Registra ScrollTrigger plugin
2407        gsap.registerPlugin(ScrollTrigger);
2408
2409        // Controlla se siamo su mobile
2410        const isMobile = window.innerWidth <= 1024;
2411
2412        if (isMobile) {
2413            // Su mobile non fare animazione
2414            return;
2415        }
2416
2417        // Calcola l'altezza totale del contenuto
2418        const containerHeight = cardsContainer.offsetHeight;
2419        const viewportHeight = viewport.offsetHeight;
2420        
2421        // Distanza totale che le card devono percorrere
2422        // Devono partire da sotto la viewport e uscire completamente sopra
2423        const travelDistance = containerHeight + viewportHeight;
2424
2425        // Imposta la posizione iniziale: card posizionate sotto la viewport
2426        gsap.set(cardsContainer, { 
2427            y: viewportHeight 
2428        });
2429
2430        // Timeline principale per l'animazione
2431        const tl = gsap.timeline({
2432            scrollTrigger: {
2433                trigger: portfolioBlock,
2434                start: 'top top',
2435                end: '+=300%', // L'animazione dura per 300% dell'altezza del viewport
2436                pin: true, // Pinna la sezione durante l'animazione
2437                scrub: 1, // Sincronizza l'animazione con lo scroll (1 = smooth)
2438                anticipatePin: 1,
2439                // markers: true, // Decommentare per debug
2440            }
2441        });
2442
2443        // Anima il container delle card dal basso verso l'alto
2444        tl.to(cardsContainer, {
2445            y: -containerHeight, // Le card escono completamente dalla viewport
2446            ease: 'none', // Linear per un movimento costante
2447            duration: 1
2448        });
2449
2450        //console.log('Portfolio Home block vertical scroll animation initialized');
2451    }
2452
2453})();
2454
2455
2456/**
2457 * Portfolio Listing Hero - Vertical Scroll Animation
2458 * Le card appaiono dal basso e salgono durante lo scroll, come se passassero attraverso una finestra
2459 */
2460(function() {
2461    'use strict';
2462
2463    let portfolioScrollTrigger = null;
2464    let portfolioTimeline = null;
2465    let portfolioCardsContainer = null;
2466    let portfolioViewport = null;
2467
2468    function initPortfolioListingHero() {
2469        const portfolioListingHero = document.querySelector('.portfolio-listing-hero');
2470        
2471        if (!portfolioListingHero) return;
2472
2473        // Verifica che l'elemento sia visibile (non nascosto da display:none)
2474        const isVisible = window.getComputedStyle(portfolioListingHero).display !== 'none';
2475        if (!isVisible) {
2476            // Se non è visibile, aspetta che diventi visibile (per template responsive dinamici)
2477            const checkVisibility = () => {
2478                const checkVisible = window.getComputedStyle(portfolioListingHero).display !== 'none';
2479                if (checkVisible) {
2480                    initPortfolioListingHero();
2481                } else {
2482                    setTimeout(checkVisibility, 100);
2483                }
2484            };
2485            checkVisibility();
2486            return;
2487        }
2488
2489        portfolioCardsContainer = portfolioListingHero.querySelector('.portfolio-listing-cards-container');
2490        portfolioViewport = portfolioListingHero.querySelector('.portfolio-listing-scroll-viewport');
2491        
2492        if (!portfolioCardsContainer || !portfolioViewport) return;
2493
2494        // Registra ScrollTrigger plugin
2495        gsap.registerPlugin(ScrollTrigger);
2496
2497        // Controlla se siamo su mobile
2498        const isMobile = window.innerWidth <= 1024;
2499
2500        if (isMobile) {
2501            // Su mobile non fare animazione
2502            return;
2503        }
2504
2505        // Se esiste già un ScrollTrigger, killalo
2506        if (portfolioScrollTrigger) {
2507            portfolioScrollTrigger.kill();
2508        }
2509
2510        // Calcola l'altezza totale del contenuto
2511        const containerHeight = portfolioCardsContainer.offsetHeight;
2512        const viewportHeight = portfolioViewport.offsetHeight;
2513
2514        // Imposta la posizione iniziale: card posizionate sotto la viewport
2515        gsap.set(portfolioCardsContainer, { 
2516            y: viewportHeight 
2517        });
2518
2519        // Calcola la distanza di scroll in base all'altezza del contenuto
2520        // La distanza di scroll deve essere proporzionale all'altezza del contenuto
2521        // per creare un effetto parallax fluido
2522        const scrollDistance = containerHeight + viewportHeight;
2523
2524        // Timeline principale per l'animazione
2525        portfolioTimeline = gsap.timeline({
2526            scrollTrigger: {
2527                trigger: portfolioListingHero,
2528                start: 'top top',
2529                end: `+=${scrollDistance}px`, // Distanza di scroll proporzionale all'altezza del contenuto
2530                pin: portfolioListingHero, // Pinna l'intera section
2531                scrub: true, // Sincronizza l'animazione con lo scroll (true = più fluido)
2532                anticipatePin: 1,
2533                // markers: true, // Decommentare per debug
2534            }
2535        });
2536
2537        // Salva l'istanza ScrollTrigger
2538        portfolioScrollTrigger = portfolioTimeline.scrollTrigger;
2539
2540        // Anima il container delle card dal basso verso l'alto
2541        portfolioTimeline.to(portfolioCardsContainer, {
2542            y: -containerHeight, // Le card escono completamente dalla viewport
2543            ease: 'none', // Linear per un movimento costante
2544            duration: 1
2545        });
2546
2547        //console.log('Portfolio Listing Hero vertical scroll animation initialized');
2548    }
2549
2550    // Funzione per ricalcolare l'animazione dopo il filtro
2551    window.recalculatePortfolioListingHero = function() {
2552        if (!portfolioCardsContainer || !portfolioViewport) {
2553            initPortfolioListingHero();
2554            return;
2555        }
2556
2557        // Controlla se siamo su mobile
2558        const isMobile = window.innerWidth <= 1024;
2559        if (isMobile) return;
2560
2561        // Kill il vecchio ScrollTrigger
2562        if (portfolioScrollTrigger) {
2563            portfolioScrollTrigger.kill();
2564            portfolioScrollTrigger = null;
2565        }
2566
2567        // Ricalcola l'animazione
2568        initPortfolioListingHero();
2569    };
2570
2571    document.addEventListener('DOMContentLoaded', () => {
2572        initPortfolioListingHero();
2573    });
2574
2575    // Reinizializza anche su resize (per template responsive)
2576    let resizeTimeout;
2577    window.addEventListener('resize', () => {
2578        clearTimeout(resizeTimeout);
2579        resizeTimeout = setTimeout(() => {
2580            // Kill il vecchio ScrollTrigger se esiste
2581            if (portfolioScrollTrigger) {
2582                portfolioScrollTrigger.kill();
2583                portfolioScrollTrigger = null;
2584                portfolioTimeline = null;
2585                portfolioCardsContainer = null;
2586                portfolioViewport = null;
2587            }
2588            // Reinizializza
2589            initPortfolioListingHero();
2590        }, 200);
2591    });
2592
2593})();
2594
2595
2596/**
2597 * Docenti Listing Hero - Vertical Scroll Animation
2598 * Le card appaiono dal basso e salgono durante lo scroll, come se passassero attraverso una finestra
2599 */
2600(function() {
2601    'use strict';
2602
2603    let docentiScrollTrigger = null;
2604    let docentiTimeline = null;
2605    let docentiCardsContainer = null;
2606    let docentiViewport = null;
2607
2608    function initDocentiListingHero() {
2609        const docentiListingHero = document.querySelector('.docenti-listing-hero');
2610        
2611        if (!docentiListingHero) return;
2612
2613        // Verifica che l'elemento sia visibile (non nascosto da display:none)
2614        const isVisible = window.getComputedStyle(docentiListingHero).display !== 'none';
2615        if (!isVisible) {
2616            // Se non è visibile, aspetta che diventi visibile (per template responsive dinamici)
2617            const checkVisibility = () => {
2618                const checkVisible = window.getComputedStyle(docentiListingHero).display !== 'none';
2619                if (checkVisible) {
2620                    initDocentiListingHero();
2621                } else {
2622                    setTimeout(checkVisibility, 100);
2623                }
2624            };
2625            checkVisibility();
2626            return;
2627        }
2628
2629        docentiCardsContainer = docentiListingHero.querySelector('.docenti-listing-cards-container');
2630        docentiViewport = docentiListingHero.querySelector('.docenti-listing-scroll-viewport');
2631        
2632        if (!docentiCardsContainer || !docentiViewport) return;
2633
2634        // Registra ScrollTrigger plugin
2635        gsap.registerPlugin(ScrollTrigger);
2636
2637        // Controlla se siamo su mobile
2638        const isMobile = window.innerWidth <= 1024;
2639
2640        if (isMobile) {
2641            // Su mobile non fare animazione
2642            return;
2643        }
2644
2645        // Se esiste già un ScrollTrigger, killalo
2646        if (docentiScrollTrigger) {
2647            docentiScrollTrigger.kill();
2648        }
2649
2650        // Calcola l'altezza totale del contenuto
2651        const containerHeight = docentiCardsContainer.offsetHeight;
2652        const viewportHeight = docentiViewport.offsetHeight;
2653
2654        // Imposta la posizione iniziale: card posizionate sotto la viewport
2655        gsap.set(docentiCardsContainer, { 
2656            y: viewportHeight 
2657        });
2658
2659        // Calcola la distanza di scroll in base all'altezza del contenuto
2660        // La distanza di scroll deve essere proporzionale all'altezza del contenuto
2661        // per creare un effetto parallax fluido
2662        const scrollDistance = containerHeight + viewportHeight;
2663
2664        // Timeline principale per l'animazione
2665        docentiTimeline = gsap.timeline({
2666            scrollTrigger: {
2667                trigger: docentiListingHero,
2668                start: 'top top',
2669                end: `+=${scrollDistance}px`, // Distanza di scroll proporzionale all'altezza del contenuto
2670                pin: docentiListingHero, // Pinna l'intera section
2671                scrub: true, // Sincronizza l'animazione con lo scroll (true = più fluido)
2672                anticipatePin: 1,
2673                // markers: true, // Decommentare per debug
2674            }
2675        });
2676
2677        // Salva l'istanza ScrollTrigger
2678        docentiScrollTrigger = docentiTimeline.scrollTrigger;
2679
2680        // Anima il container delle card dal basso verso l'alto
2681        docentiTimeline.to(docentiCardsContainer, {
2682            y: -containerHeight, // Le card escono completamente dalla viewport
2683            ease: 'none', // Linear per un movimento costante
2684            duration: 1
2685        });
2686
2687        //console.log('Docenti Listing Hero vertical scroll animation initialized');
2688    }
2689
2690    // Funzione per ricalcolare l'animazione dopo il filtro
2691    window.recalculateDocentiListingHero = function() {
2692        if (!docentiCardsContainer || !docentiViewport) {
2693            initDocentiListingHero();
2694            return;
2695        }
2696
2697        // Controlla se siamo su mobile
2698        const isMobile = window.innerWidth <= 1024;
2699        if (isMobile) return;
2700
2701        // Kill il vecchio ScrollTrigger
2702        if (docentiScrollTrigger) {
2703            docentiScrollTrigger.kill();
2704            docentiScrollTrigger = null;
2705        }
2706
2707        // Ricalcola l'animazione
2708        initDocentiListingHero();
2709    };
2710
2711    document.addEventListener('DOMContentLoaded', () => {
2712        initDocentiListingHero();
2713    });
2714
2715    // Reinizializza anche su resize (per template responsive)
2716    let resizeTimeout;
2717    window.addEventListener('resize', () => {
2718        clearTimeout(resizeTimeout);
2719        resizeTimeout = setTimeout(() => {
2720            // Kill il vecchio ScrollTrigger se esiste
2721            if (docentiScrollTrigger) {
2722                docentiScrollTrigger.kill();
2723                docentiScrollTrigger = null;
2724                docentiTimeline = null;
2725                docentiCardsContainer = null;
2726                docentiViewport = null;
2727            }
2728            // Reinizializza
2729            initDocentiListingHero();
2730        }, 200);
2731    });
2732
2733})();
2734
2735
2736/**
2737 * Hero Home Scroll Button
2738 * Scrolla alla sezione successiva quando si clicca sul bottone
2739 */
2740(function() {
2741    'use strict';
2742
2743    document.addEventListener('DOMContentLoaded', () => {
2744        const scrollBtn = document.querySelector('.hero-home__scroll-btn');
2745        
2746        if (!scrollBtn) return;
2747
2748        scrollBtn.addEventListener('click', () => {
2749            const heroHome = document.getElementById('heroHome');
2750            
2751            if (!heroHome) return;
2752
2753            // Trova la sezione successiva
2754            const nextSection = heroHome.nextElementSibling;
2755            
2756            if (nextSection) {
2757                // Smooth scroll alla sezione successiva
2758                nextSection.scrollIntoView({
2759                    behavior: 'smooth',
2760                    block: 'start'
2761                });
2762            }
2763        });
2764    });
2765
2766})();
2767
2768
2769/**
2770 * Agevolazioni Hero - Department Icon Click
2771 * Cambia l'id della section in base all'icona del dipartimento cliccata
2772 */
2773(function() {
2774    'use strict';
2775
2776    document.addEventListener('DOMContentLoaded', () => {
2777        const agevolazioniHero = document.querySelector('.agevolazioni-hero');
2778        
2779        if (!agevolazioniHero) return;
2780        
2781        // Verifica che l'elemento sia visibile (non nascosto da display:none)
2782        const isVisible = window.getComputedStyle(agevolazioniHero).display !== 'none';
2783        if (!isVisible) {
2784            // Se non è visibile, aspetta che diventi visibile (per template responsive dinamici)
2785            const checkVisibility = () => {
2786                const checkVisible = window.getComputedStyle(agevolazioniHero).display !== 'none';
2787                if (checkVisible) {
2788                    initAgevolazioniHero();
2789                } else {
2790                    setTimeout(checkVisibility, 100);
2791                }
2792            };
2793            checkVisibility();
2794            return;
2795        }
2796
2797        initAgevolazioniHero();
2798    });
2799    
2800    function initAgevolazioniHero() {
2801        const agevolazioniHero = document.querySelector('.agevolazioni-hero');
2802        if (!agevolazioniHero) return;
2803        
2804        // Verifica nuovamente la visibilità prima di inizializzare
2805        if (window.getComputedStyle(agevolazioniHero).display === 'none') {
2806            return;
2807        }
2808
2809        const departmentIcons = agevolazioniHero.querySelectorAll('.department-icon-wrapper');
2810        
2811        if (departmentIcons.length === 0) return;
2812
2813        // Aggiungi event listener a ogni icona
2814        departmentIcons.forEach((iconWrapper) => {
2815            iconWrapper.addEventListener('click', function() {
2816                // Recupera l'id del dipartimento dal wrapper (es: "fashion", "graphic", ecc.)
2817                const departmentId = this.id;
2818                
2819                if (!departmentId) return;
2820
2821                // Rimuovi classe active da tutte le icone
2822                departmentIcons.forEach(icon => icon.classList.remove('active'));
2823                
2824                // Aggiungi classe active all'icona cliccata
2825                this.classList.add('active');
2826                
2827                // Aggiorna l'id della section con "colore-{department_id}"
2828                agevolazioniHero.id = 'colore-' + departmentId;
2829                
2830                //console.log('Agevolazioni hero ID updated to:', agevolazioniHero.id);
2831            });
2832        });
2833
2834        // Imposta il primo dipartimento come attivo di default
2835        if (departmentIcons.length > 0) {
2836            const firstIcon = departmentIcons[0];
2837            const firstDepartmentId = firstIcon.id;
2838            
2839            if (firstDepartmentId) {
2840                firstIcon.classList.add('active');
2841                agevolazioniHero.id = 'colore-' + firstDepartmentId;
2842            }
2843        }
2844    }
2845
2846})();

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.