document.addEventListener('DOMContentLoaded', function () { const section = document.querySelector('.founder-roadmap'); if (!section) return; const desktopRoadmapMedia = window.matchMedia('(min-width: 901px)'); if (!desktopRoadmapMedia.matches) { return; } const tabs = section.querySelectorAll('[data-roadmap-tab]'); const items = section.querySelectorAll('[data-roadmap-item]'); const itemsWrap = section.querySelector('.founder-roadmap__items'); let isTabScrolling = false; let tabScrollTimer = null; function setActive(index) { tabs.forEach(tab => tab.classList.remove('active')); items.forEach(item => item.classList.remove('active')); if (tabs[index]) tabs[index].classList.add('active'); if (items[index]) items[index].classList.add('active'); updateRoadmapProgress(); } function getActiveIndexByScreenMiddle() { const triggerLine = window.innerHeight * 0.4; let activeIndex = 0; items.forEach((item, index) => { const dot = item.querySelector('.founder-roadmap__dot'); const target = dot || item; const rect = target.getBoundingClientRect(); const targetCenter = rect.top + rect.height / 2; if (targetCenter <= triggerLine) { activeIndex = index; } }); return activeIndex; } function updateByScroll() { if (isTabScrolling) { updateRoadmapProgress(); return; } const activeIndex = getActiveIndexByScreenMiddle(); setActive(activeIndex); } function updateRoadmapProgress() { if (!itemsWrap) return; const wrapRect = itemsWrap.getBoundingClientRect(); const viewportPoint = window.innerHeight * 0.6; let progress = viewportPoint - wrapRect.top; progress = Math.max(0, Math.min(progress, itemsWrap.offsetHeight)); itemsWrap.style.setProperty('--progress-height', `${progress}px`); const allCardDots = itemsWrap.querySelectorAll('.founder-roadmap__card-dot'); const lastCardDot = allCardDots[allCardDots.length - 1]; if (lastCardDot) { const lastDotRect = lastCardDot.getBoundingClientRect(); const lastDotCenter = lastDotRect.top + lastDotRect.height / 2 - wrapRect.top; itemsWrap.style.setProperty('--line-height', `${lastDotCenter}px`); } items.forEach(item => { const dot = item.querySelector('.founder-roadmap__dot'); if (!dot) return; const dotRect = dot.getBoundingClientRect(); const dotCenter = dotRect.top + dotRect.height / 2; const dotCenterInsideWrap = dotCenter - wrapRect.top; if (dotCenterInsideWrap <= progress) { item.classList.add('passed'); } else { item.classList.remove('passed'); } }); const allCards = itemsWrap.querySelectorAll('.founder-roadmap__card'); allCards.forEach(card => { card.classList.remove('active'); card.classList.remove('passed_child'); }); let activeCard = null; let minDistance = Infinity; allCards.forEach(card => { const cardDot = card.querySelector('.founder-roadmap__card-dot'); const target = cardDot || card; const targetRect = target.getBoundingClientRect(); const targetCenter = targetRect.top + targetRect.height / 2; const targetCenterInsideWrap = targetCenter - wrapRect.top; const distance = Math.abs(targetCenterInsideWrap - progress); if (targetCenterInsideWrap <= progress) { card.classList.add('passed_child'); } if (distance < minDistance) { minDistance = distance; activeCard = card; } }); if (activeCard) { activeCard.classList.add('active'); } } tabs.forEach(tab => { tab.addEventListener('click', function () { const index = Number(this.dataset.roadmapTab); const target = items[index]; if (!target) return; isTabScrolling = true; clearTimeout(tabScrollTimer); setActive(index); const tabsHeight = section.querySelector('.founder-roadmap__tabs')?.offsetHeight || 0; const extraOffset = 80; function getTargetScrollTop() { const targetTop = target.getBoundingClientRect().top + window.pageYOffset; return targetTop - tabsHeight - extraOffset; } window.scrollTo({ top: getTargetScrollTop(), behavior: 'smooth' }); let stableFrames = 0; function waitScrollEnd() { updateRoadmapProgress(); tabs.forEach(tab => tab.classList.remove('active')); items.forEach(item => item.classList.remove('active')); if (tabs[index]) tabs[index].classList.add('active'); if (items[index]) items[index].classList.add('active'); const targetRect = target.getBoundingClientRect(); const wantedTop = tabsHeight + extraOffset; const distance = Math.abs(targetRect.top - wantedTop); if (distance > 6) { window.scrollTo({ top: getTargetScrollTop(), behavior: 'smooth' }); stableFrames = 0; } else { stableFrames++; } if (stableFrames >= 6) { isTabScrolling = false; setActive(index); return; } requestAnimationFrame(waitScrollEnd); } requestAnimationFrame(waitScrollEnd); }); }); window.addEventListener('scroll', updateByScroll); window.addEventListener('resize', updateByScroll); updateByScroll(); }); document.addEventListener('DOMContentLoaded', function () { const mobileRoadmap = document.querySelector('.founder-roadmap-mobile'); if (!mobileRoadmap) return; if (!window.matchMedia('(max-width: 900px)').matches) return; const items = Array.from( mobileRoadmap.querySelectorAll('[data-mobile-roadmap-item]') ); if (!items.length) return; const header = document.querySelector('header'); let activeItem = items.find(item => item.classList.contains('is-open') ) || items[0]; let isAnimating = false; const isIOSWebKit = /iPad|iPhone|iPod/.test(navigator.userAgent) || ( navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1 ); function updateStickyTop() { let stickyTop = 0; if (header) { const headerStyles = window.getComputedStyle(header); if ( headerStyles.position === 'fixed' || headerStyles.position === 'sticky' ) { stickyTop = header.offsetHeight; } } mobileRoadmap.style.setProperty( '--founder-mobile-sticky-top', `${stickyTop}px` ); } function getPanel(item) { return item.querySelector('.founder-roadmap-mobile__panel'); } function getButton(item) { return item.querySelector('.founder-roadmap-mobile__toggle'); } function setButtonState(item, isOpen) { const button = getButton(item); if (button) { button.setAttribute( 'aria-expanded', isOpen ? 'true' : 'false' ); } } function prepareInitialState() { items.forEach(item => { const panel = getPanel(item); if (!panel) return; if (item === activeItem) { item.classList.add('is-open'); panel.style.height = 'auto'; panel.style.opacity = '1'; setButtonState(item, true); } else { item.classList.remove('is-open'); panel.style.height = '0px'; panel.style.opacity = '0'; setButtonState(item, false); } }); } function closeItem(item, anchorItem = null) { return new Promise(resolve => { const panel = getPanel(item); if (!panel || !item.classList.contains('is-open')) { item.classList.remove('is-open'); setButtonState(item, false); resolve(); return; } const anchorButton = anchorItem ? getButton(anchorItem) : null; const anchorPanel = anchorItem ? getPanel(anchorItem) : null; const currentHeight = Math.ceil( panel.getBoundingClientRect().height ); /* * На iPhone не запускаємо покадровий scrollBy. * Саме він конфліктує зі sticky та scroll anchoring Safari. */ if (isIOSWebKit) { const itemIndex = items.indexOf(item); const anchorIndex = anchorItem ? items.indexOf(anchorItem) : -1; /* * Якщо старий відкритий блок розташований вище * нового, його згортання підніме новий блок вгору. */ const collapsingAboveAnchor = anchorItem && itemIndex !== -1 && anchorIndex !== -1 && itemIndex < anchorIndex; panel.style.transition = 'none'; panel.style.height = `${currentHeight}px`; panel.style.opacity = '1'; // Safari має застосувати зафіксовану px-висоту panel.getBoundingClientRect(); item.classList.remove('is-open'); setButtonState(item, false); /* * Закриваємо стару панель миттєво. * Новий блок усе одно відкриватиметься плавно. */ panel.style.height = '0px'; panel.style.opacity = '0'; /* * Компенсуємо видалену висоту лише один раз. * Без requestAnimationFrame-циклу та без стрибків. */ if (collapsingAboveAnchor && currentHeight > 0) { window.scrollBy({ top: -currentHeight, left: 0, behavior: 'auto' }); } requestAnimationFrame(() => { panel.style.transition = ''; resolve(); }); return; } /* * Нижче залишаємо твою стару логіку для Chrome, * де вона вже працювала добре. */ const stickyTopValue = parseFloat( getComputedStyle(mobileRoadmap) .getPropertyValue( '--founder-mobile-sticky-top' ) ) || 0; const wantedPanelTop = anchorButton ? stickyTopValue + anchorButton.getBoundingClientRect().height : 0; panel.style.transition = 'none'; panel.style.height = `${currentHeight}px`; panel.style.opacity = '1'; panel.getBoundingClientRect(); item.classList.remove('is-open'); setButtonState(item, false); let animationFrame = null; let isFinished = false; function keepAnchorInPlace() { if ( isFinished || !anchorButton || !anchorPanel ) { return; } const currentPanelTop = anchorPanel.getBoundingClientRect().top; const difference = currentPanelTop - wantedPanelTop; if (Math.abs(difference) > 1) { window.scrollBy(0, difference); } animationFrame = requestAnimationFrame(keepAnchorInPlace); } requestAnimationFrame(() => { requestAnimationFrame(() => { panel.style.transition = ''; panel.style.height = '0px'; panel.style.opacity = '0'; if (anchorPanel) { keepAnchorInPlace(); } }); }); function finishClosing() { if (isFinished) return; isFinished = true; if (animationFrame) { cancelAnimationFrame(animationFrame); } panel.style.height = '0px'; panel.style.opacity = '0'; panel.style.transition = ''; if (anchorPanel && anchorButton) { const finalPanelTop = anchorPanel.getBoundingClientRect().top; const correction = finalPanelTop - wantedPanelTop; if (Math.abs(correction) > 1) { window.scrollBy(0, correction); } } resolve(); } const onTransitionEnd = event => { if ( event.target !== panel || event.propertyName !== 'height' ) { return; } panel.removeEventListener( 'transitionend', onTransitionEnd ); finishClosing(); }; panel.addEventListener( 'transitionend', onTransitionEnd ); setTimeout(() => { panel.removeEventListener( 'transitionend', onTransitionEnd ); finishClosing(); }, 250); }); } function openItem(item) { return new Promise(resolve => { const panel = getPanel(item); const panelInner = panel?.querySelector( '.founder-roadmap-mobile__panel-inner' ); if (!panel || !panelInner) { resolve(); return; } item.classList.add('is-open'); setButtonState(item, true); /* * Спочатку встановлюємо гарантований закритий стан. */ panel.style.transition = 'none'; panel.style.height = '0px'; panel.style.opacity = '0'; panel.getBoundingClientRect(); /* * scrollHeight у Safari іноді береться до завершення * перерахунку тексту. Вимірюємо inner-блок. */ requestAnimationFrame(() => { requestAnimationFrame(() => { const targetHeight = panelInner.getBoundingClientRect().height; panel.style.transition = ''; panel.style.height = `${Math.ceil(targetHeight)}px`; panel.style.opacity = '1'; }); }); let isFinished = false; function finishOpening() { if (isFinished) return; isFinished = true; /* * Після transition можна повернути auto, * щоб блок реагував на перенесення тексту. */ panel.style.height = 'auto'; panel.style.opacity = '1'; panel.style.transition = ''; resolve(); } const onTransitionEnd = event => { if ( event.target !== panel || event.propertyName !== 'height' ) { return; } panel.removeEventListener( 'transitionend', onTransitionEnd ); finishOpening(); }; panel.addEventListener( 'transitionend', onTransitionEnd ); setTimeout(() => { panel.removeEventListener( 'transitionend', onTransitionEnd ); finishOpening(); }, 250); }); } function scrollItemToSticky(item) { const stickyTopValue = parseFloat( getComputedStyle(mobileRoadmap) .getPropertyValue( '--founder-mobile-sticky-top' ) ) || 0; const itemTop = item.getBoundingClientRect().top + window.scrollY - stickyTopValue; window.scrollTo({ top: itemTop, behavior: 'smooth' }); } function waitForScrollEnd(item) { return new Promise(resolve => { const button = getButton(item); if (!button) { resolve(); return; } const stickyTopValue = parseFloat( getComputedStyle(mobileRoadmap) .getPropertyValue('--founder-mobile-sticky-top') ) || 0; let stableFrames = 0; let previousTop = null; let frameCount = 0; function checkPosition() { frameCount++; const currentTop = button.getBoundingClientRect().top; const distance = Math.abs(currentTop - stickyTopValue); const movement = previousTop === null ? Infinity : Math.abs(currentTop - previousTop); previousTop = currentTop; if (distance < 3 || movement < 0.25) { stableFrames++; } else { stableFrames = 0; } if (stableFrames >= 5 || frameCount > 90) { resolve(); return; } requestAnimationFrame(checkPosition); } requestAnimationFrame(checkPosition); }); } async function activateItem(nextItem) { if (isAnimating) return; isAnimating = true; /* * Повторно натиснули на вже відкритий блок: * плавно закриваємо його й залишаємо всі блоки згорнутими. */ if (nextItem === activeItem && nextItem.classList.contains('is-open')) { await closeItem(nextItem); activeItem = null; isAnimating = false; return; } const previousItem = activeItem; /* * Новий блок одразу стає поточним. * Попередній поки залишається відкритим. */ activeItem = nextItem; await openItem(nextItem); /* * Підтягуємо відкритий блок до sticky-позиції. */ scrollItemToSticky(nextItem); await waitForScrollEnd(nextItem); /* * Після відкриття нового блока закриваємо попередній. */ if ( previousItem && previousItem !== nextItem && previousItem.classList.contains('is-open') ) { await closeItem(previousItem, nextItem); } isAnimating = false; } items.forEach(item => { const button = getButton(item); if (!button) return; button.addEventListener('click', function (event) { event.preventDefault(); activateItem(item); }); }); updateStickyTop(); if (document.fonts && document.fonts.ready) { document.fonts.ready.then(() => { prepareInitialState(); updateStickyTop(); }); } else { prepareInitialState(); } window.addEventListener('resize', updateStickyTop); }); document.addEventListener('DOMContentLoaded', function () { const burger = document.querySelector('.moby_burger'); const popup = document.querySelector('.mobile_menu_popup'); const close = document.querySelector('.mobile_menu_close'); if (!burger || !popup || !close) return; burger.addEventListener('click', function () { popup.classList.add('active'); document.body.style.overflow = 'hidden'; }); close.addEventListener('click', function () { popup.classList.remove('active'); document.body.style.overflow = ''; }); }); document.addEventListener('DOMContentLoaded', function () { const form = document.querySelector('#contactForm'); if (!form) return; const readySection = form.closest('.ready'); const requiredFields = form.querySelectorAll('[required]'); function validateField(field) { const error = field.nextElementSibling; let isValid = field.checkValidity(); if (field.type === 'email' && field.value.trim() !== '') { isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(field.value); } if (!isValid) { field.classList.add('is-error'); if (error && error.classList.contains('field-error')) { if (field.validity.valueMissing) { error.textContent = 'This field is required.'; } else if (field.validity.typeMismatch || field.type === 'email') { error.textContent = 'Please enter a valid email address.'; } } } else { field.classList.remove('is-error'); } return isValid; } requiredFields.forEach(field => { field.addEventListener('input', function () { validateField(field); }); field.addEventListener('blur', function () { validateField(field); }); }); form.addEventListener('submit', function (e) { e.preventDefault(); let isFormValid = true; requiredFields.forEach(field => { if (!validateField(field)) { isFormValid = false; } }); if (!isFormValid) { return; } const formData = new FormData(form); formData.append('action', 'send_inquiry'); fetch('/wp-admin/admin-ajax.php', { method: 'POST', body: formData }) .then(response => response.json()) .then(data => { if (data.success) { form.reset(); requiredFields.forEach(field => { field.classList.remove('is-error'); }); if (readySection) { readySection.classList.add('is-success'); readySection.scrollIntoView({ behavior: 'smooth', block: 'start' }); } } }) .catch(() => {}); }); }); document.addEventListener('DOMContentLoaded', function () { const header = document.querySelector('header'); if (!header) return; if (header.classList.contains('no_sticky_head')) return; if (header.classList.contains('static_white_head')) return; const firstSection = document.querySelector('body > section, main > section'); if (!firstSection) return; function updateHeaderColor() { const triggerPoint = firstSection.offsetTop + firstSection.offsetHeight - header.offsetHeight; if (window.scrollY >= triggerPoint) { header.classList.add('white_head'); } else { header.classList.remove('white_head'); } } window.addEventListener('scroll', updateHeaderColor); window.addEventListener('resize', updateHeaderColor); updateHeaderColor(); });