---
**main.js**
```javascript
/* ==========================================================================
Неделя Моды Поволжья 2026 — main.js
========================================================================== */
(function () {
'use strict';
/* ---------- DOM refs ---------- */
var header = document.querySelector('.site-header');
var burgerBtn = document.querySelector('.burger');
var mobileMenu = document.getElementById('mobileMenu');
var mobileLinks = mobileMenu ? mobileMenu.querySelectorAll('a') : [];
var particlesEl = document.getElementById('particles');
/* ---------- Mobile menu ---------- */
function toggleMenu() {
if (!burgerBtn || !mobileMenu) return;
var isOpen = mobileMenu.classList.contains('open');
burgerBtn.classList.toggle('active');
mobileMenu.classList.toggle('open');
document.body.style.overflow = isOpen ? '' : 'hidden';
}
if (burgerBtn) {
burgerBtn.addEventListener('click', toggleMenu);
}
for (var i = 0; i < mobileLinks.length; i++) {
mobileLinks[i].addEventListener('click', function () {
if (mobileMenu.classList.contains('open')) {
toggleMenu();
}
});
}
/* ---------- Header scroll state ---------- */
var lastScroll = 0;
var headerThreshold = 40;
function onScroll() {
var y = window.pageYOffset || document.documentElement.scrollTop;
if (y > headerThreshold) {
header.classList.add('scrolled');
} else {
header.classList.remove('scrolled');
}
lastScroll = y;
}
window.addEventListener('scroll', onScroll, { passive: true });
onScroll();
/* ---------- Smooth scroll for anchor links ---------- */
var allLinks = document.querySelectorAll('a[href^="#"]');
for (var j = 0; j < allLinks.length; j++) {
allLinks[j].addEventListener('click', function (e) {
var id = this.getAttribute('href');
if (!id || id === '#') return;
var target = document.querySelector(id);
if (target) {
e.preventDefault();
var offset = header.offsetHeight + 16;
var top = target.getBoundingClientRect().top + window.pageYOffset - offset;
window.scrollTo({ top: top, behavior: 'smooth' });
}
});
}
/* ---------- Scroll reveal (IntersectionObserver) ---------- */
var revealElements = document.querySelectorAll(
'.section-label, .section-title, .body-text, .btn-primary, .btn-ghost, ' +
'.about-image-frame, .about-stats, .card, .venue-card, .contact-card, ' +
'.model-cta-text, .model-cta-deco, .footer-inner, .hero-content > *'
);
/* Assign reveal classes */
for (var k = 0; k < revealElements.length; k++) {
revealElements[k].classList.add('reveal');
}
/* Stagger cards */
var cardGroups = document.querySelectorAll('.cards-grid, .contacts-grid, .about-stats');
for (var g = 0; g < cardGroups.length; g++) {
var children = cardGroups[g].children;
for (var c = 0; c < children.length; c++) {
children[c].classList.add('reveal', 'reveal-delay-' + (c + 1));
}
}
/* Hero content stagger */
var heroItems = document.querySelectorAll('.hero-content > *');
for (var h = 0; h < heroItems.length; h++) {
heroItems[h].classList.add('reveal', 'reveal-delay-' + (h + 1));
}
var prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if ('IntersectionObserver' in window && !prefersReducedMotion) {
var revealObserver = new IntersectionObserver(function (entries) {
for (var e = 0; e < entries.length; e++) {
if (entries[e].isIntersecting) {
entries[e].target.classList.add('visible');
revealObserver.unobserve(entries[e].target);
}
}
}, { threshold: 0.12, rootMargin: '0px 0px -40px 0px' });
for (var r = 0; r < revealElements.length; r++) {
revealObserver.observe(revealElements[r]);
}
} else {
/* Fallback: show everything immediately */
for (var f = 0; f < revealElements.length; f++) {
revealElements[f].classList.add('visible');
}
}
/* ---------- Hero particles ---------- */
if (particlesEl && !prefersReducedMotion) {
var canvas = document.createElement('canvas');
particlesEl.appendChild(canvas);
var ctx = canvas.getContext('2d');
var particles = [];
var particleCount = 50;
var raf;
function resizeCanvas() {
canvas.width = particlesEl.offsetWidth;
canvas.height = particlesEl.offsetHeight;
}
function Particle() {
this.reset(true);
}
Particle.prototype.reset = function (initial) {
this.x = Math.random() * canvas.width;
this.y = initial ? Math.random() * canvas.height : canvas.height + 10;
this.size = Math.random() * 2 + 0.5;
this.speedY = -(Math.random() * 0.4 + 0.15);
this.speedX = (Math.random() - 0.5) * 0.3;
this.opacity = Math.random() * 0.5 + 0.1;
this.fadeSpeed = Math.random() * 0.003 + 0.001;
this.phase = Math.random() * Math.PI * 2;
};
Particle.prototype.update = function () {
this.y += this.speedY;
this.x += this.speedX + Math.sin(this.phase) * 0.1;
this.phase += 0.01;
this.opacity -= this.fadeSpeed;
if (this.opacity <= 0 || this.y < -10) {
this.reset(false);
}
};
Particle.prototype.draw = function () {
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(200,169,110,' + this.opacity + ')';
ctx.fill();
};
function initParticles() {
resizeCanvas();
particles = [];
for (var p = 0; p < particleCount; p++) {
particles.push(new Particle());
}
}
function animateParticles() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (var p = 0; p < particles.length; p++) {
particles[p].update();
particles[p].draw();
}
raf = requestAnimationFrame(animateParticles);
}
initParticles();
animateParticles();
var resizeTimer;
window.addEventListener('resize', function () {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(function () {
resizeCanvas();
}, 200);
});
/* Pause when hidden */
document.addEventListener('visibilitychange', function () {
if (document.hidden) {
cancelAnimationFrame(raf);
} else {
animateParticles();
}
});
}
/* ---------- Stat counter animation ---------- */
var statNumbers = document.querySelectorAll('.stat-number');
if ('IntersectionObserver' in window && statNumbers.length && !prefersReducedMotion) {
var statObserver = new IntersectionObserver(function (entries) {
for (var e = 0; e < entries.length; e++) {
if (entries[e].isIntersecting) {
animateStat(entries[e].target);
statObserver.unobserve(entries[e].target);
}
}
}, { threshold: 0.5 });
for (var s = 0; s < statNumbers.length; s++) {
statObserver.observe(statNumbers[s]);
}
}
function animateStat(el) {
var text = el.textContent.trim();
var target = parseInt(text, 10);
if (isNaN(target)) return; /* skip non-numeric like ∞ */
var duration = 1200;
var start = performance.now();
function step(now) {
var progress = Math.min((now - start) / duration, 1);
var eased = 1 - Math.pow(1 - progress, 3);
el.textContent = Math.round(eased * target);
if (progress < 1) {
requestAnimationFrame(step);
}
}
el.textContent = '0';
requestAnimationFrame(step);
}
})();