PageSourceSearch

https://vproekte.com/wp-content/themes/vproekte_theme/js/main.js?ver=1791462656

js vproekte.com collected 2026-10-09 21:53:01 UTC 137,559 bytes, 4,512 lines download raw bytes

1document.addEventListener('DOMContentLoaded', () => {
2  initHeroVideo();
3  tabsInit();
4  initPhoneInput();
5  initializeSliders();
6  initHeaderNav();
7  initFileUpload();
8  faqInit();
9  newShowcaseAccordionInit();
10  initVideoCards();
11  hydrateMobileCountries();
12  hydrateMirrors();
13  countriesMapInit();
14  modalInit();
15  initObservers();
16  quizInit();
17  initVideoJS();
18  initGallerySlider();
19  initLazyVideo();
20  initProject360Modal();
21  initFormField();
22  initToggleActive('.js-more', '.js-card');
23  initSmoothAnchorScroll();
24  initPortfolioAboutSync();
25  initPortfolioBreadcrumbsOffset();
26  initBlogPostOffset();
27  initBlogPostNav();
28  initPortfolioHeroMobileHeight();
29  initPortfolioGallerySlider();
30  initPortfolioReasonsSlider();
31  initNewExperienceTextToggle();
32  initNewExperienceCountriesToggle();
33});
34
35function initProject360Modal() {
36  const modal = document.getElementById('modal-project-360');
37  if (!modal) return;
38  if (initProject360Modal._inited) return; // guard against double-binding after AJAX modals load
39  initProject360Modal._inited = true;
40
41  const iframe = modal.querySelector('.modal-project-360__iframe');
42  const wrapper = iframe.parentElement; // контейнер с iframe и кнопкой fullscreen
43  const fullscreenBtn = modal.querySelector('.iframe__fullscreen-btn');
44  const loader = modal.querySelector('.iframe__loader');
45  const heading = modal.querySelector('.modal-project-360__heading');
46  const headingTitle = heading ? heading.querySelector('.modal-project-360__heading-title') : null;
47  const headingAreaWrap = heading
48    ? heading.querySelector('.modal-project-360__heading-area-wrap')
49    : null;
50  const headingArea = heading ? heading.querySelector('.modal-project-360__heading-area') : null;
51
52  const fillHeading = (el) => {
53    if (!heading) return;
54    const title = ((el && el.dataset.title) || '').trim();
55    const area = ((el && el.dataset.area) || '').trim();
56    if (headingTitle) headingTitle.textContent = title;
57    if (headingArea) headingArea.textContent = area ? area + ' м²' : '';
58    if (headingAreaWrap) headingAreaWrap.hidden = !area;
59    heading.hidden = !title && !area;
60  };
61
62  const pageTrigger = document.querySelector('[data-modal="modal-project-360"]');
63  if (pageTrigger) fillHeading(pageTrigger);
64
65  let currentSrc = '';
66  let originalParent = wrapper.parentElement;
67  let placeholder = document.createElement('div');
68  placeholder.style.display = 'none';
69  originalParent.insertBefore(placeholder, wrapper);
70
71  // открыть модалку и подставить iframe
72  document.addEventListener('click', (e) => {
73    const btn = e.target.closest('[data-modal="modal-project-360"]');
74    if (!btn) return;
75
76    fillHeading(btn);
77
78    const src = btn.dataset.iframe;
79    if (!src) return;
80    if (currentSrc === src && iframe.src) return;
81
82    currentSrc = src;
83    loader.classList.remove('loaded');
84    loader.hidden = false;
85    iframe.src = src;
86
87    setTimeout(() => loader.classList.add('loaded'), 2500);
88  });
89
90  // кнопка fullscreen
91  fullscreenBtn.addEventListener('click', () => {
92    if (!wrapper.classList.contains('fullscreen-fixed')) {
93      // включаем fullscreen
94      document.body.appendChild(wrapper); // переносим в body
95      wrapper.classList.add('fullscreen-fixed');
96      fullscreenBtn.textContent = '✕';
97      document.body.style.overflow = 'hidden';
98    } else {
99      // выключаем fullscreen
100      wrapper.classList.remove('fullscreen-fixed');
101      fullscreenBtn.textContent = '⛶';
102      placeholder.parentNode.insertBefore(wrapper, placeholder); // возвращаем
103      document.body.style.overflow = '';
104    }
105  });
106
107  // закрытие модалки
108  document.addEventListener('click', (e) => {
109    if (!e.target.closest('#modal-project-360 [data-close-modal]')) return;
110
111    iframe.src = '';
112    currentSrc = '';
113    loader.classList.remove('loaded');
114
115    // возвращаем wrapper в модалку
116    if (wrapper.classList.contains('fullscreen-fixed')) {
117      wrapper.classList.remove('fullscreen-fixed');
118      placeholder.parentNode.insertBefore(wrapper, placeholder);
119      document.body.style.overflow = '';
120    }
121  });
122}
123
124function quizInit() {
125  // Only init quizzes not yet initialized (guard against re-runs after AJAX modal load)
126  const quizzes = Array.from(document.querySelectorAll('.quiz')).filter(function (q) {
127    return !q.dataset.vpQuizInited;
128  });
129  if (!quizzes.length) return;
130  quizzes.forEach(function (q) {
131    q.dataset.vpQuizInited = '1';
132  });
133
134  const debouncedResizeHandler = debounce(() => {
135    quizzes.forEach((quiz) => {
136      console.log(quiz);
137      updateQuizDimensions(quiz);
138    });
139  }, 250);
140
141  window.addEventListener('resize', debouncedResizeHandler);
142  window.addEventListener('orientationchange', debouncedResizeHandler);
143
144  quizzes.forEach((quiz) => {
145    const steps = Array.from(quiz.querySelectorAll('.quiz__step'));
146    const progressEl = quiz.querySelector('.quiz__progress');
147    const titleEl = quiz.querySelector('.section__title');
148    const totalSteps = steps.length;
149    const answers = [];
150    const form = quiz.querySelector('.quiz__form');
151
152    updateQuizDimensions(quiz);
153
154    function updateHeader(stepIndex) {
155      const currentStep = steps[stepIndex];
156      const title = currentStep.dataset.title;
157      const stepNum = stepIndex + 1;
158
159      if (titleEl) titleEl.textContent = title;
160      if (progressEl) {
161        progressEl.innerHTML = `${stepNum} / <span>${totalSteps}</span>`;
162      }
163    }
164
165    function fadeOutStep(step, callback) {
166      if (!step) return callback?.();
167      step.style.transition = 'opacity 0.3s ease';
168      step.style.opacity = '0';
169      const onEnd = () => {
170        step.removeEventListener('transitionend', onEnd);
171        step.style.display = 'none';
172        callback?.();
173      };
174      step.addEventListener('transitionend', onEnd);
175    }
176
177    function fadeInStep(step) {
178      if (!step) return;
179      step.style.display = 'block';
180      void step.offsetWidth;
181      step.style.transition = 'opacity 0.3s ease';
182      step.style.opacity = '1';
183    }
184
185    function goToNextStep(currentStepIndex) {
186      const nextStepIndex = currentStepIndex + 1;
187      const nextStep = steps[nextStepIndex];
188      if (!nextStep) return;
189
190      fadeOutStep(steps[currentStepIndex], () => {
191        fadeInStep(nextStep);
192
193        if (nextStep.classList.contains('quiz__step--final')) {
194          const stepsWrapper = quiz.querySelector('.quiz__steps-wrapper');
195          if (stepsWrapper) stepsWrapper.style.height = '';
196
197          setTimeout(() => {
198            const phoneInput = nextStep.querySelector('.phone');
199            if (phoneInput) {
200              fixInputPadding(phoneInput);
201            }
202          }, 60);
203        }
204
205        nextStep.classList.add('quiz__step--active');
206        steps[currentStepIndex].classList.remove('quiz__step--active');
207        updateHeader(nextStepIndex);
208      });
209    }
210
211    function handleOptionClick(e) {
212      const option = e.currentTarget;
213      const input = option.querySelector('input[type="radio"]');
214      if (!input) return;
215
216      const step = option.closest('.quiz__step');
217      const idx = steps.indexOf(step);
218
219      step
220        .querySelectorAll('.quiz__option')
221        .forEach((opt) => opt.classList.remove('quiz__option--selected'));
222      option.classList.add('quiz__option--selected');
223
224      answers[idx] = {
225        title: step.dataset.title,
226        answer: input.value,
227      };
228
229      setTimeout(() => goToNextStep(idx), 400);
230    }
231
232    // Навешиваем обработчики на опции
233    quiz
234      .querySelectorAll('.quiz__option')
235      .forEach((opt) => opt.addEventListener('click', handleOptionClick));
236
237    // 🔥 Обработка отправки формы
238    /*if (form) {
239            form.addEventListener('submit', function (e) {
240                e.preventDefault(); // Предотвращаем обычную отправку
241
242                // 1. Собираем все ответы
243                const payload = {
244                    answers: answers,
245                    timestamp: new Date().toISOString()
246                };
247
248                // 2. Находим модальное окно успеха
249                const successModalId = quiz.dataset.successModalId;
250                const successModal = successModalId
251                    ? document.getElementById(successModalId)
252                    : null;
253
254                // 3. Закрываем родительскую модалку (если квиз внутри .modal)
255                const parentModal = quiz.closest('.modal');
256                if (parentModal) {
257                    parentModal.classList.remove('modal--active');
258                    // Добавляем задержку перед открытием success-модалки
259                    setTimeout(() => {
260                        if (successModal) {
261                            successModal.classList.add('modal--active');
262                        }
263                    }, 300);
264                } else {
265                    // Если квиз не в модалке — просто открываем success
266                    if (successModal) {
267                        successModal.classList.add('modal--active');
268                    }
269                }
270
271                // 4. Отправляем данные на сервер (AJAX
271)
272                fetch('/submit-quiz', {
273                    method: 'POST',
274                    headers: {
275                        'Content-Type': 'application/json',
276                        // Добавьте CSRF-токен, если нужно
277                        // 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
278                    },
279                    body: JSON.stringify(payload)
280                })
281                .then(response => {
282                    if (response.ok) {
283                        console.log('✅ Квиз успешно отправлен на сервер');
284                        // Можно очистить answers, если нужно
285                    } else {
286                        console.error('❌ Ошибка отправки квиза');
287                        // Обработка ошибки — можно показать другую модалку
288                    }
289                })
290                .catch(error => {
291                    console.error('🔥 Ошибка сети:', error);
292                });
293            });
294        }*/
295
296    steps.forEach((step, i) => {
297      step.style.display = i === 0 ? 'block' : 'none';
298      step.style.opacity = i === 0 ? '1' : '0';
299      step.classList.toggle('quiz__step--active', i === 0);
300    });
301    updateHeader(0);
302  });
303
304  function debounce(func, wait) {
305    let timeout;
306    return function executedFunction(...args) {
307      const later = () => {
308        clearTimeout(timeout);
309        func(...args);
310      };
311      clearTimeout(timeout);
312      timeout = setTimeout(later, wait);
313    };
314  }
315
316  function updateQuizDimensions(quiz) {
317    const steps = Array.from(quiz.querySelectorAll('.quiz__step'));
318    const stepsWrapper = quiz.querySelector('.quiz__steps-wrapper');
319
320    if (!stepsWrapper || steps.length === 0) return;
321
322    const activeStep = quiz.querySelector('.quiz__step--active');
323    if (activeStep && activeStep.classList.contains('quiz__step--final')) {
324      stepsWrapper.style.height = '';
325      return;
326    }
327
328    let maxHeight = 0;
329    const stepsToMeasure = steps.filter((step) => !step.classList.contains('quiz__step--final'));
330
331    stepsToMeasure.forEach((step) => {
332      step.style.position = 'absolute';
333      step.style.visibility = 'hidden';
334      step.style.display = 'block';
335      step.style.width = 'auto';
336      maxHeight = Math.max(maxHeight, step.offsetHeight);
337      step.style.position = '';
338      step.style.visibility = '';
339      step.style.display = '';
340    });
341
342    stepsWrapper.style.height = maxHeight + 'px';
343  }
344}
345
346function initVideoJS() {
347  const videoWrappers = document.querySelectorAll('.video-js');
348
349  videoWrappers.forEach((wrapper) => {
350    const video = wrapper.querySelector('video');
351    const playBtn = wrapper.querySelector('.video-js__play');
352
353    if (!video || !playBtn) return;
354
355    const updateState = () => {
356      if (video.paused) {
357        wrapper.classList.remove('video-js--playing');
358        wrapper.classList.add('video-js--paused');
359      } else {
360        wrapper.classList.add('video-js--playing');
361        wrapper.classList.remove('video-js--paused');
362      }
363    };
364
365    playBtn.addEventListener('click', () => {
366      if (video.paused) video.play();
367      else video.pause();
368    });
369
370    playBtn.addEventListener('keydown', (e) => {
371      if (e.key === 'Enter' || e.key === ' ') {
372        e.preventDefault();
373        playBtn.click();
374      }
375    });
376
377    // События видео
378    video.addEventListener('play', updateState);
379    video.addEventListener('pause', updateState);
380
381    wrapper.resetVideo = () => {
382      video.pause();
383      video.currentTime = 0;
384      wrapper.classList.remove('video-js--playing', 'video-js--paused');
385    };
386  });
387}
388
389function initObservers() {
390  const observer = new IntersectionObserver(
391    (entries, obs) => {
392      entries.forEach((entry) => {
393        if (!entry.isIntersecting) return;
394
395        const el = entry.target;
396
397        if (el.classList.contains('corners--animated')) {
398          el.classList.add('active');
399        }
400
401        if (el.classList.contains('animated-numbers')) {
402          animateNumbers?.(el);
403        }
404
405        if (el.classList.contains('decor-appear') || el.classList.contains('animated')) {
406          handleImageLoad(el, () => el.classList.add('_visible'), obs);
407          return;
408        }
409
410        obs.unobserve(el);
411      });
412    },
413    { threshold: 0.3 }
414  );
415
416  document
417    .querySelectorAll(
418      `
419    .animated,
420    .corners--animated,
421    .about__stats,
422    .decor-appear,
423    .animated-numbers
424  `
425    )
426    .forEach((el) => observer.observe(el));
427}
428
429function animateNumbers(container) {
430  const numbers = container.querySelectorAll('.animated--number');
431  numbers.forEach((counter) => {
432    const target = +counter.getAttribute('data-target');
433    let current = 0;
434    const increment = Math.ceil(target / 150);
435
436    const updateCount = () => {
437      current += increment;
438      if (current < target) {
439        counter.innerText = current;
440        requestAnimationFrame(updateCount);
441      } else {
442        counter.innerText = target;
443      }
444    };
445
446    updateCount();
447  });
448}
449
450function handleImageLoad(element, onAllLoaded, observer) {
451  const images = element.querySelectorAll('img');
452
453  if (!images.length) {
454    // нет картинок — вызываем callback сразу и перестаём наблюдать
455    onAllLoaded();
456    if (observer) observer.unobserve(element);
457    return;
458  }
459
460  let loadedCount = 0;
461  const markLoaded = () => {
462    loadedCount++;
463    if (loadedCount === images.length) {
464      onAllLoaded();
465      if (observer) observer.unobserve(element);
466    }
467  };
468
469  images.forEach((img) => {
470    // если картинка уже загружена (кеш или быстрый загруз)
471    if (img.complete && img.naturalWidth !== 0) {
472      markLoaded();
473    } else {
474      // слушаем событие load и error (error тоже считаем "загруженной", чтобы не вечно ждать)
475      const onFinish = () => markLoaded();
476      img.addEventListener('load', onFinish, { once: true });
477      img.addEventListener('error', onFinish, { once: true });
478    }
479  });
480}
481
482function initPhoneInput() {
483  // Skip already-initialized inputs (guard for re-run after AJAX modal load)
484  const inputs = Array.from(document.querySelectorAll('.phone')).filter(function (el) {
485    return !el.dataset.vpPhoneInited;
486  });
487  inputs.forEach(function (el) {
488    el.dataset.vpPhoneInited = '1';
489  });
490  const masks = {
491    ru: '(999) 999-99-99',
492    kz: '(999) 999-99-99',
493    bg: '(999) 999-999',
494  };
495
496  inputs.forEach((input) => {
497    let currentMask = masks.ru;
498
499    const iti = intlTelInput(input, {
500      initialCountry: 'ru',
501      i18n: {
502        searchPlaceholder: 'Поиск...',
503        zeroSearchResults: 'Ничего не найдено',
504        oneSearchResult: 'Найден 1 вариант',
505        multipleSearchResults: '${count} вариантов найдено',
506      },
507      preferredCountries: ['ru', 'kz', 'ua', 'by'],
508      separateDialCode: false,
509      utilsScript: 'https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.19/js/utils.js',
510    });
511
512    function updateMask() {
513      const countryData = iti.getSelectedCountryData();
514      const iso2 = countryData.iso2.toLowerCase();
515
516      if (masks[iso2]) {
517        currentMask = masks[iso2];
518      } else {
519        try {
520          const exampleNumber = intlTelInputUtils.getExampleNumber(iso2, false, 2);
521          const formatted = intlTelInputUtils.formatNumber(exampleNumber, iso2, 2);
522          const dialCode = countryData.dialCode;
523          let maskStr = formatted.replace('+' + dialCode + ' ', '').replace(/[0-9]/g, '9');
524          currentMask = maskStr;
525        } catch (e) {
526          currentMask = '9999999999999';
527        }
528      }
529
530      input.placeholder = currentMask.replace(/9/g, '0');
531      input.value = '';
532    }
533
534    function updateCountryCodeDisplay() {
535      const data = iti.getSelectedCountryData();
536      const iso = data.iso2.toUpperCase();
537      const dial = data.dialCode;
538      const btn = input.closest('.iti').querySelector('.iti__selected-country');
539      if (!btn) return;
540
541      let label = btn.querySelector('.country-code-label');
542      if (!label) {
543        const primaryBlock = btn.querySelector('.iti__selected-country-primary');
544        label = document.createElement('span');
545        label.className = 'country-code-label';
546        primaryBlock.appendChild(label);
547      }
548      label.textContent = `${iso} (+${dial})`;
549    }
550
551    function formatPhoneInput(e) {
552      let value = e.target.value.replace(/\D/g, '');
553      let formattedValue = '';
554      let index = 0;
555
556      for (let i = 0; i < currentMask.length && index < value.length; i++) {
557        if (currentMask[i] === '9') {
558          formattedValue += value[index];
559          index++;
560        } else {
561          formattedValue += currentMask[i];
562        }
563      }
564      input.value = formattedValue;
565    }
566
567    function getMaskDigitCount() {
568      return (currentMask.match(/9/g) || []).length;
569    }
570
571    function isPhoneMaskComplete() {
572      const digits = input.value.replace(/\D/g, '');
573      return digits.length > 0 && digits.length === getMaskDigitCount();
574    }
575
576    function getPhoneWrap() {
577      return (
578        input.closest('.wpcf7-form-control-wrap') ||
579        input.closest('.form__group') ||
580        input.parentElement
581      );
582    }
583
584    // В формах с табами Телефон/Telegram/Max валидируем только активный способ связи
585    function isActiveContactField(field) {
586      const changeWrap = field.closest('.change');
587      if (!changeWrap) return true;
588
589      const formEl = field.closest('form');
590      if (!formEl || !formEl.querySelector('.group [data-show]')) return true;
591
592      return changeWrap.classList.contains('sel');
593    }
594
595    function showPhoneError(message) {
596      input.classList.add('wpcf7-not-valid');
597      input.setAttribute('aria-invalid', 'true');
598
599      const wrap = getPhoneWrap();
600      if (!wrap) return;
601
602      let tip = wrap.querySelector('.wpcf7-not-valid-tip');
603      if (!tip) {
604        tip = document.createElement('span');
605        tip.className = 'wpcf7-not-valid-tip';
606        tip.setAttribute('aria-hidden', 'true');
607        wrap.appendChild(tip);
608      }
609      tip.textContent = message;
610    }
611
612    function clearPhoneError() {
613      input.classList.remove('wpcf7-not-valid');
614      input.removeAttribute('aria-invalid');
615
616      const wrap = getPhoneWrap();
617      const tip = wrap && wrap.querySelector('.wpcf7-not-valid-tip');
618      if (tip) tip.remove();
619    }
620
621    // Активный телефон обязателен; неполный по маске — ошибка. Неактивные табы — пропускаем.
622    input._vpValidatePhone = function () {
623      if (!isActiveContactField(input)) {
624        clearPhoneError();
625        return true;
626      }
627
628      const digits = input.value.replace(/\D/g, '');
629
630      if (!digits.length) {
631        showPhoneError('Обязательное поле');
632        return false;
633      }
634
635      if (!isPhoneMaskComplete()) {
636        showPhoneError('Введите номер полностью');
637        return false;
638      }
639
640      clearPhoneError();
641      return true;
642    };
643
644    input.setAttribute('aria-required', 'true');
645    input.classList.add('wpcf7-validates-as-required');
646    if (!input.hasAttribute('required')) {
647      input.required = true;
648    }
649
650    updateMask();
651    updateCountryCodeDisplay();
652    fixInputPadding(input);
653
654    input.addEventListener('countrychange', () => {
655      updateMask();
656      updateCountryCodeDisplay();
657      fixInputPadding(input);
658      clearPhoneError();
659    });
660
661    input.addEventListener('input', (e) => {
662      formatPhoneInput(e);
663      clearPhoneError();
664    });
665
666    input.addEventListener('blur', () => {
667      if (!isActiveContactField(input)) {
668        clearPhoneError();
669        return;
670      }
671
672      const digits = input.value.replace(/\D/g, '');
673      if (!digits.length) {
674        showPhoneError('Обязательное поле');
675      } else if (!isPhoneMaskComplete()) {
676        showPhoneError('Введите номер полностью');
677      }
678    });
679
680    const form = input.closest('form');
681    if (form && !form.dataset.vpPhoneValidateBound) {
682      form.dataset.vpPhoneValidateBound = '1';
683
684      const syncContactMethodFields = () => {
685        if (!form.querySelector('.group [data-show]')) return;
686
687        form.querySelectorAll('.change').forEach((wrap) => {
688          const active = wrap.classList.contains('sel');
689          wrap.querySelectorAll('input, textarea, select').forEach((field) => {
690            // disabled — CF7 и браузер не валидируют скрытые способы связи
691            field.disabled = !active;
692            if (!active) {
693              field.classList.remove('wpcf7-not-valid');
694              field.removeAttribute('aria-invalid');
695              const tip = field
696                .closest('.wpcf7-form-control-wrap')
697                ?.querySelector('.wpcf7-not-valid-tip');
698              if (tip) tip.remove();
699            }
700          });
701        });
702      };
703
704      const getFormFields = () =>
705        Array.from(
706          form.querySelectorAll(
707            'input:not([type="hidden"]):not([type="submit"]):not([type="button"]):not([type="checkbox"]):not([type="radio"]):not(:disabled), textarea:not(:disabled), select:not(:disabled)'
708          )
709        );
710
711      const isEmptyRequired = (field) => {
712        if (field.classList.contains('phone')) return false;
713        if (field.disabled) return false;
714        const required =
715          field.required ||
716          field.getAttribute('aria-required') === 'true' ||
717          field.classList.contains('wpcf7-validates-as-required');
718        return required && !String(field.value || '').trim();
719      };
720
721      const showFieldError = (field, message) => {
722        field.classList.add('wpcf7-not-valid');
723        field.setAttribute('aria-invalid', 'true');
724
725        const wrap =
726          field.closest('.wpcf7-form-control-wrap') ||
727          field.closest('.wpcf7-acceptance') ||
728          field.parentElement;
729        if (!wrap) return;
730
731        let tip = wrap.querySelector('.wpcf7-not-valid-tip');
732        if (!tip) {
733          tip = document.createElement('span');
734          tip.className = 'wpcf7-not-valid-tip';
735          tip.setAttribute('aria-hidden', 'true');
736          wrap.appendChild(tip);
737        }
738        tip.textContent = message;
739      };
740
741      const clearFieldError = (field) => {
742        if (!field) return;
743        field.classList.remove('wpcf7-not-valid');
744        field.removeAttribute('aria-invalid');
745
746        const wrap =
747          field.closest('.wpcf7-form-control-wrap') ||
748          field.closest('.wpcf7-acceptance') ||
749          field.parentElement;
750        const tip = wrap && wrap.querySelector('.wpcf7-not-valid-tip');
751        if (tip) tip.remove();
752      };
753
754      // Подсветка остальных обязательных полей в стиле CF7 (когда CF7 мы блокируем)
755      const highlightOtherInvalidFields = () => {
756        getFormFields().forEach((field) => {
757          if (field.classList.contains('phone')) return;
758          if (isEmptyRequired(field)) {
759            showFieldError(field, 'Обязательное поле');
760          }
761        });
762
763        form.querySelectorAll('input[type="checkbox"]:not(:disabled)').forEach((cb) => {
764          const isAcceptance =
765            cb.name === 'consent' ||
766            cb.name === 'your-accept' ||
767            cb.classList.contains('vp-consent') ||
768            cb.closest('.wpcf7-acceptance');
769          const required =
770            cb.required || cb.getAttribute('aria-required') === 'true' || isAcceptance;
771
772          if (required && !cb.checked) {
773            showFieldError(cb, 'Обязательное поле');
774          }
775        });
776      };
777
778      const focusField = (field) => {
779        if (!field || field.disabled) return;
780        field.focus();
781        setTimeout(() => field.focus(), 0);
782        setTimeout(() => field.focus(), 50);
783        setTimeout(() => field.focus(), 150);
784      };
785
786      // Логика телефона наша; при ошибке телефона CF7 не запускается —
787      // поэтому остальные обязательные поля подсвечиваем сами, как CF7.
788      const blockInvalidPhones = (e) => {
789        syncContactMethodFields();
790
791        const phones = form.querySelectorAll('.phone');
792        let hasInvalidPhone = false;
793
794        phones.forEach((phoneEl) => {
795          if (phoneEl.disabled) return;
796          if (typeof phoneEl._vpValidatePhone === 'function' && !phoneEl._vpValidatePhone()) {
797            hasInvalidPhone = true;
798          }
799        });
800
801        // Телефон ок — отдаём CF7 (ФИО, согласие и т.д. подсветятся как раньше)
802        if (!hasInvalidPhone) return false;
803
804        highlightOtherInvalidFields();
805
806        e.preventDefault();
807        e.stopImmediatePropagation();
808
809        const firstInvalid = [
810          ...getFormFields(),
811          ...form.querySelectorAll('input[type="checkbox"]:not(:disabled)'),
812        ].find((field) => field.classList.contains('wpcf7-not-valid'));
813
814        focusField(firstInvalid || form.querySelector('.phone.wpcf7-not-valid:not(:disabled)'));
815        return true;
816      };
817
818      syncContactMethodFields();
819
820      // Снимаем ошибки сразу при заполнении / чекбоксе — не ждать blur
821      form.addEventListener('input', (e) => {
822        const field = e.target;
823        if (!(field instanceof HTMLElement)) return;
824        if (field.classList.contains('phone')) return;
825        if (field.matches('input, textarea, select')) {
826          clearFieldError(field);
827        }
828      });
829
830      form.addEventListener('change', (e) => {
831        const field = e.target;
832        if (!(field instanceof HTMLElement)) return;
833        if (field.matches('input[type="checkbox"], input[type="radio"], select')) {
834          clearFieldError(field);
835        }
836      });
837
838      form.addEventListener('submit', blockInvalidPhones, true);
839
840      form
841        .querySelectorAll('button:not([type="button"]), input[type="submit"], .wpcf7-submit')
842        .forEach((btn) => {
843          btn.addEventListener('click', blockInvalidPhones, true);
844        });
845
846      form.querySelectorAll('.group [data-show]').forEach((tab) => {
847        tab.addEventListener('click', () => {
848          // scripts.js переключает .sel синхронно; синхронизируем после него
849          setTimeout(syncContactMethodFields, 0);
850        });
851      });
852    }
853
854    setTimeout(fixInputPadding, 200);
855  });
856
857  // Если CF7 всё же прошёл и сфокусировал поле после телефона — вернём фокус
858  if (!document.documentElement.dataset.vpPhoneCf7Bound) {
859    document.documentElement.dataset.vpPhoneCf7Bound = '1';
860
861    document.addEventListener('wpcf7invalid', (event) => {
862      const form = event.target;
863      if (!(form instanceof HTMLFormElement)) return;
864
865      const fields = Array.from(
866        form.querySelectorAll(
867          'input:not([type="hidden"]):not([type="submit"]):not([type="button"]):not([type="checkbox"]):not([type="radio"]):not(:disabled), textarea:not(:disabled), select:not(:disabled)'
868        )
869      );
870
871      form.querySelectorAll('.phone:not(:disabled)').forEach((phoneEl) => {
872        if (typeof phoneEl._vpValidatePhone === 'function') {
873          phoneEl._vpValidatePhone();
874        }
875      });
876
877      const firstInvalid = fields.find((field) => {
878        if (field.classList.contains('phone')) {
879          return field.classList.contains('wpcf7-not-valid');
880        }
881        return (
882          field.classList.contains('wpcf7-not-valid') ||
883          field.getAttribute('aria-invalid') === 'true'
884        );
885      });
886
887      if (firstInvalid && firstInvalid.classList.contains('phone')) {
888        setTimeout(() => firstInvalid.focus(), 0);
889        setTimeout(() => firstInvalid.focus(), 50);
890      }
891    });
892  }
893}
894
895function fixInputPadding(input) {
896  if (!input) return;
897
898  const itiWrapper = input.closest('.iti');
899  if (!itiWrapper) return;
900
901  if (getComputedStyle(itiWrapper).display === 'none') return;
902
903  const btn = itiWrapper.querySelector('.iti__selected-country');
904  if (!btn) return;
905
906  const width = btn.getBoundingClientRect().width;
907  if (!width) return;
908
909  input.style.paddingLeft = width + 6 + 'px';
910}
911
912function initializeSliders() {
913  document.querySelectorAll('.js-before-after')?.forEach(initBeforeAfterSlider);
914  initCasesSlider();
915  initWhySlider();
916  initAwardsSlider();
917  initServicesSlider();
918  initShowcaseSlider();
919  initNewMediaSlider();
920  initAchievementSlider();
921  initHeroAwardsSlider();
922  initStagesSlider();
923  initStagesSlider2();
924  initStagesSlider3();
925  initStagesSlider4();
926  initStagesSlider5();
927  initVpCardsSliders();
928  initCompositionSlider();
929  initResultSlider();
930  initResultSlider2();
931  initProcessSlider();
932  initWorkflowSlider();
933  initStepsSlider();
934  initAuditSlider();
935  //initSingleSlider();
936  initMaterialsSlider();
937  initPhotosSlider();
938  initBlogPostSlider();
939  initCategoryHeroSlider();
940
941  const responsiveSwipers = [];
942
943  responsiveSwipers.push(
944    setupResponsiveSwiper({
945      selector: '.consultation__awards',
946      breakpoint: 768,
947      options: {
948        slidesPerView: 1,
949        spaceBetween: 20,
950        loop: false,
951        pagination: {
952          el: '.swiper-pagination',
953          clickable: true,
954        },
955      },
956    })
957  );
958
959  document.addEventListener('click', (e) => {
960    const btn = e.target.closest('.slide-to-button');
961    if (!btn) return;
962
963    const sliderName = btn.dataset.slider;
964    const index = +btn.dataset.index - 1;
965
966    const container = btn.dataset.container;
967    const component = btn.closest(container) || btn.closest('.section');
968
969    const sliderEl = component.querySelector(`.swiper[data-slider-name="${sliderName}"]`);
970
971    if (!sliderEl || !sliderEl.swiper) return;
972
973    const swiper = sliderEl.swiper;
974
975    const slideCount = swiper.slides.length;
976    if (index < 0 || index >= slideCount) return;
977
978    if (swiper.params.loop) {
979      swiper.slideToLoop(index);
980    } else {
981      swiper.slideTo(index);
982    }
983  });
984}
985
986function tabsInit() {
987  const tabSections = document.querySelectorAll('.tab');
988
989  tabSections.forEach(function (tab) {
990    const tabButtons = tab.querySelectorAll('.tab-button');
991    const tabSelect = tab.querySelector('select');
992
993    tabButtons.forEach(function (button) {
994      button.addEventListener('click', function () {
995        if (this.classList.contains('tab-button--active')) return;
996
997        const contentID = this.dataset.content;
998        const activeContent = tab.querySelector('.tab-content--active');
999        const activeButton = tab.querySelector('.tab-button--active');
1000        const contentToActivate = document.getElementById(contentID);
1001
1002        if (contentToActivate) {
1003          activeContent?.classList.remove('tab-content--active');
1004          activeButton?.classList.remove('tab-button--active');
1005
1006          this.classList.add('tab-button--active');
1007          contentToActivate.classList.add('tab-content--active');
1008
1009          if (tabSelect) {
1010            tabSelect.value = contentID;
1011            tabSelect.dispatchEvent(new Event('change', { bubbles: true }));
1012          }
1013        }
1014      });
1015    });
1016
1017    if (tabSelect) {
1018      tabSelect.addEventListener('change', function () {
1019        const selectedValue = this.value;
1020        const correspondingButton = tab.querySelector(
1021          `.tab-button[data-content="${selectedValue}"]`
1022        );
1023        if (correspondingButton) {
1024          correspondingButton.click();
1025        }
1026      });
1027    }
1028  });
1029}
1030
1031function setupResponsiveSwiper({ selector, breakpoint = 768, options = {} }) {
1032  let swiperInstance;
1033
1034  function checkSwiper() {
1035    const windowWidth = window.innerWidth;
1036
1037    if (windowWidth < breakpoint && !swiperInstance) {
1038      swiperInstance = new Swiper(selector, options);
1039    } else if (windowWidth >= breakpoint && swiperInstance) {
1040      swiperInstance.destroy(true, true);
1041      swiperInstance = undefined;
1042    }
1043  }
1044
1045  checkSwiper();
1046
1047  window.addEventListener('resize', checkSwiper);
1048  window.addEventListener('orientationchange', () => setTimeout(checkSwiper, 200));
1049
1050  return {
1051    getInstance: () => swiperInstance,
1052  };
1053}
1054
1055function initPhotosSlider() {
1056  document.querySelectorAll('.photos__slider').forEach((sliderEl, i) => {
1057    const swiper = new Swiper(sliderEl, {
1058      freeMode: true,
1059      speed: 600,
1060      spaceBetween: 0,
1061      breakpoints: {
1062        0: {
1063          slidesPerView: 'auto',
1064        },
1065        768: {
1066          slidesPerView: 'auto',
1067        },
1068        1024: {
1069          slidesPerView: 4,
1070        },
1071      },
1072    });
1073  });
1074}
1075
1076function initShowcaseSlider() {
1077  document.querySelectorAll('.showcase').forEach(showcase => {
1078    if (showcase.dataset.inited) return;
1079    showcase.dataset.inited = '1';
1080
1081    const thumbsEl = showcase.querySelector('.showcase__thumbs-slider');
1082    const mainEl   = showcase.querySelector('.showcase__slider');
1083    if (!mainEl) return;
1084
1085    const SPEED = 600;
1086    const prevBtns = showcase.querySelectorAll('.showcase__nav--prev');
1087    const nextBtns = showcase.querySelectorAll('.showcase__nav--next');
1088
1089    let showcaseThumbs = null;
1090    let showcaseMain = null;
1091    let activeThumbIndex = 0;
1092
1093    function updateNav() {
1094      const target = showcaseThumbs || showcaseMain;
1095      if (!target) return;
1096      const atStart = target.isBeginning;
1097      const atEnd = target.isEnd;
1098      prevBtns.forEach(btn => {
1099        btn.classList.toggle('swiper-button-disabled', atStart);
1100        btn.setAttribute('aria-disabled', atStart ? 'true' : 'false');
1101      });
1102      nextBtns.forEach(btn => {
1103        btn.classList.toggle('swiper-button-disabled', atEnd);
1104        btn.setAttribute('aria-disabled', atEnd ? 'true' : 'false');
1105      });
1106    }
1107
1108    function setActiveThumb(idx) {
1109      activeThumbIndex = idx;
1110      if (!showcaseThumbs) return;
1111      showcaseThumbs.slides.forEach((slide, i) => {
1112        slide.classList.toggle('swiper-slide-thumb-active', i === idx);
1113      });
1114    }
1115
1116    if (thumbsEl) {
1117      const wrapper = thumbsEl.querySelector('.swiper-wrapper');
1118      Array.from(wrapper.children).forEach((slide, i) => {
1119        slide.dataset.thumbIndex = i;
1120      });
1121
1122      showcaseThumbs = new Swiper(thumbsEl, {
1123        slidesPerView: 4,
1124        spaceBetween: 36,
1125        loop: false,
1126        watchOverflow: true,
1127        speed: SPEED,
1128        initialSlide: 1,
1129        watchSlidesProgress: true,
1130        breakpoints: {
1131          0:   { slidesPerView: 2 },
1132          576: { slidesPerView: 3 },
1133          992: { slidesPerView: 4 },
1134        },
1135      });
1136    }
1137
1138    showcaseMain = new Swiper(mainEl, {
1139      slidesPerView: 'auto',
1140      spaceBetween: 0,
1141      effect: 'slide',
1142      speed: SPEED,
1143      loop: false,
1144      watchOverflow: true,
1145      // Верхний слайдер меняется только кликом по превью
1146      allowTouchMove: !showcaseThumbs,
1147      on: {
1148        slideChange: () => {
1149          showcase.querySelectorAll('.video-js').forEach(wrapper => {
1150            if (wrapper.resetVideo) wrapper.resetVideo();
1151          });
1152          if (!showcaseThumbs) updateNav();
1153        },
1154      },
1155    });
1156
1157    if (showcaseThumbs) {
1158      setActiveThumb(0);
1159
1160      showcaseThumbs.on('click', (s) => {
1161        const slide = s.clickedSlide;
1162        if (!slide) return;
1163        const idx = Number(slide.dataset.thumbIndex);
1164        if (Number.isNaN(idx) || idx === activeThumbIndex) return;
1165        setActiveThumb(idx);
1166        showcaseMain.slideTo(idx);
1167      });
1168
1169      showcaseThumbs.on('slideChange', updateNav);
1170      showcaseThumbs.on('reachBeginning', updateNav);
1171      showcaseThumbs.on('reachEnd', updateNav);
1172      showcaseThumbs.on('fromEdge', updateNav);
1173    }
1174
1175    nextBtns.forEach(btn =>
1176      btn.addEventListener('click', () => {
1177        if (btn.classList.contains('swiper-button-disabled')) return;
1178        if (showcaseThumbs) showcaseThumbs.slideNext();
1179        else showcaseMain.slideNext();
1180      })
1181    );
1182    prevBtns.forEach(btn =>
1183      btn.addEventListener('click', () => {
1184        if (btn.classList.contains('swiper-button-disabled')) return;
1185        if (showcaseThumbs) showcaseThumbs.slidePrev();
1186        else showcaseMain.slidePrev();
1187      })
1188    );
1189
1190    updateNav();
1191  });
1192}
1193
1194function initNewMediaSlider() {
1195  const wrap = document.querySelector('.new-media__slider-wrap');
1196  const el = wrap?.querySelector('.new-media__slider');
1197  if (!el) return;
1198
1199  const mq = window.matchMedia('(max-width: 1024px)');
1200  let swiper = null;
1201
1202  const sync = () => {
1203    if (mq.matches) {
1204      if (swiper) return;
1205      swiper = new Swiper(el, {
1206        slidesPerView: 'auto',
1207        centeredSlides: true,
1208        loop: true,
1209        speed: 500,
1210        spaceBetween: 24,
1211        navigation: {
1212          nextEl: wrap.querySelector('.new-media__nav--next'),
1213          prevEl: wrap.querySelector('.new-media__nav--prev'),
1214        },
1215      });
1216      return;
1217    }
1218
1219    if (swiper) {
1220      swiper.destroy(true, true);
1221      swiper = null;
1222    }
1223  };
1224
1225  if (mq.addEventListener) {
1226    mq.addEventListener('change', sync);
1227  } else {
1228    mq.addListener(sync);
1229  }
1230
1231  sync();
1232}
1233
1234function initAchievementSlider() {
1235  document.querySelectorAll('.achievement__items').forEach((items) => {
1236    if (items.closest('.achievement__slider-wrap')) return;
1237
1238    const wrap = document.createElement('div');
1239    wrap.className = 'achievement__slider-wrap';
1240
1241    const slider = document.createElement('div');
1242    slider.className = 'swiper achievement__slider';
1243
1244    items.parentNode.insertBefore(wrap, items);
1245    wrap.appendChild(slider);
1246    slider.appendChild(items);
1247
1248    items.classList.add('swiper-wrapper');
1249    items.querySelectorAll('.achievement__item').forEach((item) => {
1250      item.classList.add('swiper-slide');
1251    });
1252
1253    const nav = document.createElement('div');
1254    nav.className = 'slider-nav slider-nav--absolute achievement__nav show-tablet';
1255    nav.innerHTML = `
1256      <button type="button" class="btn-reset slider-nav__button slider-nav__button--prev achievement__nav--prev" aria-label="Предыдущий слайд"></button>
1257      <button type="button" class="btn-reset slider-nav__button slider-nav__button--next achievement__nav--next" aria-label="Следующий слайд"></button>
1258    `;
1259    wrap.appendChild(nav);
1260
1261    const mq = window.matchMedia('(max-width: 1024px)');
1262    let swiper = null;
1263
1264    const sync = () => {
1265      if (mq.matches) {
1266        if (swiper) return;
1267        swiper = new Swiper(slider, {
1268          slidesPerView: 'auto',
1269          centeredSlides: true,
1270          loop: true,
1271          speed: 500,
1272          spaceBetween: 24,
1273          navigation: {
1274            nextEl: wrap.querySelector('.achievement__nav--next'),
1275            prevEl: wrap.querySelector('.achievement__nav--prev'),
1276          },
1277        });
1278        return;
1279      }
1280
1281      if (swiper) {
1282        swiper.destroy(true, true);
1283        swiper = null;
1284      }
1285    };
1286
1287    if (mq.addEventListener) {
1288      mq.addEventListener('change', sync);
1289    } else {
1290      mq.addListener(sync);
1291    }
1292
1293    sync();
1294  });
1295}
1296
1297function initHeroAwardsSlider() {
1298  document.querySelectorAll('.new-hero__awards--after .new-hero__awards-inner').forEach((items) => {
1299    if (items.closest('.new-hero__awards-slider-wrap')) return;
1300
1301    const wrap = document.createElement('div');
1302    wrap.className = 'new-hero__awards-slider-wrap';
1303
1304    const slider = document.createElement('div');
1305    slider.className = 'swiper new-hero__awards-slider';
1306
1307    items.parentNode.insertBefore(wrap, items);
1308    wrap.appendChild(slider);
1309    slider.appendChild(items);
1310
1311    items.classList.add('swiper-wrapper');
1312    items.querySelectorAll('.new-hero__award').forEach((item) => {
1313      item.classList.add('swiper-slide');
1314    });
1315
1316    const nav = document.createElement('div');
1317    nav.className = 'slider-nav slider-nav--absolute new-hero__awards-nav show-tablet';
1318    nav.innerHTML = `
1319      <button type="button" class="btn-reset slider-nav__button slider-nav__button--prev new-hero__awards-nav--prev" aria-label="Предыдущий слайд"></button>
1320      <button type="button" class="btn-reset slider-nav__button slider-nav__button--next new-hero__awards-nav--next" aria-label="Следующий слайд"></button>
1321    `;
1322    wrap.appendChild(nav);
1323
1324    const mq = window.matchMedia('(max-width: 1024px)');
1325    let swiper = null;
1326
1327    const sync = () => {
1328      if (mq.matches) {
1329        if (swiper) return;
1330        swiper = new Swiper(slider, {
1331          slidesPerView: 'auto',
1332          centeredSlides: true,
1333          loop: true,
1334          loopedSlides: 2,
1335          speed: 500,
1336          spaceBetween: 24,
1337          navigation: {
1338            nextEl: wrap.querySelector('.new-hero__awards-nav--next'),
1339            prevEl: wrap.querySelector('.new-hero__awards-nav--prev'),
1340          },
1341        });
1342        return;
1343      }
1344
1345      if (swiper) {
1346        swiper.destroy(true, true);
1347        swiper = null;
1348      }
1349    };
1350
1351    if (mq.addEventListener) {
1352      mq.addEventListener('change', sync);
1353    } else {
1354      mq.addListener(sync);
1355    }
1356
1357    sync();
1358  });
1359}
1360
1361function initCompositionSlider() {
1362  const compositionSlider = new Swiper('.composition__slider', {
1363    slidesPerView: 1,
1364    centeredSlides: true,
1365    spaceBetween: 20,
1366    loop: true,
1367    speed: 400,
1368    pagination: {
1369      el: '.composition__pagination',
1370      clickable: true,
1371    },
1372
1373    navigation: {
1374      nextEl: '.composition__nav--next',
1375      prevEl: '.composition__nav--prev',
1376    },
1377
1378    breakpoints: {
1379      768: {
1380        slidesPerView: 1.2,
1381        spaceBetween: 40,
1382      },
1383      1025: {
1384        slidesPerView: 1.4,
1385        spaceBetween: 40,
1386      },
1387      1366: {
1388        slidesPerView: 1.6,
1389        spaceBetween: 100,
1390      },
1391    },
1392  });
1393}
1394
1395function initStagesSlider() {
1396  const sliderEl = document.querySelector('.stages__slider');
1397
1398  if (!sliderEl) return;
1399
1400  const slidesCount = sliderEl.querySelectorAll('.swiper-slide').length;
1401
1402  const slidesPerView = 3;
1403  const isEnoughSlides = slidesCount > slidesPerView;
1404
1405  const stagesSlider = new Swiper(sliderEl, {
1406    grabCursor: true,
1407    loop: isEnoughSlides,
1408    centeredSlides: isEnoughSlides,
1409    speed: 500,
1410    loop: true,
1411    pagination: {
1412      el: '.stages__pagination',
1413      clickable: true,
1414    },
1415    navigation: {
1416      nextEl: '.stages__nav--next',
1417      prevEl: '.stages__nav--prev',
1418    },
1419
1420    slidesPerView: slidesPerView,
1421    spaceBetween: 74,
1422
1423    breakpoints: {
1424      0: {
1425        slidesPerView: 1,
1426        centeredSlides: true,
1427        spaceBetween: 10,
1428      },
1429      768: {
1430        spaceBetween: 30,
1431      },
1432      1440: {
1433        spaceBetween: 74,
1434      },
1435    },
1436  });
1437}
1438function initStagesSlider3() {
1439  const sliderEl = document.querySelector('.stages__slider3');
1440
1441  if (!sliderEl) return;
1442
1443  const slidesCount = sliderEl.querySelectorAll('.swiper-slide').length;
1444
1445  const slidesPerView = 4;
1446  const isEnoughSlides = slidesCount > slidesPerView;
1447
1448  const stagesSlider = new Swiper(sliderEl, {
1449    grabCursor: true,
1450    loop: isEnoughSlides,
1451    centeredSlides: isEnoughSlides,
1452    speed: 500,
1453    loop: true,
1454    navigation: {
1455      nextEl: '.stages3__nav--next',
1456      prevEl: '.stages3__nav--prev',
1457    },
1458
1459    slidesPerView: slidesPerView,
1460    spaceBetween: 0,
1461
1462    breakpoints: {
1463      0: {
1464        slidesPerView: 1,
1465        centeredSlides: true,
1466      },
1467      768: {
1468        slidesPerView: 2,
1469      },
1470      1024: {
1471        slidesPerView: 3,
1472      },
1473      1440: {
1474        slidesPerView: 4,
1475      },
1476    },
1477  });
1478}
1479function initStagesSlider4() {
1480  const sliderEl = document.querySelector('.stages__slider4');
1481
1482  if (!sliderEl) return;
1483
1484  const slidesCount = sliderEl.querySelectorAll('.swiper-slide').length;
1485
1486  const slidesPerView = 4;
1487  const isEnoughSlides = slidesCount > slidesPerView;
1488
1489  const stagesSlider = new Swiper(sliderEl, {
1490    grabCursor: true,
1491    loop: isEnoughSlides,
1492    centeredSlides: isEnoughSlides,
1493    speed: 500,
1494    loop: true,
1495    navigation: {
1496      nextEl: '.stages4__nav--next',
1497      prevEl: '.stages4__nav--prev',
1498    },
1499
1500    slidesPerView: slidesPerView,
1501    spaceBetween: 0,
1502
1503    breakpoints: {
1504      0: {
1505        slidesPerView: 1,
1506        centeredSlides: true,
1507      },
1508      768: {
1509        slidesPerView: 2,
1510      },
1511      1024: {
1512        slidesPerView: 3,
1513      },
1514      1440: {
1515        slidesPerView: 4,
1516      },
1517    },
1518  });
1519}
1520function markStagesCurrentSlides(swiper) {
1521  const desktop = window.innerWidth >= 1024;
1522  const perView = Number(swiper.params.slidesPerView);
1523  const count = desktop && Number.isFinite(perView) ? perView : 0;
1524  const start = swiper.activeIndex;
1525
1526  swiper.slides.forEach((slide, index) => {
1527    slide.classList.toggle('is-current', count > 0 && index >= start && index < start + count);
1528  });
1529}
1530function initStagesSlider2() {
1531  const sliderEl = document.querySelector('.stages__slider2');
1532
1533  if (!sliderEl) return;
1534
1535  const slidesCount = sliderEl.querySelectorAll('.swiper-slide').length;
1536
1537  const slidesPerView = 3;
1538
1539  let initialSlide = 0;
1540  if (slidesCount > 4) initialSlide = 2;
1541  else if (slidesCount > 2) initialSlide = 1;
1542
1543  const stagesSlider = new Swiper(sliderEl, {
1544    grabCursor: true,
1545    centeredSlides: true,
1546    speed: 500,
1547    loop: false,
1548    initialSlide,
1549    pagination: {
1550      el: '.stages2__pagination',
1551      clickable: true,
1552    },
1553    navigation: {
1554      nextEl: '.stages2__nav--next',
1555      prevEl: '.stages2__nav--prev',
1556    },
1557
1558    slidesPerView: slidesPerView,
1559    spaceBetween: 74,
1560
1561    breakpoints: {
1562      0: {
1563        spaceBetween: 0,
1564      },
1565      1024: {
1566        spaceBetween: 30,
1567      },
1568      1440: {
1569        spaceBetween: 74,
1570      },
1571    },
1572  });
1573}
1574function initStagesSlider5() {
1575  //const sliderEl = document.querySelector('.stages__slider5');
1576
1577  document.querySelectorAll('.stages_slider5')?.forEach((sliderEl) => {
1578    if (!sliderEl) return;
1579
1580    const sliderElswiper = sliderEl.querySelector('.stages__slider5');
1581
1582    const prevBtn = sliderEl.querySelectorAll('.steps__nav--prev');
1583    const nextBtn = sliderEl.querySelectorAll('.steps__nav--next, .slide-next');
1584
1585    // Catalog keeps auto-width side peeks below 1024px. From 1024px both skins
1586    // show three slides and step by three, without centering.
1587    const isCatalog = sliderEl.classList.contains('stages--catalog');
1588
1589    const stagesSlider = new Swiper(sliderElswiper, {
1590      grabCursor: true,
1591      centeredSlides: true,
1592      speed: 500,
1593      loop: false,
1594      initialSlide: 0,
1595      slidesPerGroup: 1,
1596      navigation: {
1597        nextEl: sliderEl.querySelector('.stages2__nav--next'),
1598        prevEl: sliderEl.querySelector('.stages2__nav--prev'),
1599      },
1600      pagination: {
1601        el: sliderEl.querySelector('.stages5__pagination'),
1602        clickable: true,
1603      },
1604
1605      slidesPerView: isCatalog ? 'auto' : 1,
1606      spaceBetween: isCatalog ? 14 : 0,
1607
1608      breakpoints: isCatalog
1609        ? {
1610            0: { spaceBetween: 14, slidesPerView: 'auto', slidesPerGroup: 1, centeredSlides: true },
1611            1024: { spaceBetween: 36, slidesPerView: 3, slidesPerGroup: 3, centeredSlides: false },
1612            1440: { spaceBetween: 56, slidesPerView: 3, slidesPerGroup: 3, centeredSlides: false },
1613          }
1614        : {
1615            0: {
1616              spaceBetween: 0,
1617              slidesPerView: 1,
1618              slidesPerGroup: 1,
1619              centeredSlides: true,
1620            },
1621            1024: {
1622              spaceBetween: 30,
1623              slidesPerView: 3,
1624              slidesPerGroup: 3,
1625              centeredSlides: false,
1626            },
1627            1440: {
1628              spaceBetween: 74,
1629              slidesPerView: 3,
1630              slidesPerGroup: 3,
1631              centeredSlides: false,
1632            },
1633          },
1634
1635      on: {
1636        init: markStagesCurrentSlides,
1637        slideChange: markStagesCurrentSlides,
1638        breakpoint: markStagesCurrentSlides,
1639        resize: markStagesCurrentSlides,
1640      },
1641    });
1642    nextBtn?.forEach((btn) => {
1643      btn.addEventListener('click', () => stagesSlider.slideNext());
1644    });
1645
1646    prevBtn?.forEach((btn) => {
1647      btn.addEventListener('click', () => stagesSlider.slidePrev());
1648    });
1649  });
1650}
1651
1652// Card sliders on catalog terms (blocks A/B, brief 4.6). Two skins share one
1653// init: "concepts" centers the active framed slide, "features" is a plain
1654// multi-card loop. Fraction counter is shown on mobile only.
1655function initVpCardsSliders() {
1656  document.querySelectorAll('.vp-cards').forEach((sectionEl) => {
1657    const swiperEl = sectionEl.querySelector('.vp-cards__slider');
1658    if (!swiperEl) return;
1659
1660    const isConcepts = sectionEl.classList.contains('vp-cards--concepts');
1661    const total =
1662      parseInt(swiperEl.dataset.total || '0', 10) ||
1663      swiperEl.querySelectorAll('.swiper-slide').length;
1664    const currentEl = sectionEl.querySelector('.slider-nums__current');
1665    const totalEl = sectionEl.querySelector('.slider-nums__total');
1666
1667    const slider = new Swiper(swiperEl, {
1668      grabCursor: true,
1669      speed: 500,
1670      centeredSlides: isConcepts,
1671      loop: false,
1672      rewind: true,
1673      initialSlide: isConcepts && window.innerWidth >= 1024 ? 1 : 0,
1674      breakpoints: isConcepts
1675        ? {
1676            0: { slidesPerView: 'auto', spaceBetween: 24 },
1677            768: { slidesPerView: 'auto', spaceBetween: 36 },
1678            1024: { slidesPerView: 'auto', spaceBetween: 29 },
1679            1700: { slidesPerView: 'auto', spaceBetween: 34 },
1680            1920: { slidesPerView: 'auto', spaceBetween: 40 },
1681          }
1682        : {
1683            0: { slidesPerView: 'auto', spaceBetween: 4 },
1684            768: { slidesPerView: 'auto', spaceBetween: 24 },
1685            1200: { slidesPerView: 'auto', spaceBetween: 68 },
1686            1800: { slidesPerView: 'auto', spaceBetween: 85 },
1687            1920: { slidesPerView: 'auto', spaceBetween: 91 },
1688          },
1689      on: {
1690        init(sw) {
1691          if (totalEl) totalEl.textContent = total;
1692          if (currentEl) currentEl.textContent = (sw.realIndex % total) + 1;
1693        },
1694        slideChange(sw) {
1695          if (currentEl) currentEl.textContent = (sw.realIndex % total) + 1;
1696        },
1697      },
1698    });
1699
1700    sectionEl.querySelectorAll('.vp-cards__nav--prev, .vp-cards__nums-btn--prev').forEach((btn) => {
1701      btn.addEventListener('click', () => slider.slidePrev());
1702    });
1703    sectionEl.querySelectorAll('.vp-cards__nav--next, .vp-cards__nums-btn--next').forEach((btn) => {
1704      btn.addEventListener('click', () => slider.slideNext());
1705    });
1706  });
1707}
1708
1709function initAuditSlider() {
1710  document.querySelectorAll('.audit__slider').forEach((sliderEl) => {
1711    const wrapper = sliderEl.closest('.audit__slider-wrapper') || sliderEl.closest('.section');
1712
1713    const swiper = new Swiper(sliderEl, {
1714      grabCursor: true,
1715      slidesPerView: 3,
1716      spaceBetween: 30,
1717      loop: true,
1718      speed: 500,
1719      navigation: {
1720        nextEl: '.audit__nav--next',
1721        prevEl: '.audit__nav--prev',
1722      },
1723      on: {
1724        init() {
1725          toggleLockedClass(this, wrapper);
1726        },
1727        resize() {
1728          toggleLockedClass(this, wrapper);
1729        },
1730      },
1731      breakpoints: {
1732        0: {
1733          slidesPerView: 1,
1734          spaceBetween: 50,
1735        },
1736        1025: {
1737          spaceBetween: 30,
1738        },
1739        1440: {
1740          spaceBetween: 100,
1741        },
1742      },
1743    });
1744  });
1745}
1746
1747function initStepsSlider() {
1748  document.querySelectorAll('.steps')?.forEach((stepsBlock) => {
1749    const sliderEl = stepsBlock.querySelector('.steps__slider');
1750    const prevBtn = stepsBlock.querySelectorAll('.steps__nav--prev');
1751    const nextBtn = stepsBlock.querySelectorAll('.steps__nav--next, .slide-next');
1752    const numsPagination = stepsBlock.querySelector('.slider-nums');
1753
1754    const currentEl = numsPagination.querySelector('.slider-nums__current');
1755    const totalEl = numsPagination.querySelector('.slider-nums__total');
1756
1757    let swiper;
1758
1759    const getActiveInnerSlider = () => {
1760      const activeSlide = swiper.slides[swiper.activeIndex];
1761      return activeSlide?.querySelector('.single-slider')?.swiper || null;
1762    };
1763
1764    const handleStepsNav = (direction) => {
1765      const innerSwiper = getActiveInnerSlider();
1766
1767      if (innerSwiper) {
1768        if (direction === 'next' && !innerSwiper.isEnd) {
1769          innerSwiper.slideNext();
1770          return;
1771        }
1772        if (direction === 'prev' && !innerSwiper.isBeginning) {
1773          innerSwiper.slidePrev();
1774          return;
1775        }
1776      }
1777
1778      if (direction === 'next') {
1779        swiper.slideNext();
1780      } else {
1781        swiper.slidePrev();
1782      }
1783    };
1784
1785    const updateArrows = () => {
1786      const innerSwiper = getActiveInnerSlider();
1787      const atStart = swiper.isBeginning && (!innerSwiper || innerSwiper.isBeginning);
1788      const atEnd = swiper.isEnd && (!innerSwiper || innerSwiper.isEnd);
1789
1790      prevBtn.forEach((btn) => {
1791        btn.classList.toggle('swiper-button-disabled', atStart);
1792        btn.setAttribute('aria-disabled', atStart ? 'true' : 'false');
1793      });
1794      nextBtn.forEach((btn) => {
1795        if (!btn.classList.contains('steps__nav--next')) return;
1796        btn.classList.toggle('swiper-button-disabled', atEnd);
1797        btn.setAttribute('aria-disabled', atEnd ? 'true' : 'false');
1798      });
1799    };
1800
1801    swiper = new Swiper(sliderEl, {
1802      grabCursor: true,
1803      centeredSlides: true,
1804      slidesPerView: 'auto',
1805      speed: 500,
1806      spaceBetween: 90,
1807
1808      breakpoints: {
1809        0: { spaceBetween: 15 },
1810        768: { spaceBetween: 30 },
1811        1024: { spaceBetween: 90 },
1812      },
1813
1814      on: {
1815        init(swiper) {
1816          totalEl.textContent = swiper.slides.length;
1817          currentEl.textContent = swiper.realIndex + 1;
1818        },
1819        slideChange(swiper) {
1820          currentEl.textContent = swiper.realIndex + 1;
1821        },
1822      },
1823    });
1824
1825    swiper.on('slideChange', updateArrows);
1826    updateArrows();
1827
1828    setTimeout(() => {
1829      sliderEl.querySelectorAll('.single-slider').forEach((el) => {
1830        if (!el.swiper || el.dataset.stepsArrowsBound) return;
1831        el.dataset.stepsArrowsBound = '1';
1832        el.swiper.on('slideChange', updateArrows);
1833      });
1834      updateArrows();
1835    }, 0);
1836
1837    nextBtn?.forEach((btn) => {
1838      btn.addEventListener('click', () => handleStepsNav('next'));
1839    });
1840
1841    prevBtn?.forEach((btn) => {
1842      btn.addEventListener('click', () => handleStepsNav('prev'));
1843    });
1844  });
1845}
1846
1847function initWorkflowSlider() {
1848  document.querySelectorAll('.workflow__slider')?.forEach((sliderEl) => {
1849    const wrapper = sliderEl.closest('.workflow') || sliderEl.closest('.section');
1850    const pagination = wrapper.querySelector('.workflow__pagination');
1851
1852    const swiper = new Swiper(sliderEl, {
1853      grabCursor: true,
1854      centeredSlides: true,
1855      slidesPerView: 1,
1856      loop: true,
1857      speed: 500,
1858      spaceBetween: 50,
1859      pagination: {
1860        el: pagination,
1861        clickable: true,
1862      },
1863      navigation: {
1864        nextEl: '.workflow__nav--next',
1865        prevEl: '.workflow__nav--prev',
1866      },
1867      breakpoints: {
1868        0: {
1869          spaceBetween: 20,
1870        },
1871        1024: {
1872          spaceBetween: 50,
1873        },
1874      },
1875    });
1876  });
1877}
1878
1879function initCategoryHeroSlider() {
1880  document.querySelectorAll('.js-category-hero-slider').forEach((sliderEl) => {
1881    const slidesCount = sliderEl.querySelectorAll('.swiper-slide').length;
1882    if (!slidesCount) return;
1883
1884    const rawInterval = Number(sliderEl.dataset.interval);
1885    const delay = Number.isFinite(rawInterval) && rawInterval > 0 ? rawInterval : 5000;
1886
1887    new Swiper(sliderEl, {
1888      effect: 'fade',
1889      fadeEffect: {
1890        crossFade: true,
1891      },
1892      loop: slidesCount > 1,
1893      speed: 800,
1894      allowTouchMove: slidesCount > 1,
1895      autoplay:
1896        slidesCount > 1
1897          ? {
1898              delay,
1899              disableOnInteraction: false,
1900            }
1901          : false,
1902    });
1903  });
1904}
1905
1906function initAwardsSlider() {
1907  const awardsSlider = new Swiper('.awards__slider:not(.awards__slider2)', {
1908    grabCursor: true,
1909    centeredSlides: true,
1910    slidesPerView: 1,
1911    // slidesPerView: 'auto',
1912    // effect: 'coverflow',
1913    effect: 'fade',
1914    fadeEffect: {
1915      crossFade: true,
1916    },
1917    loop: true,
1918    speed: 500,
1919    autoplay: {
1920      delay: 6000,
1921      disableOnInteraction: false,
1922    },
1923    // coverflowEffect: {
1924    //   rotate: 0,
1925    //   stretch: document.querySelector('.awards__slider')?.offsetWidth * 0.86,
1926    //   depth: 200,
1927    //   modifier: 1,
1928    //   slideShadows: true,
1929    // },
1930    pagination: {
1931      el: '.awards__pagination',
1932      clickable: true,
1933    },
1934    navigation: {
1935      nextEl: '.awards__slider-nav--next',
1936      prevEl: '.awards__slider-nav--prev',
1937    },
1938    breakpoints: {
1939      0: {
1940        slidesPerView: 1,
1941        centeredSlides: false,
1942        // effect: 'slide',
1943      },
1944      // 576: {
1945      //   effect: 'coverflow',
1946      // },
1947    },
1948    on: {
1949      slideChange: updateActiveItems,
1950      init: updateActiveItems,
1951      // resize: function () {
1952      //   if (this.params.effect === 'coverflow') {
1953      //     this.params.coverflowEffect.stretch =
1954      //       document.querySelector('.awards__slider').offsetWidth * 0.86;
1955      //     this.update();
1956      //   }
1957      // },
1958    },
1959  });
1960}
1961
1962function updateActiveItems() {
1963  const slides = document.querySelectorAll('.awards__slide');
1964  slides.forEach((slide) => {
1965    const items = slide.querySelectorAll('.award-card__item');
1966    if (slide.classList.contains('swiper-slide-active')) {
1967      items.forEach((item, i) => {
1968        item.style.setProperty('--index', i);
1969        item.classList.add('active');
1970      });
1971    } else {
1972      items.forEach((item) => item.classList.remove('active'));
1973    }
1974  });
1975}
1976
1977function initServicesSlider() {
1978  const servicesThumbs = new Swiper('.services__thumbs', {
1979    slidesPerView: 3,
1980    spaceBetween: 2,
1981    watchSlidesProgress: true,
1982    slideToClickedSlide: true,
1983    breakpoints: {
1984      0: {
1985        spaceBetween: 8,
1986        slidesPerView: 'auto',
1987        direction: 'vertical',
1988      },
1989      992: {
1990        slidesPerView: 3,
1991        direction: 'horizontal',
1992      },
1993    },
1994  });
1995
1996  const servicesSlider = new Swiper('.services__slider', {
1997    slidesPerView: 1,
1998    spaceBetween: 30,
1999    loop: true,
2000    speed: 600,
2001    pagination: {
2002      el: '.services__pagination--desktop',
2003      clickable: true,
2004      type: 'bullets',
2005    },
2006    thumbs: {
2007      swiper: servicesThumbs,
2008    },
2009    breakpoints: {
2010      0: {
2011        pagination: {
2012          el: '.services__pagination--mobile',
2013          clickable: true,
2014          type: 'bullets',
2015        },
2016      },
2017      993: {
2018        pagination: {
2019          el: '.services__pagination--desktop',
2020          clickable: true,
2021          type: 'bullets',
2022        },
2023      },
2024    },
2025  });
2026
2027  document.querySelectorAll('.services__slider-nav--next')?.forEach((btn) => {
2028    btn.addEventListener('click', () => servicesSlider.slideNext());
2029  });
2030
2031  document.querySelectorAll('.services__slider-nav--prev')?.forEach((btn) => {
2032    btn.addEventListener('click', () => servicesSlider.slidePrev());
2033  });
2034}
2035
2036function initCasesSlider() {
2037  document.querySelectorAll('.cases').forEach((cases) => {
2038    const sliderEl = cases.querySelector('.cases__slider');
2039    const nextBtn = cases.querySelector('.cases__slider-nav--next');
2040    const prevBtn = cases.querySelector('.cases__slider-nav--prev');
2041
2042    if (!sliderEl) return;
2043
2044    sliderEl.querySelectorAll('.case-card__preview').forEach((img) => {
2045      img.loading = 'eager';
2046    });
2047
2048    const slidesCount = sliderEl.querySelectorAll('.cases__slide').length;
2049    let initialSlide = 0;
2050    if (slidesCount > 4) initialSlide = 2;
2051    else if (slidesCount > 2) initialSlide = 1;
2052
2053    const casesSlider = new Swiper(sliderEl, {
2054      speed: 500,
2055      slidesPerView: 4,
2056      spaceBetween: 100,
2057      loop: false,
2058      initialSlide,
2059      slideToClickedSlide: true,
2060      centeredSlides: true,
2061      watchOverflow: true,
2062      noSwiping: true,
2063      noSwipingSelector: '[data-video-volume-range], [data-video-volume]',
2064      navigation: {
2065        nextEl: nextBtn,
2066        prevEl: prevBtn,
2067      },
2068      breakpoints: {
2069        0: { slidesPerView: 1.5, spaceBetween: 20 },
2070        576: { slidesPerView: 2, spaceBetween: 50 },
2071        992: { slidesPerView: 3, spaceBetween: 50 },
2072        1170: { slidesPerView: 3, spaceBetween: 100 },
2073        1500: { slidesPerView: 4, spaceBetween: 100 },
2074      },
2075      on: {
2076        init: (swiper) => {
2077          requestAnimationFrame(() => {
2078            requestAnimationFrame(() => {
2079              updateCasesNavWidth(swiper);
2080            });
2081          });
2082        },
2083        slideChange: updateCasesNavWidth,
2084        resize: updateCasesNavWidth,
2085        breakpoint: updateCasesNavWidth,
2086      },
2087    });
2088
2089    initCasesSlideHydration(cases, casesSlider);
2090
2091    // Пауза видео ТОЛЬКО внутри текущего cases
2092    casesSlider.on('slideChange', () => {
2093      cases.querySelectorAll('[data-video-card]').forEach((card) => {
2094        const video = card.querySelector('[data-video]');
2095        if (video && !video.paused) {
2096          video.pause();
2097          card.classList.remove('is-playing');
2098        }
2099      });
2100    });
2101  });
2102}
2103
2104const vpCasesSlideHtmlCache = {};
2105
2106function initCasesSlideHydration(cases, swiper) {
2107  const sliderEl = cases.querySelector('.cases__slider');
2108  if (!sliderEl || !sliderEl.querySelector('.cases__slide.is-empty')) return;
2109
2110  const layout = cases.dataset.casesLayout || 'inner';
2111  const ajaxUrl = (window.VPAjax && window.VPAjax.url) || '/wp-admin/admin-ajax.php';
2112  const queued = new Set();
2113
2114  const originals = () => Array.from(sliderEl.querySelectorAll('.cases__slide'));
2115
2116  const applyHtml = (id, html) => {
2117    vpCasesSlideHtmlCache[layout + ':' + id] = html;
2118    sliderEl.querySelectorAll('.cases__slide[data-post-id="' + id + '"]').forEach((slide) => {
2119      if (slide.querySelector('[data-video-card]')) return;
2120      slide.innerHTML = html;
2121      slide.classList.remove('is-empty');
2122      slide.querySelectorAll('.case-card__preview').forEach((img) => {
2123        img.loading = 'eager';
2124      });
2125    });
2126  };
2127
2128  const fetchIds = (ids) => {
2129    const unique = [];
2130    ids.forEach((id) => {
2131      if (!id) return;
2132      const key = layout + ':' + id;
2133      if (vpCasesSlideHtmlCache[key]) {
2134        applyHtml(id, vpCasesSlideHtmlCache[key]);
2135        return;
2136      }
2137      const filled = sliderEl.querySelector(
2138        '.cases__slide[data-post-id="' + id + '"]:not(.is-empty)'
2139      );
2140      if (filled && filled.querySelector('[data-video-card]')) {
2141        applyHtml(id, filled.innerHTML);
2142        return;
2143      }
2144      if (queued.has(key) || unique.indexOf(id) !== -1) return;
2145      unique.push(id);
2146    });
2147    if (!unique.length) return;
2148
2149    unique.forEach((id) => queued.add(layout + ':' + id));
2150    const body = new URLSearchParams();
2151    body.set('action', 'vp_cases_slide_html');
2152    body.set('layout', layout);
2153    unique.forEach((id) => body.append('ids[]', id));
2154
2155    fetch(ajaxUrl, {
2156      method: 'POST',
2157      credentials: 'same-origin',
2158      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
2159      body,
2160    })
2161      .then((r) => r.json())
2162      .then((json) => {
2163        const map = json && json.success && json.data ? json.data : {};
2164        unique.forEach((id) => {
2165          queued.delete(layout + ':' + id);
2166          const html = map[id] || map[String(id)];
2167          if (html) applyHtml(id, html);
2168        });
2169      })
2170      .catch(() => {
2171        unique.forEach((id) => queued.delete(layout + ':' + id));
2172      });
2173  };
2174
2175  const hydrateAround = () => {
2176    const slides = originals();
2177    const total = slides.length;
2178    if (!total) return;
2179
2180    const BUFFER = 3;
2181    const spv = Number(swiper.params.slidesPerView);
2182    const visible = Number.isFinite(spv) && spv > 0 ? Math.ceil(spv) : 4;
2183    const toEdge = Math.max(1, Math.ceil(visible / 2));
2184    const active = swiper.activeIndex;
2185    const firstVisible = Math.max(0, active - toEdge);
2186    const lastVisible = Math.min(total - 1, active + toEdge);
2187
2188    const ids = [];
2189    const takeEmpty = (from, to, step) => {
2190      for (let i = from; step > 0 ? i <= to : i >= to; i += step) {
2191        if (i < 0 || i >= total) break;
2192        const slide = slides[i];
2193        if (slide && slide.classList.contains('is-empty') && slide.dataset.postId) {
2194          ids.push(slide.dataset.postId);
2195          return;
2196        }
2197      }
2198    };
2199
2200    takeEmpty(firstVisible, lastVisible, 1);
2201    takeEmpty(lastVisible + 1, lastVisible + BUFFER, 1);
2202    takeEmpty(firstVisible - 1, firstVisible - BUFFER, -1);
2203
2204    fetchIds(ids);
2205  };
2206
2207  let moveTick = false;
2208  const hydrateOnMove = () => {
2209    if (moveTick) return;
2210    moveTick = true;
2211    requestAnimationFrame(() => {
2212      moveTick = false;
2213      hydrateAround();
2214    });
2215  };
2216
2217  hydrateAround();
2218  swiper.on('slideChange', hydrateAround);
2219  swiper.on('slideChangeTransitionStart', hydrateAround);
2220  swiper.on('sliderMove', hydrateOnMove);
2221  swiper.on('breakpoint', hydrateAround);
2222}
2223
2224function initWhySlider() {
2225  document.querySelectorAll('.why__slider')?.forEach((sliderEl) => {
2226    const swiper = new Swiper(sliderEl, {
2227      slidesPerView: 1,
2228      spaceBetween: 10,
2229      speed: 600,
2230      loop: true,
2231      autoHeight: true,
2232      autoplay: {
2233        delay: 5000000,
2234      },
2235      pagination: {
2236        el: sliderEl.closest('.why')?.querySelector('.why__pagination'),
2237        clickable: true,
2238        type: 'bullets',
2239      },
2240    });
2241
2242    sliderEl
2243      .closest('.why')
2244      ?.querySelectorAll('.why__slider-nav--next')
2245      .forEach((btn) => {
2246        btn.addEventListener('click', () => swiper.slideNext());
2247      });
2248
2249    sliderEl
2250      .closest('.why')
2251      ?.querySelectorAll('.why__slider-nav--prev')
2252      .forEach((btn) => {
2253        btn.addEventListener('click', () => swiper.slidePrev());
2254      });
2255  });
2256}
2257
2258function initBlogPostSlider() {
2259  document.querySelectorAll('.blog-post__swiper')?.forEach((sliderEl) => {
2260    const swiper = new Swiper(sliderEl, {
2261      slidesPerView: 1,
2262      spaceBetween: 0,
2263      speed: 600,
2264      effect: 'slide',
2265      autoHeight: true,
2266      loop: true,
2267      pagination: {
2268        el: sliderEl.parentElement.querySelector('.blog-post__swiper-pagination'),
2269        clickable: true,
2270      },
2271    });
2272
2273    sliderEl.querySelectorAll('img').forEach((img) => {
2274      if (img.complete) return;
2275      img.addEventListener('load', () => swiper.updateAutoHeight(0), { once: true });
2276    });
2277  });
2278}
2279
2280function initResultSlider() {
2281  const preloadSlideImages = (slide) => {
2282    if (!slide) return;
2283
2284    slide.querySelectorAll('img').forEach((img) => {
2285      img.loading = 'eager';
2286
2287      if (img.dataset.src && img.src !== img.dataset.src) {
2288        img.src = img.dataset.src;
2289      }
2290
2291      const src = img.currentSrc || img.src;
2292      if (src) {
2293        const preloader = new Image();
2294        preloader.src = src;
2295      }
2296    });
2297  };
2298
2299  const preloadAdjacentSlides = (swiper) => {
2300    preloadSlideImages(swiper.slides[swiper.activeIndex]);
2301    preloadSlideImages(swiper.slides[swiper.activeIndex + 1]);
2302    preloadSlideImages(swiper.slides[swiper.activeIndex - 1]);
2303  };
2304
2305  const resultSlider = new Swiper('.result__slider', {
2306    slidesPerView: 1,
2307    spaceBetween: 10,
2308    speed: 600,
2309    loop: true,
2310    allowTouchMove: false,
2311    pagination: {
2312      el: '.result__pagination',
2313      clickable: true,
2314      type: 'bullets',
2315    },
2316    on: {
2317      init(swiper) {
2318        preloadAdjacentSlides(swiper);
2319      },
2320      slideChangeTransitionStart(swiper) {
2321        preloadAdjacentSlides(swiper);
2322      },
2323    },
2324  });
2325
2326  document.querySelectorAll('.result__nav--next')?.forEach((btn) => {
2327    btn.addEventListener('click', () => resultSlider.slideNext());
2328  });
2329
2330  document.querySelectorAll('.result__nav--prev')?.forEach((btn) => {
2331    btn.addEventListener('click', () => resultSlider.slidePrev());
2332  });
2333}
2334
2335function initResultSlider2() {
2336  const resultSlider = new Swiper('.result__slider2', {
2337    slidesPerView: 1,
2338    spaceBetween: 10,
2339    speed: 600,
2340    loop: true,
2341    allowTouchMove: true,
2342    pagination: {
2343      el: '.result__pagination',
2344      clickable: true,
2345      type: 'bullets',
2346    },
2347
2348    breakpoints: {
2349      0: {
2350        direction: 'horizontal',
2351      },
2352      577: {
2353        direction: 'vertical',
2354      },
2355    },
2356  });
2357
2358  document.querySelectorAll('.result2__nav--next')?.forEach((btn) => {
2359    btn.addEventListener('click', () => resultSlider.slideNext());
2360  });
2361
2362  document.querySelectorAll('.result2__nav--prev')?.forEach((btn) => {
2363    btn.addEventListener('click', () => resultSlider.slidePrev());
2364  });
2365
2366  const element = document.querySelector('.result__slider2');
2367  if (element) {
2368    element.classList.add('result__slider');
2369  }
2370}
2371
2372function initProcessSlider() {
2373  const wrapper = document.querySelector('.process');
2374
2375  if (!wrapper) return;
2376
2377  const nextBtn = wrapper.querySelectorAll('.process__next, .process__nav--next');
2378  const prevBtn = wrapper.querySelectorAll('.process__nav--prev');
2379  const paginationEl = wrapper.querySelector('.process__pagination');
2380
2381  const processSlider = new Swiper('.process__slider', {
2382    slidesPerView: 'auto',
2383    spaceBetween: 70,
2384    speed: 600,
2385    loop: true,
2386    allowTouchMove: false,
2387    pagination: {
2388      el: paginationEl,
2389      clickable: true,
2390    },
2391  });
2392
2393  nextBtn?.forEach((btn) => {
2394    btn.addEventListener('click', () => processSlider.slideNext());
2395  });
2396
2397  prevBtn?.forEach((btn) => {
2398    btn.addEventListener('click', () => processSlider.slidePrev());
2399  });
2400
2401  const resizeHandler = () => {
2402    if (window.innerWidth < 768) {
2403      paginationEl.classList.replace('slider-pagination-line', 'slider-pagination');
2404    } else {
2405      paginationEl.classList.replace('slider-pagination', 'slider-pagination-line');
2406    }
2407  };
2408
2409  resizeHandler();
2410
2411  let resizeTimer;
2412  window.addEventListener('resize', () => {
2413    clearTimeout(resizeTimer);
2414    resizeTimer = setTimeout(resizeHandler, 100);
2415  });
2416
2417  wrapper.querySelectorAll('.process__gallery-wrapper').forEach((galleryWrapper) => {
2418    const slider = galleryWrapper.querySelector('.process__gallery-slider');
2419    const slidesCount = slider.querySelectorAll('.swiper-slide').length;
2420
2421    if (slidesCount <= 1) {
2422      galleryWrapper.classList.add('is-single');
2423    }
2424
2425    new Swiper(slider, {
2426      slidesPerView: 'auto',
2427      spaceBetween: 10,
2428      speed: 600,
2429      loop: slidesCount > 1,
2430      pagination: {
2431        el: galleryWrapper.querySelector('.process__gallery-pagination'),
2432        clickable: true,
2433      },
2434      navigation: {
2435        nextEl: galleryWrapper.querySelector('.process__gallery-nav--next'),
2436        prevEl: galleryWrapper.querySelector('.process__gallery-nav--prev'),
2437      },
2438    });
2439  });
2440}
2441
2442function initGallerySlider() {
2443  document.querySelectorAll('.gallery-slider').forEach((sliderEl) => {
2444    const wrapper = sliderEl.closest('.zone-slider') || sliderEl.parentElement;
2445
2446    const nextBtn = wrapper.querySelectorAll('.gallery-slider__nav--next');
2447    const prevBtn = wrapper.querySelectorAll('.gallery-slider__nav--prev');
2448    const pagination = sliderEl.querySelector('.gallery-slider__pagination');
2449
2450    const swiper = new Swiper(sliderEl, {
2451      centeredSlides: true,
2452      slidesPerView: 'auto',
2453      spaceBetween: 0,
2454      loop: true,
2455      speed: 600,
2456
2457      pagination: {
2458        el: pagination,
2459        clickable: true,
2460      },
2461
2462      breakpoints: {
2463        0: {
2464          slidesPerView: 1,
2465        },
2466        768: {
2467          slidesPerView: 'auto',
2468        },
2469      },
2470    });
2471
2472    nextBtn?.forEach((btn) => {
2473      btn.addEventListener('click', () => swiper.slideNext());
2474    });
2475
2476    prevBtn?.forEach((btn) => {
2477      btn.addEventListener('click', () => swiper.slidePrev());
2478    });
2479  });
2480}
2481
2482function initSingleSlider() {
2483  document.querySelectorAll('.single-slider').forEach((sliderEl, i) => {
2484    let wrapper = sliderEl.closest('.single-slider-outer') || sliderEl.parentElement;
2485
2486    if (sliderEl.hasAttribute('data-container-class')) {
2487      wrapper = sliderEl.closest(sliderEl.getAttribute('data-container-class'));
2488    }
2489
2490    const nextBtn = wrapper.querySelectorAll(
2491      '.single-slider__nav--next:not(.inc), .slide-next:not(.inc)'
2492    );
2493    const prevBtn = wrapper.querySelectorAll('.single-slider__nav--prev:not(.inc)');
2494    const pagination = wrapper.querySelector('.single-slider__pagination:not(.inc)');
2495
2496    const swiper = new Swiper(sliderEl, {
2497      centeredSlides: true,
2498      slidesPerView: 'auto',
2499      spaceBetween: 0,
2500      loop: false,
2501      speed: 600,
2502      pagination: {
2503        el: pagination,
2504        clickable: true,
2505      },
2506
2507      on: {
2508        init() {
2509          toggleLockedClass(this, wrapper);
2510        },
2511        resize() {
2512          toggleLockedClass(this, wrapper);
2513        },
2514        slideChange() {
2515          syncButtons(this);
2516          syncBadge(this);
2517        },
2518      },
2519    });
2520
2521    nextBtn?.forEach((btn) => {
2522      btn.addEventListener('click', () => {
2523        swiper.slideNext();
2524      });
2525    });
2526
2527    prevBtn?.forEach((btn) => {
2528      btn.addEventListener('click', () => {
2529        swiper.slidePrev();
2530      });
2531    });
2532  });
2533}
2534
2535function initMaterialsSlider() {
2536  document.querySelectorAll('.materials__slider').forEach((sliderEl, i) => {
2537    const wrapper = sliderEl.closest('.materials');
2538
2539    const nextBtn = wrapper.querySelectorAll('.materials__nav--next, .slide-next');
2540    const prevBtn = wrapper.querySelectorAll('.materials__nav--prev');
2541    const pagination = wrapper.querySelector('.materials__pagination');
2542
2543    const swiper = new Swiper(sliderEl, {
2544      slidesPerView: 'auto',
2545      spaceBetween: 50,
2546      loop: true,
2547      speed: 600,
2548      pagination: {
2549        el: pagination,
2550        clickable: true,
2551      },
2552      on: {
2553        slideChange() {
2554          syncButtons(this);
2555        },
2556      },
2557    });
2558
2559    nextBtn?.forEach((btn) => {
2560      btn.addEventListener('click', () => swiper.slideNext());
2561    });
2562
2563    prevBtn?.forEach((btn) => {
2564      btn.addEventListener('click', () => swiper.slidePrev());
2565    });
2566  });
2567}
2568
2569function syncBadge(swiper) {
2570  const activeIndex = swiper.realIndex + 1;
2571
2572  const step = swiper.el.closest('.section');
2573  if (!step) return;
2574
2575  const badgeSpans = step.querySelectorAll('.section__badge span');
2576  if (badgeSpans) {
2577    badgeSpans.forEach((badgeSpan) => {
2578      badgeSpan.textContent = activeIndex;
2579    });
2580  }
2581}
2582
2583function syncButtons(swiper) {
2584  const sliderClass = swiper.el.dataset.sliderName;
2585  const scope = swiper.el.closest('.swiper-slide') || swiper.el.closest('.section');
2586  const activeIndex = swiper.realIndex;
2587
2588  scope.querySelectorAll(`.slide-to-button[data-slider="${sliderClass}"]`).forEach((btn) => {
2589    btn.classList.toggle('active', +btn.dataset.index - 1 === activeIndex);
2590  });
2591}
2592
2593function toggleLockedClass(swiper, wrapper) {
2594  wrapper.classList.toggle('is-locked', swiper.isLocked);
2595}
2596
2597function updateCasesNavWidth() {
2598  const activeSlide = document.querySelector('.cases__slide.swiper-slide-active');
2599  const nav = document.querySelector('.cases__slider-nav');
2600
2601  if (activeSlide && nav) {
2602    const navWidth = activeSlide.offsetWidth + 100;
2603    nav.style.width = navWidth + 'px';
2604  }
2605}
2606
2607function initLazyVideo(selector = '.lazy-video') {
2608  document.querySelectorAll(selector)?.forEach((item) => {
2609    const video = item.querySelector('.lazy-video__video');
2610
2611    if (video) {
2612      video.addEventListener(
2613        'playing',
2614        () => {
2615          requestAnimationFrame(() => {
2616            item.classList.add('lazy-video--loaded');
2617          });
2618        },
2619        { once: true }
2620      );
2621
2622      video.play().catch((err) => console.log('Autoplay blocked:', err));
2623    }
2624  });
2625}
2626
2627function initBeforeAfterSlider(slider) {
2628  const beforeItem = slider.querySelector('.before-after__item--before');
2629  const afterItem = slider.querySelector('.before-after__item--after');
2630  const handle = slider.querySelector('.before-after__handle');
2631  const drag = slider.querySelector('.before-after__drag');
2632
2633  if (!beforeItem || !afterItem || !handle || !drag) return;
2634
2635  const beforeOverlay = beforeItem.querySelector('.before-after__overlay');
2636  const afterOverlay = afterItem.querySelector('.before-after__overlay');
2637
2638  let isDragging = false;
2639  let currentPosition = 50;
2640
2641  if (!drag.hasAttribute('tabindex')) {
2642    drag.setAttribute('tabindex', '0');
2643  }
2644  drag.style.pointerEvents = 'all';
2645
2646  updatePosition();
2647
2648  drag.addEventListener('mousedown', startDrag);
2649  drag.addEventListener('touchstart', startDrag);
2650  drag.addEventListener('keydown', handleKeyboard);
2651
2652  function handleKeyboard(e) {
2653    const step = e.shiftKey ? 10 : 1;
2654
2655    switch (e.key) {
2656      case 'ArrowLeft':
2657      case 'Left':
2658        e.preventDefault();
2659        currentPosition = Math.max(0, currentPosition - step);
2660        updatePosition();
2661        break;
2662      case 'ArrowRight':
2663      case 'Right':
2664        e.preventDefault();
2665        currentPosition = Math.min(100, currentPosition + step);
2666        updatePosition();
2667        break;
2668      case 'Home':
2669        e.preventDefault();
2670        currentPosition = 0;
2671        updatePosition();
2672        break;
2673      case 'End':
2674        e.preventDefault();
2675        currentPosition = 100;
2676        updatePosition();
2677        break;
2678    }
2679  }
2680
2681  function startDrag(e) {
2682    isDragging = true;
2683    e.preventDefault();
2684    document.addEventListener('mousemove', move);
2685    document.addEventListener('mouseup', stopDrag);
2686    document.addEventListener('touchmove', move);
2687    document.addEventListener('touchend', stopDrag);
2688  }
2689
2690  function move(e) {
2691    if (!isDragging) return;
2692    e.preventDefault();
2693
2694    const rect = slider.getBoundingClientRect();
2695    const clientX = e.type.includes('touch') ? e.touches[0].clientX : e.clientX;
2696    const x = Math.max(0, Math.min(clientX - rect.left, rect.width));
2697
2698    currentPosition = (x / rect.width) * 100;
2699    updatePosition();
2700  }
2701
2702  function stopDrag() {
2703    isDragging = false;
2704    document.removeEventListener('mousemove', move);
2705    document.removeEventListener('mouseup', stopDrag);
2706    document.removeEventListener('touchmove', move);
2707    document.removeEventListener('touchend', stopDrag);
2708  }
2709
2710  function updatePosition() {
2711    beforeItem.style.clipPath = `inset(0% ${100 - currentPosition}% 0% 0%)`;
2712    handle.style.left = `${currentPosition}%`;
2713    updateOverlays();
2714  }
2715
2716  function updateOverlays() {
2717    const factor = currentPosition / 100;
2718    const afterOpacity = factor < 0.5 ? factor * 2 : 1;
2719    const beforeOpacity = factor >= 0.5 ? (1 - factor) * 2 : 1;
2720
2721    if (afterOverlay) afterOverlay.style.opacity = afterOpacity;
2722    if (beforeOverlay) beforeOverlay.style.opacity = beforeOpacity;
2723  }
2724}
2725
2726function initHeaderNav() {
2727  const header = document.getElementById('header');
2728  if (!header) return;
2729
2730  const MOBILE_BREAKPOINT = 1201;
2731  const SUBMENU_DURATION = 350;
2732  let wasMobile = window.innerWidth < MOBILE_BREAKPOINT;
2733  let isAnimating = false;
2734  let submenuAnimating = false;
2735
2736  const burgers = header.querySelectorAll('.header__burger');
2737  const submenuToggles = header.querySelectorAll('.header__nav-submenu-toggle');
2738
2739  if (!burgers.length) return;
2740
2741  syncSubmenuStructure();
2742
2743  burgers.forEach((burger) => {
2744    burger.addEventListener('click', () => {
2745      if (isAnimating) return;
2746
2747      isAnimating = true;
2748      const willOpen = !header.classList.contains('header--nav-active');
2749
2750      burgers.forEach((b) => b.classList.toggle('burger--active', willOpen));
2751      if (isMobileViewport()) {
2752        document.body.classList.toggle('overflow-hidden', willOpen);
2753      } else {
2754        document.body.classList.remove('overflow-hidden');
2755      }
2756      header.classList.toggle('header--nav-active', willOpen);
2757
2758      if (!willOpen) {
2759        resetSubmenuStyles();
2760      }
2761
2762      setTimeout(() => {
2763        isAnimating = false;
2764      }, 300);
2765    });
2766  });
2767
2768  // Десктоп: нижнее меню открыто
2769  if (!isMobileViewport()) {
2770    header.classList.add('header--nav-active');
2771    burgers.forEach((b) => b.classList.add('burger--active'));
2772  }
2773
2774  submenuToggles.forEach((toggle) => {
2775    toggle.addEventListener('click', (e) => {
2776      e.preventDefault();
2777      e.stopPropagation();
2778      if (!isMobileViewport()) return;
2779      toggleSubmenu(toggle);
2780    });
2781  });
2782
2783  header
2784    .querySelectorAll('.header__nav-item--has-children > .header__nav-item-inner')
2785    .forEach((inner) => {
2786      inner.addEventListener('click', (e) => {
2787        if (!isMobileViewport()) return;
2788        const toggle = inner.querySelector('.header__nav-submenu-toggle');
2789        if (!toggle) return;
2790        e.preventDefault();
2791        e.stopPropagation();
2792        toggleSubmenu(toggle);
2793      });
2794    });
2795
2796  document.addEventListener('click', (e) => {
2797    if (!isMobileViewport()) return;
2798    if (header.classList.contains('header--nav-active') && !header.contains(e.target)) {
2799      closeMobileMenu();
2800    }
2801  });
2802
2803  let resizeTimer;
2804  window.addEventListener('resize', () => {
2805    clearTimeout(resizeTimer);
2806    resizeTimer = setTimeout(() => {
2807      handleBreakpointChange();
2808    }, 250);
2809  });
2810
2811  window.addEventListener('orientationchange', () => {
2812    setTimeout(() => {
2813      handleBreakpointChange();
2814    }, 300);
2815  });
2816
2817  function isMobileViewport() {
2818    return window.innerWidth < MOBILE_BREAKPOINT;
2819  }
2820
2821  function syncSubmenuStructure() {
2822    if (isMobileViewport()) {
2823      prepareSubmenuInners();
2824    } else {
2825      unwrapSubmenuInners();
2826    }
2827  }
2828
2829  function prepareSubmenuInners() {
2830    header.querySelectorAll('.header__nav-submenu').forEach((submenu) => {
2831      if (submenu.dataset.accordionReady) return;
2832      const inner = document.createElement('div');
2833      inner.className = 'header__nav-submenu-inner';
2834      while (submenu.firstChild) {
2835        inner.appendChild(submenu.firstChild);
2836      }
2837      submenu.appendChild(inner);
2838      submenu.dataset.accordionReady = '1';
2839    });
2840  }
2841
2842  function unwrapSubmenuInners() {
2843    header.querySelectorAll('.header__nav-submenu').forEach((submenu) => {
2844      const inner = submenu.querySelector(':scope > .header__nav-submenu-inner');
2845      if (inner) {
2846        while (inner.firstChild) {
2847          submenu.appendChild(inner.firstChild);
2848        }
2849        inner.remove();
2850      }
2851      delete submenu.dataset.accordionReady;
2852      submenu.style.maxHeight = '';
2853      submenu.style.transition = '';
2854      submenu.style.display = '';
2855    });
2856  }
2857
2858  function handleBreakpointChange() {
2859    const isMobile = isMobileViewport();
2860    if (wasMobile === isMobile) return;
2861
2862    resetSubmenuStyles();
2863    syncSubmenuStructure();
2864
2865    if (isMobile) {
2866      header.classList.remove('header--nav-active');
2867      burgers.forEach((b) => b.classList.remove('burger--active'));
2868      document.body.classList.remove('overflow-hidden');
2869    } else {
2870      header.classList.add('header--nav-active');
2871      burgers.forEach((b) => b.classList.add('burger--active'));
2872      document.body.classList.remove('overflow-hidden');
2873    }
2874
2875    wasMobile = isMobile;
2876  }
2877
2878  function toggleSubmenu(toggle) {
2879    if (submenuAnimating || !isMobileViewport()) return;
2880
2881    const parentItem = toggle.closest('.header__nav-item--has-children');
2882    if (!parentItem?.querySelector('.header__nav-submenu')) return;
2883
2884    const isOpen = parentItem.classList.contains('header__nav-item--submenu-open');
2885    submenuAnimating = true;
2886
2887    const animations = [];
2888    header.querySelectorAll('.header__nav-item--submenu-open').forEach((item) => {
2889      if (item === parentItem) return;
2890      animations.push(closeSubmenu(item));
2891    });
2892
2893    if (isOpen) {
2894      animations.push(closeSubmenu(parentItem));
2895      Promise.all(animations).finally(() => {
2896        submenuAnimating = false;
2897      });
2898      return;
2899    }
2900
2901    animations.push(openSubmenu(parentItem));
2902    Promise.all(animations)
2903      .then(() => scrollTitleIntoView(parentItem))
2904      .finally(() => {
2905        submenuAnimating = false;
2906      });
2907  }
2908
2909  function openSubmenu(item) {
2910    const submenu = item.querySelector('.header__nav-submenu');
2911    const toggle = item.querySelector('.header__nav-submenu-toggle');
2912    if (!submenu) return Promise.resolve();
2913
2914    item.classList.add('header__nav-item--submenu-open');
2915    toggle?.classList.add('header__nav-submenu-toggle--active');
2916
2917    submenu.style.transition = 'none';
2918    submenu.style.maxHeight = 'none';
2919    const targetHeight = submenu.scrollHeight;
2920    submenu.style.maxHeight = '0px';
2921    void submenu.offsetHeight;
2922
2923    return new Promise((resolve) => {
2924      requestAnimationFrame(() => {
2925        submenu.style.transition = '';
2926        submenu.style.maxHeight = `${targetHeight}px`;
2927        waitForTransition(submenu).then(() => {
2928          if (item.classList.contains('header__nav-item--submenu-open')) {
2929            submenu.style.maxHeight = 'none';
2930          }
2931          resolve();
2932        });
2933      });
2934    });
2935  }
2936
2937  function closeSubmenu(item) {
2938    const submenu = item.querySelector('.header__nav-submenu');
2939    const toggle = item.querySelector('.header__nav-submenu-toggle');
2940    if (!submenu) return Promise.resolve();
2941
2942    submenu.style.transition = 'none';
2943    submenu.style.maxHeight = `${submenu.scrollHeight}px`;
2944    void submenu.offsetHeight;
2945
2946    return new Promise((resolve) => {
2947      requestAnimationFrame(() => {
2948        item.classList.remove('header__nav-item--submenu-open');
2949        toggle?.classList.remove('header__nav-submenu-toggle--active');
2950        submenu.style.transition = '';
2951        submenu.style.maxHeight = '0px';
2952        waitForTransition(submenu).then(resolve);
2953      });
2954    });
2955  }
2956
2957  function waitForTransition(el) {
2958    return new Promise((resolve) => {
2959      let done = false;
2960      const finish = () => {
2961        if (done) return;
2962        done = true;
2963        el.removeEventListener('transitionend', onEnd);
2964        resolve();
2965      };
2966      const onEnd = (e) => {
2967        if (e.target !== el || e.propertyName !== 'max-height') return;
2968        finish();
2969      };
2970      el.addEventListener('transitionend', onEnd);
2971      setTimeout(finish, SUBMENU_DURATION + 50);
2972    });
2973  }
2974
2975  function getScrollParent(el) {
2976    let parent = el.parentElement;
2977    while (parent && parent !== document.body) {
2978      const { overflowY } = getComputedStyle(parent);
2979      if (
2980        (overflowY === 'auto' || overflowY === 'scroll') &&
2981        parent.scrollHeight > parent.clientHeight
2982      ) {
2983        return parent;
2984      }
2985      parent = parent.parentElement;
2986    }
2987    return document.scrollingElement || document.documentElement;
2988  }
2989
2990  function scrollTitleIntoView(item) {
2991    const title = item.querySelector('.header__nav-item-inner') || item;
2992    const rect = title.getBoundingClientRect();
2993    const viewportTop = 0;
2994    const viewportBottom = window.innerHeight;
2995
2996    if (rect.top >= viewportTop && rect.bottom <= viewportBottom) {
2997      return Promise.resolve();
2998    }
2999
3000    const scrollParent = getScrollParent(title);
3001    const parentRect = scrollParent.getBoundingClientRect();
3002    const padding = 12;
3003    let nextTop = scrollParent.scrollTop;
3004
3005    if (rect.top < Math.max(viewportTop, parentRect.top) + padding) {
3006      nextTop += rect.top - Math.max(viewportTop, parentRect.top) - padding;
3007    } else if (rect.bottom > Math.min(viewportBottom, parentRect.bottom) - padding) {
3008      nextTop += rect.bottom - Math.min(viewportBottom, parentRect.bottom) + padding;
3009    }
3010
3011    return animateScrollLinear(scrollParent, nextTop, SUBMENU_DURATION);
3012  }
3013
3014  function animateScrollLinear(el, to, duration) {
3015    const start = el.scrollTop;
3016    const change = to - start;
3017    if (Math.abs(change) < 1) return Promise.resolve();
3018
3019    const startTime = performance.now();
3020    return new Promise((resolve) => {
3021      const step = (now) => {
3022        const progress = Math.min(1, (now - startTime) / duration);
3023        el.scrollTop = start + change * progress;
3024        if (progress < 1) {
3025          requestAnimationFrame(step);
3026        } else {
3027          resolve();
3028        }
3029      };
3030      requestAnimationFrame(step);
3031    });
3032  }
3033
3034  function resetSubmenuStyles() {
3035    header.querySelectorAll('.header__nav-submenu').forEach((submenu) => {
3036      submenu.style.maxHeight = '';
3037      submenu.style.display = '';
3038      submenu.style.transition = '';
3039    });
3040
3041    header.querySelectorAll('.header__nav-item--submenu-open').forEach((item) => {
3042      item.classList.remove('header__nav-item--submenu-open');
3043    });
3044
3045    header.querySelectorAll('.header__nav-submenu-toggle').forEach((toggle) => {
3046      toggle.classList.remove('header__nav-submenu-toggle--active');
3047    });
3048
3049    submenuAnimating = false;
3050  }
3051
3052  function closeMobileMenu() {
3053    burgers.forEach((b) => b.classList.remove('burger--active'));
3054    header.classList.remove('header--nav-active');
3055    document.body.classList.remove('overflow-hidden');
3056    resetSubmenuStyles();
3057  }
3058}
3059
3060function initFileUpload(selector = '.form__file') {
3061  document.querySelectorAll(selector).forEach((wrapper) => {
3062    if (wrapper.dataset.vpFileInited) return; // guard against re-init after AJAX modal load
3063    wrapper.dataset.vpFileInited = '1';
3064    const input = wrapper.querySelector('.form__file-input');
3065    const textEl = wrapper.querySelector('.form__file-text');
3066    const removeBtn = wrapper.querySelector('.form__file-remove');
3067
3068    if (!input) return;
3069
3070    const defaultText = wrapper.dataset.text || textEl.textContent;
3071
3072    input.addEventListener('change', () => {
3073      if (input.files.length > 0) {
3074        const file = input.files[0];
3075        textEl.textContent = shortenFileName(file.name);
3076        wrapper.classList.add('form__file--uploaded');
3077      } else {
3078        resetFile();
3079      }
3080    });
3081
3082    removeBtn?.addEventListener('click', resetFile);
3083
3084    function resetFile() {
3085      input.value = '';
3086      textEl.textContent = defaultText;
3087      wrapper.classList.remove('form__file--uploaded');
3088      removeBtn.hidden = true;
3089    }
3090  });
3091}
3092
3093function shortenFileName(filename, maxLength = 20) {
3094  if (filename.length <= maxLength) return filename;
3095
3096  const parts = filename.split('.');
3097  const ext = parts.pop();
3098  const name = parts.join('.');
3099  const charsToShow = Math.floor((maxLength - ext.length - 3) / 2);
3100
3101  return `${name.substring(0, charsToShow)}...${name.substring(name.length - charsToShow)}.${ext}`;
3102}
3103
3104function faqInit() {
3105  document.querySelectorAll('.faq-item-toggle').forEach((toggle) => {
3106    toggle.addEventListener('click', (e) => {
3107      e.preventDefault();
3108
3109      const container = toggle.closest('.faq');
3110      const parent = toggle.closest('.faq-item');
3111      const content = parent.querySelector('.faq-item-content');
3112
3113      container.querySelectorAll('.faq-item--active').forEach((item) => {
3114        if (item !== parent) {
3115          item.classList.remove('faq-item--active');
3116          item.querySelector('.faq-item-content')?.slideToggle(300, 'flex');
3117        }
3118      });
3119
3120      parent.classList.toggle('faq-item--active');
3121      content?.slideToggle(300, 'flex');
3122    });
3123  });
3124}
3125
3126function newShowcaseAccordionInit() {
3127  document.querySelectorAll('.new-showcase__item-head').forEach((head) => {
3128    head.addEventListener('click', () => {
3129      const item = head.closest('.new-showcase__item');
3130      const items = item?.closest('.new-showcase__items');
3131      if (!item || !items) return;
3132
3133      items.querySelectorAll('.new-showcase__item.is-open').forEach((openItem) => {
3134        if (openItem !== item) {
3135          openItem.classList.remove('is-open');
3136          openItem
3137            .querySelector('.new-showcase__item-head')
3138            ?.setAttribute('aria-expanded', 'false');
3139        }
3140      });
3141
3142      const isOpen = item.classList.toggle('is-open');
3143      head.setAttribute('aria-expanded', String(isOpen));
3144    });
3145  });
3146}
3147
3148const VP_CASE_VOLUME = 0.5;
3149
3150function vpVideoAudioContext() {
3151  const Ctx = window.AudioContext || window.webkitAudioContext;
3152  if (!Ctx) return null;
3153  if (!vpVideoAudioContext.ctx) {
3154    vpVideoAudioContext.ctx = new Ctx();
3155  }
3156  return vpVideoAudioContext.ctx;
3157}
3158
3159function vpVideoLevel(video) {
3160  const slider = video.closest('[data-video-card]')?.querySelector('[data-video-volume-range]');
3161  const value = slider ? Number(slider.value) : VP_CASE_VOLUME;
3162  if (!Number.isFinite(value)) return VP_CASE_VOLUME;
3163  return Math.max(0, Math.min(1, value));
3164}
3165
3166function vpApplyVideoLevel(video, level) {
3167  const next = Math.max(0, Math.min(1, level));
3168  const ctx = vpVideoAudioContext();
3169  if (ctx) {
3170    try {
3171      if (!video._vpGain) {
3172        const source = ctx.createMediaElementSource(video);
3173        const gain = ctx.createGain();
3174        source.connect(gain).connect(ctx.destination);
3175        video._vpGain = gain;
3176      }
3177      video._vpGain.gain.value = next;
3178      if (ctx.state === 'suspended') {
3179        ctx.resume().catch(() => {});
3180      }
3181      return;
3182    } catch (e) {}
3183  }
3184  try {
3185    video.volume = next;
3186  } catch (e) {}
3187}
3188
3189function vpSyncVolumeIcon(card, video) {
3190  const icon = card.querySelector('[data-video-volume] use');
3191  if (!icon) return;
3192  const href = icon.getAttribute('href') || icon.getAttribute('xlink:href') || '';
3193  const base = href.split('#')[0] || '';
3194  const hash = video.muted ? 'volume-muted' : 'volume';
3195  icon.setAttribute('href', base + '#' + hash);
3196  if (icon.hasAttribute('xlink:href')) {
3197    icon.setAttribute('xlink:href', base + '#' + hash);
3198  }
3199}
3200
3201function initVideoCards() {
3202  if (initVideoCards.bound) return;
3203  initVideoCards.bound = true;
3204
3205  document.addEventListener(
3206    'input',
3207    (e) => {
3208      const range = e.target.closest('[data-video-volume-range]');
3209      if (!range) return;
3210      const card = range.closest('[data-video-card]');
3211      const video = card && card.querySelector('[data-video]');
3212      if (!video) return;
3213      const level = Number(range.value);
3214      const next = Number.isFinite(level) ? level : VP_CASE_VOLUME;
3215      vpApplyVideoLevel(video, next);
3216      video.muted = next <= 0;
3217      card.classList.toggle('is-muted', video.muted);
3218      vpSyncVolumeIcon(card, video);
3219    },
3220    true
3221  );
3222
3223  document.addEventListener(
3224    'click',
3225    (e) => {
3226      if (e.target.closest('[data-video-volume-range]')) {
3227        e.stopPropagation();
3228        return;
3229      }
3230
3231      const volumeBtn = e.target.closest('[data-video-volume]');
3232      if (volumeBtn) {
3233        const card = volumeBtn.closest('[data-video-card]');
3234        const video = card && card.querySelector('[data-video]');
3235        if (!video) return;
3236        e.preventDefault();
3237        e.stopPropagation();
3238        video.muted = !video.muted;
3239        if (!video.muted) {
3240          const range = card.querySelector('[data-video-volume-range]');
3241          if (range && Number(range.value) <= 0) {
3242            range.value = String(VP_CASE_VOLUME);
3243          }
3244          vpApplyVideoLevel(video, vpVideoLevel(video));
3245        }
3246        card.classList.toggle('is-muted', video.muted);
3247        vpSyncVolumeIcon(card, video);
3248        return;
3249      }
3250
3251      const playBtn = e.target.closest('[data-video-play]');
3252      const card =
3253        (playBtn && playBtn.closest('[data-video-card]')) ||
3254        e.target.closest('[data-video-card].is-playing');
3255      if (!card) return;
3256
3257      const video = card.querySelector('[data-video]');
3258      if (!video) return;
3259
3260      e.preventDefault();
3261      e.stopPropagation();
3262      if (video.paused) {
3263        const level = vpVideoLevel(video);
3264        video.muted = level <= 0;
3265        card.classList.toggle('is-muted', video.muted);
3266        vpApplyVideoLevel(video, level);
3267        vpSyncVolumeIcon(card, video);
3268        const playPromise = video.play();
3269        if (playPromise && typeof playPromise.catch === 'function') {
3270          playPromise.catch(() => {});
3271        }
3272        card.classList.add('is-playing');
3273      } else {
3274        video.pause();
3275        card.classList.remove('is-playing');
3276      }
3277    },
3278    true
3279  );
3280}
3281
3282function initPortfolioBreadcrumbsOffset() {
3283  const header = document.getElementById('header');
3284  const hero = document.querySelector('.hero-new-portfolio--detail');
3285  if (!header || !hero) return;
3286
3287  const mediaQuery1024 = window.matchMedia('(max-width: 1024px)');
3288  const mediaQuery767 = window.matchMedia('(max-width: 767px)');
3289
3290  const setOffset = () => {
3291    const gap = mediaQuery767.matches ? 18 : mediaQuery1024.matches ? 44 : 30;
3292    hero.style.setProperty('--hero-breadcrumbs-top', `${header.offsetHeight + gap}px`);
3293  };
3294
3295  setOffset();
3296  window.addEventListener('resize', setOffset);
3297  mediaQuery1024.addEventListener('change', setOffset);
3298  mediaQuery767.addEventListener('change', setOffset);
3299
3300  if (typeof ResizeObserver !== 'undefined') {
3301    const observer = new ResizeObserver(setOffset);
3302    observer.observe(header);
3303  }
3304}
3305
3306function initBlogPostOffset() {
3307  const header = document.getElementById('header');
3308  const post = document.querySelector('.blog-post');
3309  if (!header || !post) return;
3310
3311  const setOffset = () => {
3312    post.style.setProperty('--blog-post-header-height', `${header.offsetHeight}px`);
3313  };
3314
3315  setOffset();
3316  window.addEventListener('resize', setOffset);
3317
3318  if (typeof ResizeObserver !== 'undefined') {
3319    const observer = new ResizeObserver(setOffset);
3320    observer.observe(header);
3321  }
3322}
3323
3324function initBlogPostNav() {
3325  const post = document.querySelector('.blog-post');
3326  if (!post) return;
3327
3328  const leftCol =
3329    post.querySelector('.blog-post__sidebar > .blog-post__col:first-child') ||
3330    post.querySelector('.blog-post__row > .blog-post__col');
3331  const content = post.querySelector('.how-apply__footer-text');
3332  if (!leftCol || !content) return;
3333
3334  const headings = Array.from(content.querySelectorAll('h2'));
3335  if (!headings.length) return;
3336
3337  const usedIds = new Set();
3338
3339  const escapeHtml = (value) =>
3340    value
3341      .replace(/&/g, '&amp;')
3342      .replace(/</g, '&lt;')
3343      .replace(/>/g, '&gt;')
3344      .replace(/"/g, '&quot;');
3345
3346  const slugify = (text, index) => {
3347    const base = text
3348      .toLowerCase()
3349      .replace(/ё/g, 'е')
3350      .replace(/[^a-zа-я0-9]+/gi, '-')
3351      .replace(/^-+|-+$/g, '')
3352      .slice(0, 60);
3353    let id = base || `blog-post-section-${String(index).padStart(2, '0')}`;
3354    if (usedIds.has(id) || document.getElementById(id)) {
3355      id = `${id}-${String(index).padStart(2, '0')}`;
3356    }
3357    usedIds.add(id);
3358    return id;
3359  };
3360
3361  const items = headings.map((heading, index) => {
3362    const title = heading.textContent.replace(/\s+/g, ' ').trim();
3363    if (!heading.id) {
3364      heading.id = slugify(title, index + 1);
3365    } else {
3366      usedIds.add(heading.id);
3367    }
3368
3369    const num = String(index + 1).padStart(2, '0');
3370    return { id: heading.id, title, num };
3371  });
3372
3373  const nav = document.createElement('nav');
3374  nav.className = 'blog-post__toc';
3375  nav.setAttribute('aria-label', 'Содержание');
3376  nav.innerHTML = `
3377    <div class="blog-post__toc-head">
3378      <span class="blog-post__toc-line" aria-hidden="true"></span>
3379      <div class="blog-post__toc-title">Содержание</div>
3380    </div>
3381    <ol class="blog-post__toc-list list-reset">
3382      ${items
3383        .map(
3384          (item, index) => `
3385        <li class="blog-post__toc-item${index === 0 ? ' is-active' : ''}">
3386          <a class="blog-post__toc-link" href="#${item.id}">
3387            <span class="blog-post__toc-num">${item.num}</span>
3388            <span class="blog-post__toc-text">${escapeHtml(item.title)}</span>
3389          </a>
3390        </li>`
3391        )
3392        .join('')}
3393    </ol>
3394  `;
3395
3396  leftCol.appendChild(nav);
3397
3398  const tocItems = Array.from(nav.querySelectorAll('.blog-post__toc-item'));
3399
3400  const setActive = (index) => {
3401    tocItems.forEach((el, i) => {
3402      el.classList.toggle('is-active', i === index);
3403    });
3404  };
3405
3406  const getNavOffset = () => {
3407    const header = document.getElementById('header');
3408    return (header ? header.offsetHeight : 0) + 30;
3409  };
3410
3411  const updateActiveFromScroll = () => {
3412    const offset = getNavOffset() + 1;
3413    let activeIndex = 0;
3414
3415    for (let i = 0; i < headings.length; i++) {
3416      if (headings[i].getBoundingClientRect().top <= offset) {
3417        activeIndex = i;
3418      } else {
3419        break;
3420      }
3421    }
3422
3423    setActive(activeIndex);
3424  };
3425
3426  nav.addEventListener('click', (event) => {
3427    const link = event.target.closest('.blog-post__toc-link');
3428    if (!link) return;
3429    const item = link.closest('.blog-post__toc-item');
3430    if (!item) return;
3431    const index = tocItems.indexOf(item);
3432    if (index !== -1) setActive(index);
3433  });
3434
3435  window.addEventListener('scroll', updateActiveFromScroll, { passive: true });
3436  window.addEventListener('resize', updateActiveFromScroll);
3437  updateActiveFromScroll();
3438}
3439
3440function initPortfolioHeroMobileHeight() {
3441  const hero = document.querySelector('section.hero.hero-new-portfolio--detail');
3442  if (!hero) return;
3443
3444  const actions = document.querySelector('.new-portfolio__hero-panel-actions');
3445  const aboutSection = document.querySelector('.new-portfolio__section-about');
3446  const mediaQuery = window.matchMedia('(max-width: 767px)');
3447  let cachedActionsHeight = 0;
3448
3449  const reset = () => {
3450    hero.style.removeProperty('--hero-actions-height');
3451    hero.style.height = '';
3452    hero.style.minHeight = '';
3453  };
3454
3455  const cacheActionsHeight = () => {
3456    if (!actions || getComputedStyle(actions).display === 'none') return cachedActionsHeight;
3457
3458    const height = actions.offsetHeight;
3459    if (height > 0) cachedActionsHeight = height;
3460    return cachedActionsHeight;
3461  };
3462
3463  const measureHiddenActionsHeight = () => {
3464    if (!actions) return 0;
3465    if (cachedActionsHeight > 0) return cachedActionsHeight;
3466
3467    const prevDisplay = actions.style.display;
3468    const prevVisibility = actions.style.visibility;
3469    const prevPosition = actions.style.position;
3470    const prevPointerEvents = actions.style.pointerEvents;
3471
3472    actions.style.display = 'flex';
3473    actions.style.visibility = 'hidden';
3474    actions.style.position = 'absolute';
3475    actions.style.pointerEvents = 'none';
3476
3477    const height = actions.offsetHeight;
3478    if (height > 0) cachedActionsHeight = height;
3479
3480    actions.style.display = prevDisplay;
3481    actions.style.visibility = prevVisibility;
3482    actions.style.position = prevPosition;
3483    actions.style.pointerEvents = prevPointerEvents;
3484
3485    return cachedActionsHeight;
3486  };
3487
3488  const update = () => {
3489    // Strictly mobile: never touch hero height outside ≤767px
3490    if (!mediaQuery.matches || !actions) {
3491      reset();
3492      return;
3493    }
3494
3495    // At ≤1024 the duplicate block is hidden; real buttons live in the hero panel.
3496    if (getComputedStyle(actions).display === 'none') {
3497      reset();
3498      return;
3499    }
3500
3501    const aboutOpen =
3502      aboutSection?.classList.contains('is-about-open') || hero.classList.contains('is-about-open');
3503
3504    // When about opens, buttons are display:none — shrink hero by that block's height
3505    const actionsHeight = aboutOpen ? measureHiddenActionsHeight() : cacheActionsHeight();
3506
3507    hero.style.setProperty('--hero-actions-height', `${actionsHeight}px`);
3508  };
3509
3510  update();
3511  window.addEventListener('resize', update);
3512  mediaQuery.addEventListener('change', update);
3513
3514  if (typeof ResizeObserver !== 'undefined' && actions) {
3515    new ResizeObserver(() => {
3516      if (!mediaQuery.matches) return;
3517      if (
3518        aboutSection?.classList.contains('is-about-open') ||
3519        hero.classList.contains('is-about-open')
3520      ) {
3521        return;
3522      }
3523      cacheActionsHeight();
3524      update();
3525    }).observe(actions);
3526  }
3527
3528  const observeAboutClass = (target) => {
3529    if (!target || typeof MutationObserver === 'undefined') return;
3530    new MutationObserver(update).observe(target, {
3531      attributes: true,
3532      attributeFilter: ['class'],
3533    });
3534  };
3535
3536  observeAboutClass(aboutSection);
3537  observeAboutClass(hero);
3538
3539  // Expose for sync before display:none so height is cached while buttons are visible
3540  initPortfolioHeroMobileHeight.cacheBeforeOpen = () => {
3541    if (!mediaQuery.matches) return;
3542    cacheActionsHeight();
3543  };
3544  initPortfolioHeroMobileHeight.update = update;
3545}
3546
3547function initNewExperienceTextToggle() {
3548  const text = document.querySelector('.new-experience__text');
3549  const btn = text?.querySelector('.new-experience__more');
3550  const icon = btn?.querySelector('.new-experience__more-icon');
3551  if (!text || !btn) return;
3552
3553  const mediaQuery = window.matchMedia('(max-width: 1024px)');
3554
3555  const setExpanded = (open) => {
3556    text.classList.toggle('is-expanded', open);
3557    btn.setAttribute('aria-expanded', String(open));
3558    if (icon) icon.textContent = open ? '—' : '+';
3559  };
3560
3561  btn.addEventListener('click', () => {
3562    if (!mediaQuery.matches) return;
3563    setExpanded(btn.getAttribute('aria-expanded') !== 'true');
3564  });
3565
3566  const sync = () => {
3567    if (!mediaQuery.matches) setExpanded(false);
3568  };
3569
3570  if (typeof mediaQuery.addEventListener === 'function') {
3571    mediaQuery.addEventListener('change', sync);
3572  } else if (typeof mediaQuery.addListener === 'function') {
3573    mediaQuery.addListener(sync);
3574  }
3575}
3576
3577function initNewExperienceCountriesToggle() {
3578  const block = document.querySelector('.new-experience__countries-block');
3579  const list = block?.querySelector('.new-experience__countries');
3580  const head = block?.querySelector('.new-experience__countries-head');
3581  const btn = block?.querySelector('.new-experience__countries-toggle');
3582  if (!block || !list || !head || !btn) return;
3583
3584  const mediaQuery = window.matchMedia('(max-width: 1024px)');
3585
3586  const getRowHeight = () => {
3587    const first = list.querySelector('.new-experience__country');
3588    if (!first) return 37;
3589    return Math.ceil(first.getBoundingClientRect().height);
3590  };
3591
3592  const applyRowHeightVar = () => {
3593    block.style.setProperty('--countries-row-height', `${getRowHeight()}px`);
3594  };
3595
3596  const setListHeight = (open, animate = true) => {
3597    if (!mediaQuery.matches) {
3598      list.style.maxHeight = '';
3599      return;
3600    }
3601
3602    applyRowHeightVar();
3603
3604    if (open) {
3605      list.style.maxHeight = `${list.scrollHeight}px`;
3606      return;
3607    }
3608
3609    if (!animate) {
3610      list.style.maxHeight = `${getRowHeight()}px`;
3611      return;
3612    }
3613
3614    list.style.maxHeight = `${list.scrollHeight}px`;
3615    requestAnimationFrame(() => {
3616      list.style.maxHeight = `${getRowHeight()}px`;
3617    });
3618  };
3619
3620  const setExpanded = (open, animate = true) => {
3621    block.classList.toggle('is-expanded', open);
3622    btn.setAttribute('aria-expanded', String(open));
3623    setListHeight(open, animate);
3624  };
3625
3626  btn.addEventListener('click', (event) => {
3627    if (!mediaQuery.matches) return;
3628    event.stopPropagation();
3629    setExpanded(btn.getAttribute('aria-expanded') !== 'true');
3630  });
3631
3632  head.addEventListener('click', () => {
3633    if (!mediaQuery.matches) return;
3634    if (!block.classList.contains('is-expanded')) return;
3635    setExpanded(false);
3636  });
3637
3638  const sync = () => {
3639    if (!mediaQuery.matches) {
3640      setExpanded(false, false);
3641      list.style.maxHeight = '';
3642      return;
3643    }
3644    setListHeight(block.classList.contains('is-expanded'), false);
3645  };
3646
3647  sync();
3648
3649  if (typeof mediaQuery.addEventListener === 'function') {
3650    mediaQuery.addEventListener('change', sync);
3651  } else if (typeof mediaQuery.addListener === 'function') {
3652    mediaQuery.addListener(sync);
3653  }
3654
3655  window.addEventListener('resize', () => {
3656    if (!mediaQuery.matches) return;
3657    applyRowHeightVar();
3658    if (block.classList.contains('is-expanded')) {
3659      list.style.maxHeight = `${list.scrollHeight}px`;
3660    } else {
3661      list.style.maxHeight = `${getRowHeight()}px`;
3662    }
3663  });
3664}
3665
3666function initPortfolioAboutSync() {
3667  const hero = document.querySelector('.hero-new-portfolio--detail');
3668  const aboutSection = document.querySelector('.new-portfolio__section-about');
3669  const text = document.querySelector('.new-portfolio__about-text');
3670  const aboutBtn = document.querySelector('.new-portfolio__about-toggle');
3671  const openAboutBtns = document.querySelectorAll(
3672    '.hero-new-portfolio--detail .hero__panel .hero__panel-btn--about, .new-portfolio__about-panel-btn, .new-portfolio__hero-panel-actions .hero__panel-btn--about'
3673  );
3674  const heroCloseBtn = hero?.querySelector('.hero__content-close');
3675  const aboutCollapseBtn = document.querySelector('.new-portfolio__about-collapse');
3676  if (!hero && !aboutSection) return;
3677
3678  const setOpen = (open) => {
3679    if (open) {
3680      initPortfolioHeroMobileHeight.cacheBeforeOpen?.();
3681    }
3682
3683    hero?.classList.toggle('is-about-open', open);
3684    aboutSection?.classList.toggle('is-about-open', open);
3685    text?.classList.toggle('is-open', open);
3686    aboutBtn?.setAttribute('aria-expanded', String(open));
3687    openAboutBtns.forEach((btn) => btn.setAttribute('aria-expanded', String(open)));
3688
3689    requestAnimationFrame(() => {
3690      initPortfolioHeroMobileHeight.update?.();
3691    });
3692  };
3693
3694  openAboutBtns.forEach((btn) => {
3695    btn.addEventListener('click', (event) => {
3696      event.preventDefault();
3697      const open = btn.getAttribute('aria-expanded') !== 'true';
3698      setOpen(open);
3699    });
3700  });
3701
3702  if (heroCloseBtn) {
3703    heroCloseBtn.addEventListener('click', () => {
3704      setOpen(false);
3705    });
3706  }
3707
3708  if (aboutCollapseBtn) {
3709    aboutCollapseBtn.addEventListener('click', () => {
3710      setOpen(false);
3711    });
3712  }
3713
3714  if (aboutBtn) {
3715    aboutBtn.addEventListener('click', () => {
3716      const open = aboutBtn.getAttribute('aria-expanded') !== 'true';
3717      setOpen(open);
3718    });
3719  }
3720}
3721
3722function initPortfolioGallerySlider() {
3723  const section = document.querySelector('.new-portfolio__section-gallery');
3724  if (!section) return;
3725
3726  const formatIndex = (index) => String(index + 1).padStart(2, '0');
3727
3728  section.querySelectorAll('.new-portfolio__gallery').forEach((block) => {
3729    const sliderEl = block.querySelector('.new-portfolio__gallery-slider');
3730    const currentEl = block.querySelector('.new-portfolio__gallery-counter-current');
3731    const totalEl = block.querySelector('.new-portfolio__gallery-counter-total');
3732    const prevBtn = block.querySelector('.new-portfolio__gallery-nav-btn--prev');
3733    const nextBtn = block.querySelector('.new-portfolio__gallery-nav-btn--next');
3734
3735    if (!sliderEl) return;
3736
3737    const slides = sliderEl.querySelectorAll('.swiper-slide');
3738    const total = slides.length;
3739
3740    if (totalEl) {
3741      totalEl.textContent = String(total).padStart(2, '0');
3742    }
3743
3744    const updateCounter = (swiper) => {
3745      if (currentEl) {
3746        currentEl.textContent = formatIndex(swiper.activeIndex);
3747      }
3748    };
3749
3750    const swiper = new Swiper(sliderEl, {
3751      slidesPerView: 'auto',
3752      slidesPerGroup: 1,
3753      spaceBetween: 20,
3754      speed: 900,
3755      loop: false,
3756      breakpoints: {
3757        0: {
3758          spaceBetween: 12,
3759        },
3760        768: {
3761          spaceBetween: 20,
3762        },
3763      },
3764      on: {
3765        init: updateCounter,
3766        slideChange: updateCounter,
3767      },
3768    });
3769
3770    sliderEl.querySelectorAll('img').forEach((img) => {
3771      if (img.complete) return;
3772      img.addEventListener('load', () =>
3772 swiper.update(), { once: true });
3773    });
3774
3775    prevBtn?.addEventListener('click', () => swiper.slidePrev());
3776    nextBtn?.addEventListener('click', () => swiper.slideNext());
3777  });
3778}
3779
3780function initPortfolioReasonsSlider() {
3781  const sliderEl = document.querySelector('.new-portfolio__reasons-cards-slider');
3782  if (!sliderEl) return;
3783
3784  const cards = sliderEl.closest('.new-portfolio__reasons-cards');
3785  const prevBtn = cards?.querySelector('.new-portfolio__reasons-nav-btn--prev');
3786  const nextBtn = cards?.querySelector('.new-portfolio__reasons-nav-btn--next');
3787  const paginationEl = cards?.querySelector('.new-portfolio__reasons-pagination');
3788
3789  setupResponsiveSwiper({
3790    selector: '.new-portfolio__reasons-cards-slider',
3791    breakpoint: 1025,
3792    options: {
3793      slidesPerView: 'auto',
3794      spaceBetween: 16,
3795      speed: 600,
3796      breakpoints: {
3797        768: {
3798          spaceBetween: 20,
3799        },
3800      },
3801      navigation: {
3802        prevEl: prevBtn,
3803        nextEl: nextBtn,
3804      },
3805      pagination: {
3806        el: paginationEl,
3807        clickable: true,
3808      },
3809    },
3810  });
3811}
3812
3813function initSmoothAnchorScroll() {
3814  const header = document.getElementById('header');
3815  if (!header) return;
3816
3817  let scrollAnimationId = null;
3818
3819  const getHeaderOffset = () => header.getBoundingClientRect().height;
3820
3821  const getScrollTopForTarget = (target) => {
3822    const top = window.scrollY + target.getBoundingClientRect().top - getHeaderOffset();
3823    return Math.max(0, top);
3824  };
3825
3826  const easeInOutCubic = (progress) =>
3827    progress < 0.5 ? 4 * progress * progress * progress : 1 - Math.pow(-2 * progress + 2, 3) / 2;
3828
3829  const playActiveShowcaseVideo = (scope) => {
3830    const activeSlide = scope.querySelector('.showcase__slide.swiper-slide-active');
3831    if (!activeSlide) return;
3832
3833    const video = activeSlide.querySelector('video');
3834    const playBtn = activeSlide.querySelector('.video-js__play');
3835    if (!video || !video.paused) return;
3836
3837    if (playBtn) {
3838      playBtn.click();
3839      return;
3840    }
3841
3842    video.play().catch(() => {});
3843  };
3844
3845  // play() после rAF-скролла теряет user-gesture; удерживаем медиа до конца скролла.
3846  let holdShowcaseVideoUntilScrollEnd = false;
3847
3848  const prepareShowcaseVideoForScrollPlay = (scope) => {
3849    const activeSlide = scope.querySelector('.showcase__slide.swiper-slide-active');
3850    const video = activeSlide?.querySelector('video');
3851    if (!video) return;
3852
3853    holdShowcaseVideoUntilScrollEnd = true;
3854
3855    const playPromise = video.play();
3856    if (!playPromise?.then) return;
3857
3858    playPromise
3859      .then(() => {
3860        if (!holdShowcaseVideoUntilScrollEnd) return;
3861        video.pause();
3862        video.currentTime = 0;
3863      })
3864      .catch(() => {});
3865  };
3866
3867  const smoothScrollTo = (top, duration = 600, onComplete) => {
3868    if (scrollAnimationId) {
3869      cancelAnimationFrame(scrollAnimationId);
3870      scrollAnimationId = null;
3871    }
3872
3873    const start = window.scrollY;
3874    const distance = top - start;
3875
3876    if (Math.abs(distance) < 1) {
3877      window.scrollTo(0, top);
3878      onComplete?.();
3879      return;
3880    }
3881
3882    const startTime = performance.now();
3883
3884    const step = (currentTime) => {
3885      const elapsed = currentTime - startTime;
3886      const progress = Math.min(elapsed / duration, 1);
3887      window.scrollTo(0, start + distance * easeInOutCubic(progress));
3888
3889      if (progress < 1) {
3890        scrollAnimationId = requestAnimationFrame(step);
3891      } else {
3892        scrollAnimationId = null;
3893        onComplete?.();
3894      }
3895    };
3896
3897    scrollAnimationId = requestAnimationFrame(step);
3898  };
3899
3900  const scrollToHashTarget = (target, { updateHash = true, smooth = true } = {}) => {
3901    if (!target) return;
3902
3903    const top = getScrollTopForTarget(target);
3904    const afterScroll = () => {
3905      if (target.id === 'video') {
3906        holdShowcaseVideoUntilScrollEnd = false;
3907        playActiveShowcaseVideo(target);
3908      }
3909    };
3910
3911    if (smooth) {
3912      smoothScrollTo(top, 600, afterScroll);
3913    } else {
3914      window.scrollTo(0, top);
3915      afterScroll();
3916    }
3917
3918    if (updateHash && target.id) {
3919      history.pushState(null, '', `#${target.id}`);
3920    }
3921  };
3922
3923  const getHashFromLink = (link) => {
3924    const href = link.getAttribute('href');
3925    if (!href) return '';
3926
3927    if (href.startsWith('#')) {
3928      return href;
3929    }
3930
3931    try {
3932      const url = new URL(href, window.location.href);
3933      if (url.origin !== window.location.origin) return '';
3934      if (url.pathname !== window.location.pathname) return '';
3935      return url.hash;
3936    } catch {
3937      return '';
3938    }
3939  };
3940
3941  const handleAnchorClick = (event) => {
3942    const link = event.target.closest('a[href*="#"]');
3943    if (!link || link.target === '_blank' || link.classList.contains('hero__panel-btn--about')) {
3944      return;
3945    }
3946
3947    const hash = getHashFromLink(link);
3948    if (!hash || hash === '#') return;
3949
3950    let target;
3951    try {
3952      target = document.querySelector(hash);
3953    } catch {
3954      return;
3955    }
3956
3957    if (!target) return;
3958
3959    event.preventDefault();
3960    event.stopPropagation();
3961
3962    if (target.id === 'video') {
3963      prepareShowcaseVideoForScrollPlay(target);
3964    }
3965
3966    scrollToHashTarget(target);
3967  };
3968
3969  document.addEventListener('click', handleAnchorClick, true);
3970
3971  if (window.location.hash && window.location.hash !== '#') {
3972    if ('scrollRestoration' in history) {
3973      history.scrollRestoration = 'manual';
3974    }
3975    window.scrollTo(0, 0);
3976  }
3977
3978  const scrollOnLoad = () => {
3979    const { hash } = window.location;
3980    if (!hash || hash === '#') return;
3981
3982    let target;
3983    try {
3984      target = document.querySelector(hash);
3985    } catch {
3986      return;
3987    }
3988
3989    if (!target) return;
3990
3991    scrollToHashTarget(target, { updateHash: false, smooth: false });
3992  };
3993
3994  if (document.readyState === 'complete') {
3995    scrollOnLoad();
3996  } else {
3997    window.addEventListener('load', scrollOnLoad, { once: true });
3998  }
3999}
4000
4001function initToggleActive(buttonSelector, containerSelector, activeClass = 'is-active') {
4002  document.querySelectorAll(buttonSelector).forEach((button) => {
4003    button.addEventListener('click', () => {
4004      const container = button.closest(containerSelector);
4005      if (container) {
4006        container.classList.toggle(activeClass);
4007      }
4008    });
4009  });
4010}
4011
4012function hydrateMirrors() {
4013  const groups = {};
4014  document.querySelectorAll('[data-mirror]').forEach((el) => {
4015    const key = el.dataset.mirror;
4016    (groups[key] = groups[key] || []).push(el);
4017  });
4018  Object.values(groups).forEach((els) => {
4019    const src = els.find((e) => e.innerHTML.trim() !== '');
4020    if (!src) return;
4021    els.forEach((e) => {
4022      if (e !== src && e.innerHTML.trim() === '') e.innerHTML = src.innerHTML;
4023    });
4024  });
4025}
4026
4027function hydrateMobileCountries() {
4028  document.querySelectorAll('[data-mobile-countries]').forEach((container) => {
4029    const tpl = document.querySelector(
4030      `template[data-mobile-countries-tpl="${container.dataset.mobileCountries}"]`
4031    );
4032    if (!tpl) return;
4033    container.appendChild(tpl.content.cloneNode(true));
4034    tpl.remove();
4035  });
4036}
4037
4038function countriesMapInit() {
4039  const buttons = document.querySelectorAll('.experience__country');
4040  const regions = document.querySelectorAll('.experience__map .mapsvg-region');
4041
4042  buttons.forEach((btn) => {
4043    btn.addEventListener('mouseenter', () => {
4044      const regionId = btn.dataset.countryId;
4045      const region = document.querySelector(`.mapsvg-region[data-name="${regionId}"]`);
4046
4047      regions.forEach((r) => r.classList.remove('active'));
4048      buttons.forEach((b) => b.classList.remove('active'));
4049      btn.classList.add('active');
4050
4051      if (region) {
4052        region.classList.add('active');
4053
4054        const bbox = region.getBBox();
4055        const cx = bbox.x + bbox.width / 2;
4056        const cy = bbox.y + bbox.height / 2;
4057
4058        let marker = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
4059        marker.setAttribute('cx', cx);
4060        marker.setAttribute('cy', cy);
4061        marker.setAttribute('r', 5);
4062        marker.setAttribute('fill', 'red');
4063        marker.classList.add('region-marker');
4064
4065        region.parentNode.appendChild(marker);
4066      }
4067    });
4068
4069    btn.addEventListener('mouseleave', () => {
4070      btn.classList.remove('active');
4071
4072      const regionId = btn.dataset.countryId;
4073      const region = document.querySelector(`.mapsvg-region[data-name="${regionId}"]`);
4074
4075      if (region) {
4076        region.classList.remove('active');
4077
4078        const marker = document.querySelector('.region-marker');
4079        if (marker) marker.remove();
4080      }
4081    });
4082  });
4083}
4084
4085// AJAX-loaded modals: keep modal scaffolding out of the static HTML for crawlers.
4086// Modals live in modals.php and are fetched on demand via /wp-admin/admin-ajax.php?action=vp_modals.
4087window.vpModalsState = window.vpModalsState || { loaded: false, promise: null };
4088
4089function vpLoadModals() {
4090  if (window.vpModalsState.loaded) return Promise.resolve(true);
4091  if (window.vpModalsState.promise) return window.vpModalsState.promise;
4092  const ajaxUrl = (window.VPAjax && window.VPAjax.url) || '/wp-admin/admin-ajax.php';
4093  window.vpModalsState.promise = fetch(ajaxUrl + '?action=vp_modals', {
4094    credentials: 'same-origin',
4095  })
4096    .then((r) => r.text())
4097    .then((html) => {
4098      const mount = document.getElementById('vp-modals-mount');
4099      if (!mount) return false;
4100      mount.insertAdjacentHTML('beforeend', html);
4101      mount.removeAttribute('aria-hidden');
4102      window.vpModalsState.loaded = true;
4103
4104      // Re-init CF7 forms inside newly mounted modals (v5+ API)
4105      if (window.wpcf7 && typeof window.wpcf7.init === 'function') {
4106        mount.querySelectorAll('form.wpcf7-form, .wpcf7 form').forEach(function (f) {
4107          try {
4108            window.wpcf7.init(f);
4109          } catch (e) {}
4110        });
4111      }
4112
4113      // Run modal-related init hooks now that elements exist
4114      try {
4115        if (typeof initProject360Modal === 'function') initProject360Modal();
4116      } catch (e) {}
4117      try {
4118        if (typeof quizInit === 'function') quizInit();
4119      } catch (e) {}
4120      try {
4121        if (typeof initPhoneInput === 'function') initPhoneInput();
4122      } catch (e) {}
4123      try {
4124        if (typeof initFileUpload === 'function') initFileUpload();
4125      } catch (e) {}
4126      try {
4127        if (typeof hydrateMirrors === 'function') hydrateMirrors();
4128      } catch (e) {}
4129
4130      return true;
4131    })
4132    .catch(function (err) {
4133      // Reset so retry is possible
4134      window.vpModalsState.promise = null;
4135      console.warn('[vp] modals AJAX failed', err);
4136      return false;
4137    });
4138  return window.vpModalsState.promise;
4139}
4140
4141// Eager-load modals after page is idle (without blocking critical render).
4142// Most users get modals ready before clicking. Falls back to on-click load if this fails.
4143(function eagerLoadModals() {
4144  if (typeof window === 'undefined') return;
4145  const trigger = function () {
4146    vpLoadModals();
4147  };
4148  if ('requestIdleCallback' in window) {
4149    window.addEventListener('load', function () {
4150      requestIdleCallback(trigger, { timeout: 2500 });
4151    });
4152  } else {
4153    window.addEventListener('load', function () {
4154      setTimeout(trigger, 800);
4155    });
4156  }
4157  // Also trigger on first user interaction in case 'load' is delayed
4158  ['pointerdown', 'keydown', 'touchstart'].forEach(function (ev) {
4159    document.addEventListener(ev, trigger, { once: true, passive: true });
4160  });
4161})();
4162
4163function modalInit() {
4164  function closeModal(modal) {
4165    if (!modal) return;
4166    const source = modal.querySelector('input[name="source"]');
4167    modal.classList.remove('modal--active');
4168    if (source) source.value = '';
4169    if (!document.querySelector('.modal--active')) {
4170      document.body.classList.remove('overflow-hidden');
4171    }
4172  }
4173
4174  function activateModal(button, modal) {
4175    document.body.classList.add('overflow-hidden');
4176    modal.classList.add('modal--active');
4177
4178    if (button.hasAttribute('data-title')) {
4179      const sourceInput = modal.querySelector('input[name="source"]');
4180      if (sourceInput) sourceInput.value = button.getAttribute('data-title');
4181    }
4182
4183    const title = button.dataset.title;
4184    const img = button.dataset.img;
4185    const decorIndex = button.dataset.decorIndex;
4186    const decorLeftSrc = button.dataset.decorLeft;
4187    const decorRightSrc = button.dataset.decorRight;
4188
4189    const modalTitle = modal.querySelector('.js-modal-title');
4190    const modalImg = modal.querySelector('.js-modal-img');
4191    const decorLeft = modal.querySelector('.js-modal-decor-left');
4192    const decorRight = modal.querySelector('.js-modal-decor-right');
4193    const decorWrappers = modal.querySelectorAll('.modal__decor-wrapper');
4194
4195    if (title && modalTitle) modalTitle.innerHTML = title;
4196    if (img && modalImg) modalImg.src = img;
4197
4198    if (decorLeft && decorRight) {
4199      decorLeft.className = 'decor modal__decor js-modal-decor-left';
4200      decorRight.className = 'decor modal__decor js-modal-decor-right';
4201
4202      decorWrappers.forEach(function (wrapper) {
4203        wrapper.className = 'decor-wrapper modal__decor-wrapper hide-md';
4204      });
4205
4206      if (decorIndex) {
4207        decorLeft.classList.add('modal__decor-' + decorIndex + '--left');
4208        decorRight.classList.add('modal__decor-' + decorIndex + '--right');
4209        decorWrappers.forEach(function (wrapper) {
4210          wrapper.classList.add('modal__decor-wrapper-' + decorIndex);
4211        });
4212      }
4213
4214      const leftImg = decorLeft.querySelector('img');
4215      const rightImg = decorRight.querySelector('img');
4216      if (decorLeftSrc && leftImg) leftImg.src = decorLeftSrc;
4217      if (decorRightSrc && rightImg) rightImg.src = decorRightSrc;
4218    }
4219  }
4220
4221  // Delegated OPEN handler. Modals are AJAX-loaded into #vp-modals-mount; wait for that
4222  // before activating. Falls back gracefully if AJAX hasn't finished.
4223  document.addEventListener('click', function (e) {
4224    const button = e.target.closest('[data-modal]');
4225    if (!button) return;
4226    if (e.target.closest('[data-close-modal]')) return; // edge case: click on close inside an active modal
4227    e.preventDefault();
4228
4229    const modalId = button.dataset.modal;
4230    // Try immediate open (modals may already be mounted from eager-load)
4231    let modal = document.getElementById(modalId);
4232    if (modal) {
4233      activateModal(button, modal);
4234      return;
4235    }
4236    // Modals not yet loaded — fetch and then open
4237    if (typeof vpLoadModals === 'function') {
4238      vpLoadModals().then(function () {
4239        const m = document.getElementById(modalId);
4240        if (m) activateModal(button, m);
4241      });
4242    }
4243  });
4244
4245  // Delegated CLOSE handler: works for close buttons inside dynamically mounted modals
4246  document.addEventListener('click', function (e) {
4247    const btn = e.target.closest('[data-close-modal]');
4248    if (!btn) return;
4249    closeModal(btn.closest('.modal'));
4250  });
4251
4252  // Esc to close (верхняя модалка, если открыто несколько)
4253  document.addEventListener('keydown', function (e) {
4254    if (e.key === 'Escape' || e.key === 'Esc') {
4255      const actives = document.querySelectorAll('.modal.modal--active');
4256      closeModal(actives[actives.length - 1]);
4257    }
4258  });
4259
4260  // Click outside (on backdrop) to close
4261  document.addEventListener('click', function (e) {
4262    if (e.target.classList && e.target.classList.contains('modal--active')) {
4263      closeModal(e.target);
4264    }
4265  });
4266}
4267
4268HTMLElement.prototype.slideToggle = function (duration = 400, displayType = 'block') {
4269  this.clientHeight === 0 ? slideDown(this, duration, displayType) : slideUp(this, duration);
4270};
4271
4272function slideDown(el, duration, displayType) {
4273  el.style.overflow = 'hidden';
4274  el.style.display = displayType;
4275
4276  const elStyles = window.getComputedStyle(el);
4277  const original = {
4278    height: parseFloat(elStyles.height),
4279    paddingTop: parseFloat(elStyles.paddingTop),
4280    paddingBottom: parseFloat(elStyles.paddingBottom),
4281    marginTop: parseFloat(elStyles.marginTop),
4282    marginBottom: parseFloat(elStyles.marginBottom),
4283  };
4284
4285  animate(el, original, duration, true);
4286}
4287
4288function slideUp(el, duration) {
4289  const elStyles = window.getComputedStyle(el);
4290  const original = {
4291    height: parseFloat(elStyles.height),
4292    paddingTop: parseFloat(elStyles.paddingTop),
4293    paddingBottom: parseFloat(elStyles.paddingBottom),
4294    marginTop: parseFloat(elStyles.marginTop),
4295    marginBottom: parseFloat(elStyles.marginBottom),
4296  };
4297
4298  animate(el, original, duration, false);
4299}
4300
4301function animate(el, original, duration, isDown) {
4302  let start;
4303
4304  function step(timestamp) {
4305    if (start === undefined) start = timestamp;
4306    const progress = Math.min((timestamp - start) / duration, 1);
4307    const factor = isDown ? progress : 1 - progress;
4308
4309    el.style.height = original.height * factor + 'px';
4310    el.style.paddingTop = original.paddingTop * factor + 'px';
4311    el.style.paddingBottom = original.paddingBottom * factor + 'px';
4312    el.style.marginTop = original.marginTop * factor + 'px';
4313    el.style.marginBottom = original.marginBottom * factor + 'px';
4314
4315    if (progress >= 1) {
4316      el.style.height = '';
4317      el.style.paddingTop = '';
4318      el.style.paddingBottom = '';
4319      el.style.marginTop = '';
4320      el.style.marginBottom = '';
4321      el.style.overflow = '';
4322      if (!isDown) el.style.display = 'none';
4323    } else {
4324      requestAnimationFrame(step);
4325    }
4326  }
4327
4328  requestAnimationFrame(step);
4329}
4330
4331function initFormField() {
4332  const currentUrl = window.location.href;
4333  const pageUrlFields = document.querySelectorAll('input[name="your-page-url"]');
4334
4335  if (pageUrlFields.length) {
4336    const url = new URL(currentUrl);
4337    const searchParams = new URLSearchParams(url.search);
4338    const utmParams = new URLSearchParams();
4339
4340    // Собираем только UTM-параметры
4341    for (const [key, value] of searchParams.entries()) {
4342      if (key.startsWith('utm_')) {
4343        utmParams.set(key, value);
4344      }
4345    }
4346
4347    // Если есть UTM-параметры, добавляем их в URL
4348    if (utmParams.size > 0) {
4349      // Добавляем UTM к существующим параметрам
4350      for (const [key, value] of utmParams.entries()) {
4351        searchParams.set(key, value);
4352      }
4353      url.search = searchParams.toString();
4354
4355      // Обновляем currentUrl с новым URL
4356      const updatedUrl = url.toString();
4357      // Заполняем все найденные поля
4358      pageUrlFields.forEach((field) => (field.value = updatedUrl));
4359    } else {
4360      // Если UTM-параметров нет, просто заполняем текущим URL
4361      pageUrlFields.forEach((field) => (field.value = currentUrl));
4362    }
4363  }
4364}
4365
4366document.addEventListener('click', (e) => {
4367  const modalBtn = e.target.closest('#header .header__bottom-mobile [data-modal]');
4368  if (!modalBtn) return;
4369
4370  const header = document.getElementById('header');
4371  if (!header?.classList.contains('header--nav-active')) return;
4372
4373  // только мобильное меню
4374  if (window.innerWidth >= 1201) return;
4375
4376  const burger = header.querySelector('.header__burger');
4377  if (burger) burger.click();
4378});
4379
4380function initHeroVideo() {
4381  document.querySelectorAll('.hero__slider .before-after__handle button').forEach((button) => {
4382    button.addEventListener('pointerup', () => {
4383      const slider = button.closest('.hero__slider');
4384      if (!slider) return;
4385      slider.querySelectorAll('video').forEach((video) => {
4386        video.play().catch((err) => console.log('Play blocked:', err));
4387      });
4388      slider.querySelectorAll('.before-after__icon--hand').forEach((el) => {
4389        el.classList.remove('btn-hidden');
4390      });
4391      slider.querySelectorAll('.before-after__icon--play').forEach((el) => {
4392        el.classList.add('btn-hidden');
4393      });
4394    });
4395  });
4396}
4397
4398/* ============================================================
4399   YANDEX METRIKA — цели (счётчик 23472505)
4400   ============================================================ */
4401(function initMetrikaGoals() {
4402  const COUNTER_ID = 23472505;
4403  const reach = (g, p) => { try { window.ym(COUNTER_ID, 'reachGoal', g, p || {}); } catch (e) {} };
4404
4405  // ---------- 1. Соцсети (BEM-классы) ----------
4406  const SOCIAL = {
4407    'social__link--whatsapp': 'whatsapp',
4408    'social__link--telegram': 'telegram',
4409    'social__link--youtube':  'youtube',
4410    'header__contacts-messenger--telegram': 'telegram',
4411  };
4412  document.addEventListener('click', (e) => {
4413    const a = e.target.closest('a[class*="social__link--"], a[class*="header__contacts-messenger--"]');
4414    if (!a) return;
4415    for (const c in SOCIAL) if (a.classList.contains(c)) { reach(SOCIAL[c]); return; }
4416  }, true);
4417
4418  // ---------- 2. Соцсети/площадки по href ----------
4419  document.addEventListener('click', (e) => {
4420    const a = e.target.closest('a[href]');
4421    if (!a) return;
4422    const h = (a.getAttribute('href') || '').toLowerCase();
4423    if (/youtube\.com|youtu\.be/.test(h)) reach('youtube');
4424    else if (/houzz\./.test(h))          reach('houzz');
4425    else if (/profi\.ru/.test(h))        reach('profi');
4426    else if (/contacts/.test(h))         reach('contacts');
4427  }, true);
4428
4429  // ---------- 3. on_phone ----------
4430  document.addEventListener('click', (e) => {
4431    const a = e.target.closest('a[href^="tel:"]');
4432    if (a) reach('on_phone', { phone: a.getAttribute('href') });
4433  }, true);
4434
4435  // ---------- 4. Определяем цель кнопки ----------
4436  const MODAL_ID_TO_GOAL = {
4437    'modal-rasschitat-stoimost':                   'smeta',
4438    'modal-ostavit-zayavku-3-vedomost-smeta':      'smeta',
4439    'modal-kvartiry-doma':                         'zakaz_dp',
4440    'modal-kommercheskie-interery':                'zakaz_dp',
4441    'modal-zhilye-interery':                       'zakaz_dp',
4442    'modal-sotrudnichestvo':                       'napishite',
4443    'modal-prezentacziya-mopov-i-lobbi':           'prez_gold',
4444    'modal-prezentacziya-arhitekturnyh-proektov':  'prez_chrome',
4445    'modal-prezentacziya-ofisnyh-intererov':       'prez_white',
4446    'modal-premialnyj-stil':                       'prez_black',
4447  };
4448
4449  const TEXT_TO_GOAL = {
4450    'перезвоните мне':            'call_back',
4451    'заказать звонок':            'call_back',
4452    'заказать консультацию':      'consult',
4453    'получить консультацию':      'consult',
4454    'заказать смету':             'smeta',
4455    'рассчитать стоимость':       'smeta',
4456    'заказать день с дизайнером': 'day_wd',
4457    'заказать тест-драйв':        'test_drive',
4458    'заказать дизайн и надзор':   'des_nadzor',
4459    'заказать дизайн под ключ':   'des_kluch',
4460    'заявка на авторский надзор': 'aut_nadzor',
4461    'заявка на комплектацию':     'komplekt',
4462    'заявка на управление':       'uprav',
4463    'напишите нам отзыв':         'otzyv',
4464    'напишите нам':               'napishite',
4465    'оставить заявку':            'zayavka',
4466    'подать заявку':              'zayavka',
4467    'получить альбом со стилем':  'album_style',
4468  };
4469
4470  function detectGoal(btn) {
4471    if (!btn) return null;
4472    if (btn.dataset.goal) return btn.dataset.goal;
4473
4474    // --- Спец-случай: заявка внутри .cta = «Заказать смету» ---
4475    if (btn.closest('.cta')) return 'smeta';
4476
4477    const title = (btn.dataset.title || '').trim().toLowerCase();
4478    if (TEXT_TO_GOAL[title]) return TEXT_TO_GOAL[title];
4479
4480    const text = (btn.textContent || '').replace(/\s+/g, ' ').trim().toLowerCase();
4481    if (TEXT_TO_GOAL[text]) return TEXT_TO_GOAL[text];
4482
4483    const modalId = btn.dataset.modal;
4484    if (MODAL_ID_TO_GOAL[modalId]) return MODAL_ID_TO_GOAL[modalId];
4485
4486    return null;
4487  }
4488
4489  let pendingGoal = null;
4490  document.addEventListener('click', (e) => {
4491    const btn = e.target.closest('[data-modal]');
4492    if (!btn) return;
4493    pendingGoal = detectGoal(btn) || pendingGoal;
4494  }, true);
4495
4496  // ---------- 5. Отправка CF7 ----------
4497  document.addEventListener('wpcf7mailsent', (e) => {
4498    const form = e.target;
4499    const goal = form.dataset.goal || pendingGoal || 'zayavka';
4500    reach(goal, { source: form.querySelector('input[name="source"]')?.value || '' });
4501    pendingGoal = null;
4502  });
4503
4504  // ---------- 6. Отправка квиза ----------
4505  document.addEventListener('submit', (e) => {
4506    const form = e.target.closest('.quiz__form');
4507    if (!form) return;
4508    const quiz = form.closest('.quiz');
4509    const goal = quiz?.dataset.goal || 'style_q'; // style_q / album_style
4510    reach(goal);
4511  }, true);
4512})();

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.