PageSourceSearch

https://everest-marketing.ru/assets/js/site.js?v=caeb503c

js everest-marketing.ru collected 2026-10-10 02:20:52 UTC 30,451 bytes, 687 lines download raw bytes

1/* ==========================================================================
2   Эверест. Поведение сайта.
3   Без внешних зависимостей. Скролл слушаем только через IntersectionObserver.
4   ========================================================================== */
5(function () {
6  'use strict';
7
8  var reduced = window.matchMedia('(prefers-reduced-motion: reduce)');
9  var desktop = window.matchMedia('(min-width: 1181px)');
10
11  /* ---------------------------------------------- Шапка: состояние «прилипла» */
12  function stickyHeader() {
13    var hdr = document.querySelector('.hdr');
14    var sentinel = document.querySelector('[data-hdr-sentinel]');
15    if (!hdr || !sentinel || !('IntersectionObserver' in window)) return;
16    new IntersectionObserver(function (entries) {
17      hdr.classList.toggle('is-stuck', !entries[0].isIntersecting);
18    }, { threshold: 0 }).observe(sentinel);
19  }
20
21  /* ------------------------------------------------------ Меню и подменю */
22  function menu() {
23    var nav = document.querySelector('[data-nav]');
24    var burger = document.querySelector('[data-burger]');
25    var items = Array.prototype.slice.call(document.querySelectorAll('.nav__item[data-sub]'));
26    if (!nav) return;
27
28    function closeAll(except) {
29      items.forEach(function (it) {
30        if (it === except) return;
31        it.setAttribute('data-open', 'false');
32        var t = it.querySelector('[data-sub-toggle]');
33        if (t) t.setAttribute('aria-expanded', 'false');
34        it.querySelectorAll('[data-sub2]').forEach(function (g) {
35          g.setAttribute('data-open', 'false');
36          var b = g.querySelector('.subnav__toggle');
37          if (b) b.setAttribute('aria-expanded', 'false');
38        });
39      });
40    }
41    function toggle(item, open) {
42      item.setAttribute('data-open', open ? 'true' : 'false');
43      var t = item.querySelector('[data-sub-toggle]');
44      if (t) t.setAttribute('aria-expanded', open ? 'true' : 'false');
45    }
46
47    items.forEach(function (item) {
48      var link = item.querySelector('.nav__link');
49      var more = item.querySelector('[data-sub-toggle]');
50      var timer;
51
52      // Десктоп: наведение с небольшой задержкой на уход, чтобы не мигало
53      item.addEventListener('pointerenter', function (e) {
54        if (!desktop.matches || e.pointerType === 'touch') return;
55        clearTimeout(timer);
56        closeAll(item);
57        toggle(item, true);
58      });
59      item.addEventListener('pointerleave', function (e) {
60        if (!desktop.matches || e.pointerType === 'touch') return;
61        timer = setTimeout(function () { toggle(item, false); }, 120);
62      });
63
64      /* Ссылка ведёт на страницу раздела и на десктопе, и на телефоне.
65         Подменю раскрывает соседняя стрелка - её и жмут, когда нужен список. */
66      if (more) {
67        more.addEventListener('click', function () {
68          var open = item.getAttribute('data-open') === 'true';
69          closeAll(item);
70          toggle(item, !open);
71        });
72      }
73      link.addEventListener('focus', function () {
74        if (!desktop.matches) return;
75        closeAll(item);
76        toggle(item, true);
77      });
78      item.addEventListener('keydown', function (e) {
79        if (e.key === 'Escape') { toggle(item, false); link.focus(); }
80      });
81      item.addEventListener('focusout', function (e) {
82        if (!item.contains(e.relatedTarget)) toggle(item, false);
83      });
84    });
85
86    /* Второй уровень: ниши. На десктопе раскрывается наведением, на тапе - кликом */
87    var groups = Array.prototype.slice.call(document.querySelectorAll('[data-sub2]'));
88    function toggle2(group, open) {
89      group.setAttribute('data-open', open ? 'true' : 'false');
90      var btn = group.querySelector('.subnav__toggle');
91      if (btn) btn.setAttribute('aria-expanded', open ? 'true' : 'false');
92    }
93    function closeGroups(except) {
94      groups.forEach(function (g) { if (g !== except) toggle2(g, false); });
95    }
96    groups.forEach(function (group) {
97      var btn = group.querySelector('.subnav__toggle');
98      var timer;
99      group.addEventListener('pointerenter', function (e) {
100        if (!desktop.matches || e.pointerType === 'touch') return;
101        clearTimeout(timer); closeGroups(group); toggle2(group, true);
102      });
103      group.addEventListener('pointerleave', function (e) {
104        if (!desktop.matches || e.pointerType === 'touch') return;
105        timer = setTimeout(function () { toggle2(group, false); }, 120);
106      });
107      if (btn) {
108        btn.addEventListener('click', function () {
109          var open = group.getAttribute('data-open') === 'true';
110          closeGroups(group); toggle2(group, !open);
111        });
112      }
113      group.addEventListener('keydown', function (e) {
114        if (e.key === 'Escape') { toggle2(group, false); if (btn) btn.focus(); }
115      });
116      group.addEventListener('focusout', function (e) {
117        if (!group.contains(e.relatedTarget)) toggle2(group, false);
118      });
119    });
120
121    document.addEventListener('pointerdown', function (e) {
122      if (!e.target.closest('.nav__item')) closeAll(null);
123    });
124
125    if (burger) {
126      burger.addEventListener('click', function () {
127        var open = burger.getAttribute('aria-expanded') === 'true';
128        burger.setAttribute('aria-expanded', open ? 'false' : 'true');
129        nav.classList.toggle('is-open', !open);
130        if (open) closeAll(null);
131      });
132    }
133    desktop.addEventListener('change', function () {
134      nav.classList.remove('is-open');
135      if (burger) burger.setAttribute('aria-expanded', 'false');
136      closeAll(null);
137    });
138  }
139
140  /* ------------------------------------------------------------- Аккордеон */
141  function accordion() {
142    document.querySelectorAll('[data-acc] .acc__btn').forEach(function (btn) {
143      btn.addEventListener('click', function () {
144        var item = btn.closest('.acc__item');
145        var open = item.getAttribute('data-open') === 'true';
146        item.setAttribute('data-open', open ? 'false' : 'true');
147        btn.setAttribute('aria-expanded', open ? 'false' : 'true');
148      });
149    });
150  }
151
152  /* ------------------------------------------- Длинный отзыв: читать полностью */
153  function reviews() {
154    document.querySelectorAll('[data-review-more]').forEach(function (btn) {
155      btn.addEventListener('click', function () {
156        var box = btn.closest('.review');
157        box.setAttribute('data-open', 'true');
158        var text = box.querySelector('.review__text');
159        if (text) text.removeAttribute('aria-expanded');
160      });
161    });
162  }
163
164  /* ------------------------------------------------- Фильтр и «показать ещё» */
165  function caseList() {
166    var root = document.querySelector('[data-cases]');
167    if (!root) return;
168    var cards = Array.prototype.slice.call(root.querySelectorAll('.case, .post'));
169    var filters = Array.prototype.slice.call(document.querySelectorAll('[data-filter]'));
170    var moreBtn = document.querySelector('[data-cases-more]');
171    var empty = document.querySelector('[data-cases-empty]');
172    var STEP = parseInt(root.dataset.casesStep || '9', 10);
173    var shown = STEP;
174    var active = 'all';
175
176    function matches(card) {
177      return active === 'all' || (card.dataset.tags || '').split('|').indexOf(active) > -1;
178    }
179    function render() {
180      var visible = 0;
181      cards.forEach(function (card) {
182        if (!matches(card)) { card.hidden = true; return; }
183        visible++;
184        card.hidden = visible > shown;
185      });
186      if (moreBtn) moreBtn.hidden = visible <= shown;
187      if (empty) empty.hidden = visible !== 0;
188    }
189    filters.forEach(function (f) {
190      f.addEventListener('click', function () {
191        active = f.dataset.filter;
192        shown = STEP;
193        filters.forEach(function (x) { x.setAttribute('aria-pressed', String(x === f)); });
194        render();
195      });
196    });
197    if (moreBtn) moreBtn.addEventListener('click', function () { shown += STEP; render(); });
198    render();
199  }
200
201  /* ------------------------------------------------------------ Блокировка прокрутки */
202  /* Скрываем прокрутку под окном и компенсируем ширину полосы, чтобы страница не прыгала */
203  var locks = 0;
204  function lockScroll() {
205    if (locks++ > 0) return;
206    var w = window.innerWidth - document.documentElement.clientWidth;
207    document.documentElement.style.setProperty('--sbw', w + 'px');
208    document.body.classList.add('is-locked');
209  }
210  function unlockScroll() {
211    if (--locks > 0) return;
212    locks = 0;
213    document.body.classList.remove('is-locked');
214    document.documentElement.style.removeProperty('--sbw');
215  }
216
217  /* ------------------------------------------------------------ Цели Метрики */
218  var METRIKA = 50166202;
219  function goal(name, params) {
220    if (typeof window.ym !== 'function') return;
221    try {
222      if (params) window.ym(METRIKA, 'reachGoal', name, params);
223      else window.ym(METRIKA, 'reachGoal', name);
224    } catch (e) {}
225  }
226
227  /* Отмеченные чипы «что хотелось бы улучшить»: уходят в параметр цели,
228     чтобы в отчёте было видно, с какой задачей приходят. */
229  function pickedTags(form) {
230    var v = [];
231    form.querySelectorAll('input[name="tags[]"]:checked').forEach(function (i) {
232      v.push(i.value);
233    });
234    return v.join(', ');
235  }
236
237  /* ------------------------------------------------------------ Маска телефона */
238  /* +7 (___) ___-__-__: ведущие 8 и 7 считаем кодом страны */
239  function phoneMask(input) {
240    function format(value) {
241      var d = value.replace(/\D/g, '');
242      /* +7 уже в поле - это код страны; вставленный номер из 11 цифр начинается с 7 или 8 */
243      if (/^\s*\+7/.test(value)) {
244        d = d.slice(1);
245        /* первая набранная 8 или 7 - привычный выход на межгород, а не часть номера */
246        if (d.length === 1 && (d === '8' || d === '7')) d = '';
247      } else if (d.length === 11 && (d.charAt(0) === '8' || d.charAt(0) === '7')) d = d.slice(1);
248      d = d.slice(0, 10);
249      if (!d) return /^\s*\+7/.test(value) ? '+7 (' : '';
250      var out = '+7 (' + d.slice(0, 3);
251      if (d.length >= 3) out += ')';
252      if (d.length > 3) out += ' ' + d.slice(3, 6);
253      if (d.length > 6) out += '-' + d.slice(6, 8);
254      if (d.length > 8) out += '-' + d.slice(8, 10);
255      return out;
256    }
257    input.addEventListener('input', function () {
258      var atEnd = input.selectionStart === input.value.length;
259      input.value = format(input.value);
260      if (atEnd) input.setSelectionRange(input.value.length, input.value.length);
261    });
262    input.addEventListener('focus', function () { if (!input.value) input.value = '+7 ('; });
263    input.addEventListener('blur', function () { if (input.value.replace(/\D/g, '').length <= 1) input.value = ''; });
264  }
265
266  /* ------------------------------------------------------------ Отправка форм */
267  /* Одна отправка для короткой формы в окне и большой внизу. Адрес обработчика
268     задаётся в одном месте, _build/common.js -> FORM_ENDPOINT. Пусто - заглушка. */
269  var mdlRoot = document.querySelector('[data-mdl]');
270  var ENDPOINT = mdlRoot ? mdlRoot.getAttribute('data-endpoint') || '' : '';
271  var SERVICE = mdlRoot ? mdlRoot.getAttribute('data-service') || '' : '';
272
273  function send(data) {
274    if (!ENDPOINT) {
275      /* заглушка: данные никуда не уходят, ответ как у настоящего обработчика */
276      return new Promise(function (resolve) { setTimeout(function () { resolve({ ok: true, stub: true }); }, 600); });
277    }
278    return fetch(ENDPOINT, { method: 'POST', body: data, headers: { 'Accept': 'application/json' } })
279      .then(function (r) { return r.json().catch(function () { return { ok: r.ok }; }); });
280  }
281
282  function setupForm(form, hooks) {
283    var submit = form.querySelector('[data-submit]');
284    var status = form.querySelector('[data-status]');
285    var ts = form.querySelector('[data-ts]');
286    if (ts) ts.value = String(Date.now());
287    form.querySelectorAll('[data-phone]').forEach(phoneMask);
288
289    /* Файл с ТЗ: имя выбранного файла и лимит 10 МБ */
290    var fileInput = form.querySelector('[data-file]');
291    var fileName = form.querySelector('[data-file-name]');
292    if (fileInput && fileName) {
293      fileInput.addEventListener('change', function () {
294        var f = fileInput.files && fileInput.files[0];
295        fileName.classList.remove('is-invalid');
296        if (!f) { fileName.textContent = ''; return; }
297        if (f.size > 10 * 1024 * 1024) {
298          fileName.textContent = 'Файл больше 10 МБ, выберите файл поменьше';
299          fileName.classList.add('is-invalid');
300          fileInput.value = '';
301          return;
302        }
303        fileName.textContent = f.name;
304      });
305    }
306
307    function setError(field, on) { field.classList.toggle('is-invalid', on); }
308
309    function validate() {
310      var ok = true;
311      form.querySelectorAll('.field[data-required]').forEach(function (field) {
312        if (field.hidden) return;
313        var input = field.querySelector('input');
314        var value = (input.value || '').trim();
315        var bad = !value;
316        if (!bad && input.type === 'email') bad = !/^[^@\s]+@[^@\s]+\.[^@\s]{2,}$/.test(value);
317        if (!bad && input.type === 'tel') bad = value.replace(/\D/g, '').length < 11;
318        setError(field, bad);
319        if (bad && ok) input.focus();
320        if (bad) ok = false;
321      });
322      form.querySelectorAll('.field:not([data-required]) input[type="email"]').forEach(function (input) {
323        var v = (input.value || '').trim();
324        var bad = !!v && !/^[^@\s]+@[^@\s]+\.[^@\s]{2,}$/.test(v);
325        setError(input.closest('.field'), bad);
326        if (bad) ok = false;
327      });
328      var agree = form.querySelector('[data-agree]');
329      if (agree) {
330        var box = agree.querySelector('input');
331        agree.classList.toggle('is-invalid', !box.checked);
332        if (!box.checked) ok = false;
333      }
334      return ok;
335    }
336
337    form.querySelectorAll('.field input').forEach(function (input) {
338      input.addEventListener('input', function () {
339        var field = input.closest('.field');
340        if (field.classList.contains('is-invalid')) setError(field, false);
341      });
342    });
343    var agreeBox = form.querySelector('[data-agree] input');
344    if (agreeBox) agreeBox.addEventListener('change', function () {
345      if (agreeBox.checked) agreeBox.closest('[data-agree]').classList.remove('is-invalid');
346    });
347
348    /* Первый ввод в любое поле: шаг воронки между открытием и отправкой.
349       Слушатель одноразовый, иначе цель уйдёт на каждую нажатую клавишу. */
350    var started = false;
351    form.addEventListener('input', function () {
352      if (started) return;
353      started = true;
354      goal('form_start', {
355        form: form.getAttribute('data-form-type') || 'full',
356        page: location.pathname
357      });
358    });
359
360    form.addEventListener('submit', function (e) {
361      e.preventDefault();
362      if (status) { status.textContent = ''; status.classList.remove('is-error'); }
363      if (!validate()) {
364        if (status) status.textContent = 'Проверьте отмеченные поля.';
365        return;
366      }
367      submit.setAttribute('data-loading', 'true');
368      submit.disabled = true;
369
370      var type = form.getAttribute('data-form-type') || 'full';
371      var data = new FormData(form);
372      data.set('form_type', type);
373      data.set('page_url', location.href);
374      data.set('page_title', document.title);
375      data.set('service', SERVICE);
376      data.set('button_text', form.getAttribute('data-button-text') || (type === 'full' ? 'Отправить' : ''));
377      data.set('page', location.pathname);
378
379      send(data).then(function (res) {
380        if (!res || !res.ok) throw new Error((res && res.error) || 'send');
381        goal('form_' + type, {
382          service: SERVICE || '',
383          page: location.pathname,
384          topic: pickedTags(form),
385          form: type
386        });
387        if (hooks && hooks.done) hooks.done();
388        form.reset();
389        if (fileName) fileName.textContent = '';
390        if (ts) ts.value = String(Date.now());
391        if (status && !(hooks && hooks.done)) {
392          status.textContent = 'Спасибо за доверие! Менеджер свяжется с Вами в ближайшее время.';
393        }
394      }).catch(function () {
395        if (hooks && hooks.fail) { hooks.fail(); return; }
396        if (status) {
397          status.classList.add('is-error');
398          status.innerHTML = 'Не удалось отправить, позвоните нам: <a href="tel:+79237375033">8 (923) 737-50-33</a>';
399        }
400      }).then(function () {
401        submit.removeAttribute('data-loading');
402        submit.disabled = false;
403      });
404    });
405  }
406
407  function forms() {
408    document.querySelectorAll('[data-form]').forEach(function (form) {
409      if (form.closest('[data-mdl]')) return;          // форму в окне настраивает modal()
410      setupForm(form);
411    });
412  }
413
414  /* ------------------------------------------------------------ Окно с короткой формой */
415  function modal() {
416    var root = mdlRoot;
417    if (!root) return;
418    var card = root.querySelector('.mdl__card');
419    var body = root.querySelector('[data-mdl-body]');
420    var form = root.querySelector('[data-form]');
421    var title = root.querySelector('[data-mdl-title]');
422    var siteField = root.querySelector('[data-mdl-site]');
423    var formHtml = body.innerHTML;                     // исходное содержимое для повторного открытия
424    var trigger = null, closeTimer = null;
425    var reduced = window.matchMedia('(prefers-reduced-motion: reduce)');
426
427    function focusables() {
428      return Array.prototype.slice.call(card.querySelectorAll(
429        'a[href], button:not([disabled]), input:not([type="hidden"]):not([tabindex="-1"]), textarea, select'))
430        .filter(function (el) { return el.offsetParent !== null; });
431    }
432
433    function bindForm() {
434      form = root.querySelector('[data-form]');
435      title = root.querySelector('[data-mdl-title]');
436      siteField = root.querySelector('[data-mdl-site]');
437      setupForm(form, { done: showDone, fail: showFail });
438    }
439
440    function configure(kind, heading, buttonText) {
441      if (!form) bindForm();
442      form.setAttribute('data-form-type', kind);
443      form.setAttribute('data-button-text', buttonText || '');
444      title.textContent = kind === 'callback' ? 'Заказать звонок' : (heading || 'Оставить заявку');
445      siteField.hidden = kind === 'callback';
446      var status = form.querySelector('[data-status]');
447      if (status) { status.textContent = ''; status.classList.remove('is-error'); }
448      form.querySelectorAll('.is-invalid').forEach(function (el) { el.classList.remove('is-invalid'); });
449    }
450
451    /* Итог отправки заменяет содержимое окна; при следующем открытии форма возвращается */
452    function showDone() {
453      body.innerHTML = '<div class="mdl__done">'
454        + '<span class="mdl__done-ic" aria-hidden="true"><svg class="ic"><use href="#check"/></svg></span>'
455        + '<h2 class="mdl__title" id="mdl-title">Спасибо за доверие!</h2>'
456        + '<p>Менеджер свяжется с Вами в ближайшее время</p>'
457        + '<button class="btn btn--lg" type="button" data-mdl-close>Закрыть</button></div>';
458      form = null;
459      var btn = body.querySelector('[data-mdl-close]');
460      if (btn) btn.focus();
461    }
462    function showFail() {
463      var status = form && form.querySelector('[data-status]');
464      if (!status) return;
465      status.classList.add('is-error');
466      status.innerHTML = 'Не удалось отправить, позвоните нам: <a href="tel:+79237375033">8 (923) 737-50-33</a>';
467    }
468
469    function open(btn) {
470      trigger = btn;
471      clearTimeout(closeTimer);
472      if (!form) { body.innerHTML = formHtml; bindForm(); }
473      configure(btn.getAttribute('data-modal'), btn.getAttribute('data-title'), (btn.textContent || '').trim());
474      root.hidden = false;
475      root.setAttribute('data-open', 'true');
476      lockScroll();
477      /* кадр на применение исходного состояния, иначе переход не проиграется */
478      requestAnimationFrame(function () {
479        requestAnimationFrame(function () { root.setAttribute('data-in', 'true'); });
480      });
481      var first = form.querySelector('input:not([type="hidden"]):not([tabindex="-1"])');
482      setTimeout(function () { if (first) first.focus({ preventScroll: true }); }, reduced.matches ? 0 : 60);
483      goal('form_open', {
484        button: (btn.textContent || '').trim(),
485        page: location.pathname
486      });
487    }
488
489    function close() {
490      if (root.getAttribute('data-open') !== 'true') return;
491      root.setAttribute('data-in', 'false');
492      closeTimer = setTimeout(function () {
493        root.setAttribute('data-open', 'false');
494        root.hidden = true;
495        unlockScroll();
496        if (trigger && document.contains(trigger)) trigger.focus({ preventScroll: true });
497      }, reduced.matches ? 0 : 200);
498    }
499
500    bindForm();
501
502    document.addEventListener('click', function (e) {
503      var btn = e.target.closest('[data-modal]');
504      if (btn) { e.preventDefault(); open(btn); return; }
505      if (e.target.closest('[data-mdl-close]') && root.contains(e.target)) close();
506    });
507
508    document.addEventListener('keydown', function (e) {
509      if (root.getAttribute('data-open') !== 'true') return;
510      if (e.key === 'Escape') { e.preventDefault(); close(); return; }
511      if (e.key !== 'Tab') return;                     // фокус не уходит из окна
512      var f = focusables();
513      if (!f.length) return;
514      var first = f[0], last = f[f.length - 1];
515      if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
516      else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
517      else if (!card.contains(document.activeElement)) { e.preventDefault(); first.focus(); }
518    });
519  }
520
521  /* ------------------------------------------- Появление секций при скролле */
522  function reveal() {
523    var nodes = document.querySelectorAll('.reveal');
524    if (!nodes.length) return;
525    if (reduced.matches || !('IntersectionObserver' in window)) {
526      nodes.forEach(function (n) { n.classList.add('is-in'); });
527      return;
528    }
529    var io = new IntersectionObserver(function (entries) {
530      entries.forEach(function (entry) {
531        if (!entry.isIntersecting) return;
532        entry.target.classList.add('is-in');
533        io.unobserve(entry.target);
534      });
535    }, { rootMargin: '0px 0px -10% 0px', threshold: 0 });
536    /* threshold в долях не годится: у длинных блоков (политика, статьи) 8% высоты
537       больше экрана, и появление не срабатывало никогда */
538    nodes.forEach(function (n) { io.observe(n); });
539  }
540
541
542  /* ------------------------------------------------- Лайтбокс: письма и фото */
543  function lightbox() {
544    /* Групп на странице может быть несколько: письма, дипломы, факты.
545       Окно одно, список подставляем тот, по которому кликнули. */
546    var groups = Array.prototype.slice.call(document.querySelectorAll('[data-lightbox]'));
547    if (!groups.length) return;
548    var items = [];
549
550    var box = document.querySelector('[data-lb]');
551    if (!box) return;                 // на странице нет окна просмотра - выходим тихо
552    var frame = box.querySelector('.lb__frame');
553    var img = box.querySelector('.lb__img');
554    var count = box.querySelector('[data-lb-count]');
555    var prev = box.querySelector('[data-lb-prev]');
556    var next = box.querySelector('[data-lb-next]');
557    var closeBtn = box.querySelector('[data-lb-close]');
558    var bar = box.querySelector('.lb__bar');
559    var i = 0, lastFocus = null;
560
561    function show(n, instant) {
562      if (!items.length) return;
563      // листаем по кругу: за последним письмом идёт первое и наоборот
564      i = (n + items.length) % items.length;
565      var a = items[i];
566      var apply = function () {
567        img.src = a.dataset.lbSrc;
568        img.alt = a.dataset.lbAlt || '';
569        frame.classList.remove('is-swapping');
570      };
571      if (instant || reduced.matches) apply();
572      else { frame.classList.add('is-swapping'); setTimeout(apply, 130); }
573      count.textContent = (i + 1) + ' из ' + items.length;
574    }
575
576    function open(list, n) {
577      items = list;
578      /* У группы из одного изображения листать нечего */
579      if (bar) bar.hidden = items.length < 2;
580      lastFocus = document.activeElement;
581      show(n, true);
582      box.setAttribute('data-open', 'true');
583      lockScroll();
584      requestAnimationFrame(function () { box.setAttribute('data-in', 'true'); });
585      closeBtn.focus();
586    }
587    function close() {
588      box.setAttribute('data-in', 'false');
589      var done = function () {
590        box.setAttribute('data-open', 'false');
591        unlockScroll();
592        if (lastFocus) lastFocus.focus();
593      };
594      if (reduced.matches) done(); else setTimeout(done, 200);
595    }
596
597    groups.forEach(function (root) {
598      var list = Array.prototype.slice.call(root.querySelectorAll('a[data-lb-src]'));
599      list.forEach(function (a, n) {
600        a.addEventListener('click', function (e) { e.preventDefault(); open(list, n); });
601      });
602    });
603    prev.addEventListener('click', function () { show(i - 1); });
604    next.addEventListener('click', function () { show(i + 1); });
605    closeBtn.addEventListener('click', close);
606    box.querySelector('.lb__scrim').addEventListener('click', close);
607
608    document.addEventListener('keydown', function (e) {
609      if (box.getAttribute('data-open') !== 'true') return;
610      if (e.key === 'Escape') { e.preventDefault(); close(); }
611      if (e.key === 'ArrowLeft') show(i - 1);
612      if (e.key === 'ArrowRight') show(i + 1);
613      if (e.key === 'Tab') {                       // фокус не уходит из окна
614        var f = box.querySelectorAll('button:not([disabled])');
615        var first = f[0], last = f[f.length - 1];
616        if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
617        else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
618      }
619    });
620
621    /* Свайп: картинка идёт за пальцем один к одному, решение по скорости броска */
622    var x0 = 0, t0 = 0, dx = 0, dragging = false;
623    frame.addEventListener('pointerdown', function (e) {
624      if (e.pointerType === 'mouse') return;
625      dragging = true; x0 = e.clientX; t0 = e.timeStamp; dx = 0;
626      frame.setPointerCapture(e.pointerId);
627      frame.style.transition = 'none';
628    });
629    frame.addEventListener('pointermove', function (e) {
630      if (!dragging) return;
631      dx = e.clientX - x0;
632      frame.style.transform = 'translateX(' + dx.toFixed(1) + 'px)';
633    });
634    function release(e) {
635      if (!dragging) return;
636      dragging = false;
637      var v = Math.abs(dx) / Math.max(1, e.timeStamp - t0);
638      frame.style.transition = 'transform 260ms cubic-bezier(.23,1,.32,1)';
639      frame.style.transform = '';
640      if (Math.abs(dx) > 70 || v > .5) show(dx < 0 ? i + 1 : i - 1);
641      dx = 0;
642    }
643    frame.addEventListener('pointerup', release);
644    frame.addEventListener('pointercancel', release);
645  }
646
647  /* ------------------------------------------------- Дочитывание материала */
648  /* Цель read_75. Считаем по высоте самой статьи, а не всей страницы:
649     подвал, «читайте также» и форма внизу в счёт идти не должны. */
650  function readDepth() {
651    if (!/^\/(blog|issledovaniya)\/[^/]+\/?$/.test(location.pathname)) return;
652    var art = document.querySelector('.art__body');
653    if (!art) return;
654    var done = false;
655
656    function check() {
657      if (done) return;
658      var box = art.getBoundingClientRect();
659      if (box.height <= 0) return;
660      /* сколько статьи уже ушло выше нижней кромки экрана */
661      var seen = window.innerHeight - box.top;
662      if (seen / box.height >= 0.75) {
663        done = true;
664        window.removeEventListener('scroll', check);
665        goal('read_75', { page: location.pathname });
666      }
667    }
668
669    window.addEventListener('scroll', check, { passive: true });
670    check();
671  }
672
673  /* ----------------------------------------------------------- Заглушки */
674  function stubs() {
675    document.addEventListener('click', function (e) {
676      var a = e.target.closest('a[aria-disabled="true"]');
677      if (a) e.preventDefault();
678    });
679  }
680
681  function init() {
682    stickyHeader(); menu(); accordion(); reviews(); caseList();
683    forms(); modal(); lightbox(); reveal(); stubs(); readDepth();
684  }
685  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
686  else init();
687})();

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.