Compare commits

...

7 Commits

Author SHA1 Message Date
Alexander Zaytsev
ce2e3e3870
Тут тоже для раскрытия чето тоже 2026-06-02 22:27:17 +03:00
Alexander Zaytsev
e1af430022
Добавил хуйню, шоб FAQ раскрывалось нормально на мобилках 2026-06-02 22:26:47 +03:00
Alexander Zaytsev
2a04f1cfaa
Добавил пару вопросов в FAQ 2026-06-02 22:22:32 +03:00
Alexander Zaytsev
3583d19d66
Merge pull request #2 from pvlnes/master
подсовываем мастер в ветку
2026-06-02 22:21:05 +03:00
c4b466d9a8 minor fix 2026-06-01 22:02:10 +03:00
45d7f7c2ad upd caddy 2026-06-01 21:47:31 +03:00
052880f5db burka 2026-06-01 21:42:59 +03:00
14 changed files with 2315 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
.DS_Store
*.MOV
*.mov

View File

@ -0,0 +1,494 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Александр и Юлия — 22 августа 2026</title>
<link rel="stylesheet" href="style.css" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,700;1,400&family=Nunito:wght@400;600;700&display=swap" rel="stylesheet" />
</head>
<body>
<!-- ========== НАВИГАЦИЯ ========== -->
<nav id="navbar">
<div class="nav-inner">
<a href="#hero" class="nav-logo">А&nbsp;&amp;&nbsp;Ю</a>
<button class="burger" id="burger" aria-label="Меню">&#9776;</button>
<ul class="nav-links" id="nav-links">
<li><a href="#video">Видео</a></li>
<li><a href="#gallery">Галерея</a></li>
<li><a href="#stats">История</a></li>
<li><a href="#venue">Место</a></li>
<li><a href="#rsvp">Анкета</a></li>
<li><a href="#faq">FAQ</a></li>
</ul>
</div>
</nav>
<!-- ========== ГЕРОЙ ========== -->
<section id="hero">
<div class="confetti-wrap" aria-hidden="true" id="confetti"></div>
<div class="hero-content">
<p class="hero-pre">Приглашаем вас на свадьбу</p>
<h1 class="hero-names">Александр<span class="amp">&amp;</span>Юлия</h1>
<p class="hero-date">22 августа 2026 16:00</p>
<div class="countdown" id="countdown">
<div class="countdown-block">
<span class="cd-num" id="cd-days">--</span>
<span class="cd-label">дней</span>
</div>
<div class="countdown-block">
<span class="cd-num" id="cd-hours">--</span>
<span class="cd-label">часов</span>
</div>
<div class="countdown-block">
<span class="cd-num" id="cd-minutes">--</span>
<span class="cd-label">минут</span>
</div>
<div class="countdown-block">
<span class="cd-num" id="cd-seconds">--</span>
<span class="cd-label">секунд</span>
</div>
</div>
</div>
<div class="hero-deco hero-deco--left" aria-hidden="true">🌸</div>
<div class="hero-deco hero-deco--right" aria-hidden="true">🌼</div>
</section>
<!-- ========== ВИДЕОПРИГЛАШЕНИЕ ========== -->
<section id="video" class="section section--alt">
<div class="container">
<h2 class="section-title">Видеоприглашение</h2>
<p class="section-sub">Александр и Юлия приглашают вас лично</p>
<div class="video-wrap">
<video
class="invite-video"
poster="poster.jpg"
controls
preload="none"
playsinline
>
<source src="invitation.mov" type="video/mp4" />
<source src="invitation.mov" type="video/quicktime" />
<p>Ваш браузер не поддерживает воспроизведение видео. <a href="invitation.mov">Скачать видео</a>.</p>
</video>
</div>
<div class="video-btn-wrap">
<a href="#rsvp" class="btn btn-primary">Заполнить анкету</a>
</div>
</div>
</section>
<!-- ========== ГАЛЕРЕЯ ========== -->
<section id="gallery" class="section section--alt">
<div class="container">
<h2 class="section-title">Галерея</h2>
<p class="section-sub">Вдруг вы забыли, как мы выглядим</p>
<div class="gallery-grid">
<button class="gallery-item" type="button" data-index="0" aria-label="Открыть фото 1">
<img src="photos/photo1.jpg" alt="Александр и Юлия" loading="lazy">
</button>
<button class="gallery-item" type="button" data-index="1" aria-label="Открыть фото 2">
<img src="photos/photo2.jpg" alt="Александр и Юлия с соком" loading="lazy">
</button>
<button class="gallery-item" type="button" data-index="2" aria-label="Открыть фото 3">
<img src="photos/photo3.jpg" alt="Александр и Юлия в машине" loading="lazy">
</button>
<button class="gallery-item" type="button" data-index="3" aria-label="Открыть фото 4">
<img src="photos/photo4.jpg" alt="Александр и Юлия в салоне" loading="lazy">
</button>
<button class="gallery-item" type="button" data-index="4" aria-label="Открыть фото 5">
<img src="photos/photo5.jpg" alt="Александр и Юлия на турнике" loading="lazy">
</button>
<button class="gallery-item" type="button" data-index="5" aria-label="Открыть фото 6">
<img src="photos/photo6.jpg" alt="Александр и Юлия у машины" loading="lazy">
</button>
<button class="gallery-item" type="button" data-index="6" aria-label="Открыть фото 7">
<img src="photos/photo7.jpg" alt="Александр и Юлия на качелях" loading="lazy">
</button>
</div>
</div>
</section>
<!-- ========== ИСТОРИЯ В ЦИФРАХ ========== -->
<section id="stats" class="section">
<div class="container">
<h2 class="section-title">8 лет отношений</h2>
<p class="section-sub">Но вам мы покажем преимущественно последние 4 года переписки</p>
<!-- Ключевые цифры -->
<div class="stats-grid">
<div class="stat-card">
<span class="stat-value">46 572</span>
<span class="stat-label">сообщений за 7 лет</span>
</div>
<div class="stat-card">
<span class="stat-value">38×</span>
<span class="stat-label">рост переписки в 2022</span>
</div>
<div class="stat-card">
<span class="stat-value">618</span>
<span class="stat-label">сердечных эмодзи</span>
</div>
<div class="stat-card">
<span class="stat-value">42</span>
<span class="stat-label">раза написали «люблю»</span>
</div>
</div>
<!-- Кто пишет больше -->
<div class="stats-who">
<div class="stats-who-labels">
<span class="stats-who-name who-alex">Александр — 51.8%</span>
<span class="stats-who-name who-yulia">Юля — 48.2%</span>
</div>
<div class="stats-usage-bar">
<div class="usage-seg usage-alex" style="width:51.8%"></div>
<div class="usage-seg usage-yulia" style="width:48.2%"></div>
</div>
<p class="stats-note">Разрыв — 1&nbsp;688 сообщений за 7 лет. Почти идеальный баланс.</p>
</div>
<!-- График по годам -->
<h3 class="stats-chart-title">Сообщений по годам</h3>
<div class="stats-chart-wrap">
<svg class="stats-chart" viewBox="0 0 760 240" preserveAspectRatio="xMidYMid meet" aria-label="Активность по годам">
<!-- Горизонтальные линии сетки -->
<line x1="48" y1="20" x2="748" y2="20" stroke="currentColor" stroke-opacity=".1" stroke-width="1"/>
<line x1="48" y1="70" x2="748" y2="70" stroke="currentColor" stroke-opacity=".1" stroke-width="1"/>
<line x1="48" y1="120" x2="748" y2="120" stroke="currentColor" stroke-opacity=".1" stroke-width="1"/>
<line x1="48" y1="170" x2="748" y2="170" stroke="currentColor" stroke-opacity=".1" stroke-width="1"/>
<line x1="48" y1="210" x2="748" y2="210" stroke="currentColor" stroke-opacity=".15" stroke-width="1"/>
<!-- Подписи оси Y -->
<text x="42" y="24" text-anchor="end" class="chart-label">12К</text>
<text x="42" y="74" text-anchor="end" class="chart-label">9К</text>
<text x="42" y="124" text-anchor="end" class="chart-label">6К</text>
<text x="42" y="174" text-anchor="end" class="chart-label">3К</text>
<text x="42" y="214" text-anchor="end" class="chart-label">0</text>
<!-- Данные: 2019(307), 2020(215), 2021(205), 2022(7902), 2023(10774), 2024(9542), 2025(11639), 2026*(6093) -->
<!-- Масштаб: 12000 → 190px (y базовая = 210) -->
<!-- Ширина группы: (748-48)/8 = 87.5, bar width = 34, gap между alex/yulia = 2 -->
<!-- 2019: alex=159(2.5px), yulia=148(2.3px) -->
<rect x="62" y="207.5" width="34" height="2.5" fill="var(--blue-bar)" rx="2"/>
<rect x="62" y="210" width="34" height="0" fill="var(--purple-bar)" rx="0"/>
<g>
<rect x="62" y="207.5" width="34" height="2.5" fill="var(--blue-bar)" rx="2" class="bar-alex"/>
<rect x="62" y="205.1" width="34" height="2.3" fill="var(--purple-bar)" rx="2" class="bar-yulia"/>
</g>
<text x="79" y="228" text-anchor="middle" class="chart-label">2019</text>
<!-- 2020: 215 → 3.4px -->
<g>
<rect x="149" y="208.2" width="34" height="1.8" fill="var(--blue-bar)" rx="2" class="bar-alex"/>
<rect x="149" y="206.5" width="34" height="1.6" fill="var(--purple-bar)" rx="2" class="bar-yulia"/>
</g>
<text x="166" y="228" text-anchor="middle" class="chart-label">2020</text>
<!-- 2021: 205 → 3.2px -->
<g>
<rect x="236" y="208.3" width="34" height="1.7" fill="var(--blue-bar)" rx="2" class="bar-alex"/>
<rect x="236" y="206.7" width="34" height="1.6" fill="var(--purple-bar)" rx="2" class="bar-yulia"/>
</g>
<text x="253" y="228" text-anchor="middle" class="chart-label">2021</text>
<!-- 2022: 7902 → 125.2px. alex=4093(64.8), yulia=3809(60.3) -->
<g>
<rect x="323" y="145.2" width="34" height="64.8" fill="var(--blue-bar)" rx="2" class="bar-alex"/>
<rect x="323" y="84.9" width="34" height="60.3" fill="var(--purple-bar)" rx="2" class="bar-yulia"/>
</g>
<text x="340" y="228" text-anchor="middle" class="chart-label">2022</text>
<!-- 2023: 10774 → 170.6px. alex=5581(88.4), yulia=5193(82.2) -->
<g>
<rect x="410" y="121.6" width="34" height="88.4" fill="var(--blue-bar)" rx="2" class="bar-alex"/>
<rect x="410" y="39.4" width="34" height="82.2" fill="var(--purple-bar)" rx="2" class="bar-yulia"/>
</g>
<text x="427" y="228" text-anchor="middle" class="chart-label">2023</text>
<!-- 2024: 9542 → 151.1px. alex=4943(78.3), yulia=4599(72.8) -->
<g>
<rect x="497" y="131.7" width="34" height="78.3" fill="var(--blue-bar)" rx="2" class="bar-alex"/>
<rect x="497" y="58.9" width="34" height="72.8" fill="var(--purple-bar)" rx="2" class="bar-yulia"/>
</g>
<text x="514" y="228" text-anchor="middle" class="chart-label">2024</text>
<!-- 2025: 11639 → 184.3px. alex=6029(95.5), yulia=5610(88.8) -->
<g>
<rect x="584" y="114.5" width="34" height="95.5" fill="var(--blue-bar)" rx="2" class="bar-alex"/>
<rect x="584" y="25.7" width="34" height="88.8" fill="var(--purple-bar)" rx="2" class="bar-yulia"/>
</g>
<text x="601" y="228" text-anchor="middle" class="chart-label">2025</text>
<!-- 2026*: 6093 → 96.5px. alex=3156(50.0), yulia=2937(46.5) -->
<g>
<rect x="671" y="160.0" width="34" height="50.0" fill="var(--blue-bar)" rx="2" class="bar-alex"/>
<rect x="671" y="113.5" width="34" height="46.5" fill="var(--purple-bar)" rx="2" class="bar-yulia"/>
</g>
<text x="688" y="228" text-anchor="middle" class="chart-label">2026*</text>
</svg>
<!-- Легенда -->
<div class="chart-legend">
<span class="legend-dot legend-alex"></span><span>Александр</span>
<span class="legend-dot legend-yulia"></span><span>Юля</span>
</div>
<p class="stats-note">* 2026 — данные за январь–май. В 2022 переписка выросла в 38 раз.</p>
</div>
<!-- Два столбца: медиа + слова -->
<div class="stats-two-col">
<div class="stats-col">
<h3 class="stats-col-title">Что отправляли</h3>
<div class="stats-bars">
<div class="stats-bar-row">
<span class="stats-bar-label">Фотографии</span>
<div class="stats-bar-track"><div class="stats-bar-fill" style="width:100%">2 267</div></div>
</div>
<div class="stats-bar-row">
<span class="stats-bar-label">Стикеры</span>
<div class="stats-bar-track"><div class="stats-bar-fill" style="width:52.3%">1 185</div></div>
</div>
<div class="stats-bar-row">
<span class="stats-bar-label">Видео</span>
<div class="stats-bar-track"><div class="stats-bar-fill" style="width:9.9%">225</div></div>
</div>
<div class="stats-bar-row">
<span class="stats-bar-label">Голосовые</span>
<div class="stats-bar-track"><div class="stats-bar-fill" style="width:3.1%">70</div></div>
</div>
</div>
</div>
<div class="stats-col">
<h3 class="stats-col-title">Популярные слова</h3>
<div class="stats-bars">
<div class="stats-bar-row">
<span class="stats-bar-label">Ок / Хорошо</span>
<div class="stats-bar-track"><div class="stats-bar-fill stats-bar-fill--alt" style="width:100%">2 076</div></div>
</div>
<div class="stats-bar-row">
<span class="stats-bar-label">Смех (ахаха…)</span>
<div class="stats-bar-track"><div class="stats-bar-fill stats-bar-fill--alt" style="width:92.9%">1 928</div></div>
</div>
<div class="stats-bar-row">
<span class="stats-bar-label">Доброй ночи…</span>
<div class="stats-bar-track"><div class="stats-bar-fill stats-bar-fill--alt" style="width:19.0%">394</div></div>
</div>
<div class="stats-bar-row">
<span class="stats-bar-label">«Люблю»</span>
<div class="stats-bar-track"><div class="stats-bar-fill stats-bar-fill--alt" style="width:2.0%">42</div></div>
</div>
</div>
</div>
</div>
<!-- Нецензурная лексика -->
<div class="swear-block">
<h3 class="stats-chart-title" style="margin-bottom:1rem">🤬 Нецензурная лексика</h3>
<div class="swear-total-row">
<div class="swear-total-card">
<span class="swear-total-num">1 797</span>
<span class="swear-total-label">сообщений с матом за 7 лет</span>
</div>
<div class="swear-winner">
<span class="swear-winner-crown">👑</span>
<span class="swear-winner-name">Александр</span>
<span class="swear-winner-sub">матерится в 3.8× чаще</span>
</div>
</div>
<div class="stats-who" style="margin-top:1rem">
<div class="stats-who-labels">
<span class="stats-who-name who-alex">Александр — 79.2% (1 424 сообщ.)</span>
<span class="stats-who-name who-yulia">Юля — 20.8% (373 сообщ.)</span>
</div>
<div class="stats-usage-bar">
<div class="usage-seg usage-alex" style="width:79.2%"></div>
<div class="usage-seg usage-yulia" style="width:20.8%"></div>
</div>
</div>
</div>
</div>
</section>
<!-- ========== МЕСТО ========== -->
<section id="venue" class="section">
<div class="container">
<h2 class="section-title">Место проведения</h2>
<div class="venue-wrap">
<div class="venue-info">
<div class="venue-icon">📍</div>
<h3 class="venue-name">Загородный клуб «ЗаВидное»</h3>
<p class="venue-desc">Московская область, Ленинский район, пос. Измайлово д. 24 — 1 км от МКАД. Мы будем рады видеть вас!</p>
<div class="venue-btns">
<a href="https://zavidnoe-club.com/contacts" target="_blank" rel="noopener" class="btn btn-primary">
Сайт клуба
</a>
<a href="https://yandex.ru/maps/-/CPDRABzn" target="_blank" rel="noopener" class="btn btn-outline">
Яндекс.Карты
</a>
</div>
</div>
<div class="venue-map">
<iframe
src="https://yandex.ru/map-widget/v1/?um=constructor%3A88709e287fd710196ff3105f292a89bdf59222c1d431ddebdead0ebfe5fc06bb&source=constructor"
id="ymap-frame"
title="Карта места проведения"
allowfullscreen
loading="lazy"
></iframe>
<div class="map-fallback" id="map-fallback">
<p>Нажмите, чтобы открыть карту</p>
<a href="https://yandex.ru/maps/-/CPDRABzn" target="_blank" rel="noopener" class="btn btn-primary">
Открыть карту
</a>
</div>
</div>
</div>
</div>
</section>
<!-- ========== АНКЕТА ГОСТЯ ========== -->
<section id="rsvp" class="section">
<div class="container">
<h2 class="section-title">Анкета гостя</h2>
<p class="section-sub">Пожалуйста, ответьте до <strong>1 августа 2026</strong></p>
<!--
ИНСТРУКЦИЯ: Создайте форму на forms.google.com
Нажмите «Отправить» → значок «<>» (встроить) → скопируйте ссылку из атрибута src
и вставьте её вместо YOUR_GOOGLE_FORM_EMBED_URL ниже.
Пример URL: https://docs.google.com/forms/d/e/XXXXXXXXXX/viewform?embedded=true
-->
<div class="gform-wrap">
<iframe
id="gform-frame"
src="https://forms.gle/xoyDUDQZ2ZcM5wth7"
title="Анкета гостя"
frameborder="0"
marginheight="0"
marginwidth="0"
loading="lazy"
>Загрузка…</iframe>
</div>
</div>
</section>
<!-- ========== FAQ ========== -->
<section id="faq" class="section section--alt">
<div class="container">
<h2 class="section-title">Часто задаваемые вопросы</h2>
<div class="faq-list">
<div class="faq-item">
<button class="faq-question" aria-expanded="false">
Когда начинается мероприятие?
<span class="faq-arrow"></span>
</button>
<div class="faq-answer">
<p>Велком секция (фуршет) начинается в <strong>16:00</strong>.</p>
<p>Основная секция (банкет) начинается в <strong>17:00</strong>.</p>
</div>
</div>
<div class="faq-item">
<button class="faq-question" aria-expanded="false">
Какой дресс-код мероприятия?
<span class="faq-arrow"></span>
</button>
<div class="faq-answer">
<p>Для нас важно, чтобы гости выглядели нарядно.</p>
<p><strong>Мужчинам</strong> — классический костюм. Брюки, рубашка и туфли — вполне достаточно.</p>
<p><strong>Женщинам</strong> — платье или элегантный наряд.</p>
<p>По цвету — мы не ограничиваем вас в выборе цвета наряда на свадьбу. Если вам тяжело выбрать цвет, ниже представлены примеры цветов, которые будет максимально уместно надеть.</p>
<div class="dresscode-colors">
<span class="dresscode-chip"><span class="dresscode-dot" style="background:#1c2d5e"></span>Тёмно-синий</span>
<span class="dresscode-chip"><span class="dresscode-dot" style="background:#4a4a4a"></span>Графит</span>
<span class="dresscode-chip"><span class="dresscode-dot" style="background:#b8bfc9"></span>Светло-серый</span>
<span class="dresscode-chip"><span class="dresscode-dot" style="background:#708090"></span>Серо-стальной</span>
<span class="dresscode-chip"><span class="dresscode-dot" style="background:#6b4226"></span>Коричневый</span>
<span class="dresscode-chip"><span class="dresscode-dot" style="background:#7d1128"></span>Бордовый</span>
<span class="dresscode-chip"><span class="dresscode-dot" style="background:#2d7a5a"></span>Изумрудный</span>
</div>
</div>
</div>
<div class="faq-item">
<button class="faq-question" aria-expanded="false">
Есть ли парковка на месте?
<span class="faq-arrow"></span>
</button>
<div class="faq-answer">
<p>Да, рядом с местом проведения есть бесплатная парковка. Подробности — на карте.</p>
</div>
</div>
<div class="faq-item">
<button class="faq-question" aria-expanded="false">
До какого числа нужно подтвердить участие?
<span class="faq-arrow"></span>
</button>
<div class="faq-answer">
<p>Пожалуйста, заполните анкету гостя до <strong>20 июля 2026</strong>. Это поможет нам правильно рассадить гостей и подготовить меню.</p>
</div>
</div>
<div class="faq-item">
<button class="faq-question" aria-expanded="false">
Дарить деньги - это OK?
<span class="faq-arrow"></span>
</button>
<div class="faq-answer">
<p>Да, конечно. Мы будем рады любому подарку. Ведь единственный пункт в нашем вишлисте - это отлично проведенный праздник</p>
</div>
</div>
<div class="faq-item">
<button class="faq-question" aria-expanded="false">
Уместно ли дарить цветы?
<span class="faq-arrow"></span>
</button>
<div class="faq-answer">
<p>Мы бы попросили вас воздержаться от дарения цветов.</p>
<p>Потому что живем мы в небольшой квартире, и складывать такое потенциальное количество цветов нам банально будет некуда.</p>
</div>
</div>
</div>
</div>
</section>
<!-- ========== КООРДИНАТОР ========== -->
<section id="coordinator" class="section">
<div class="container">
<div class="coordinator-card">
<div class="coordinator-avatar">
<img src="photos/photo8.jpg" alt="Анастасия — координатор свадьбы">
</div>
<div class="coordinator-info">
<h2 class="coordinator-title">Остались вопросы?</h2>
<p class="coordinator-text">Любые вопросы по мероприятию, на которые мы не ответили, вы можете направить Анастасии — нашему координатору свадьбы.</p>
<p class="coordinator-phone">
Телефон: <a href="tel:+79586356872">8-958-635-68-72</a>
</p>
<a href="https://t.me/napartyev" target="_blank" rel="noopener" class="btn btn-primary coordinator-btn">
<span class="tg-icon"></span> Написать в Telegram
</a>
</div>
</div>
</div>
</section>
<!-- ========== ФУТЕР ========== -->
<footer class="footer">
<div class="footer-hearts" aria-hidden="true">💛💚💜💙❤️</div>
<p>С любовью, <strong>Александр и Юлия</strong> · 22 августа 2026</p>
<p class="footer-small">Сделано с ❤️ для нашего особенного дня</p>
</footer>
<div class="lightbox" id="lightbox" hidden aria-hidden="true">
<button class="lightbox-close" type="button" aria-label="Закрыть">×</button>
<button class="lightbox-prev" type="button" aria-label="Предыдущее фото"></button>
<img class="lightbox-img" src="" alt="">
<button class="lightbox-next" type="button" aria-label="Следующее фото"></button>
</div>
<script src="script.js"></script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 602 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@ -0,0 +1,729 @@
/* =============================================
WEDDING SITE АЛЕКСАНДР & ЮЛИЯ
============================================= */
// ---- Обратный отсчёт ----
const WEDDING_DATE = new Date('2026-08-22T16:00:00+03:00');
function updateCountdown() {
const now = new Date();
const diff = WEDDING_DATE - now;
if (diff <= 0) {
document.getElementById('countdown').innerHTML =
'<p style="font-size:1.5rem;font-weight:700;color:var(--purple)">🎉 Сегодня наша свадьба!</p>';
return;
}
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((diff % (1000 * 60)) / 1000);
document.getElementById('cd-days').textContent = String(days);
document.getElementById('cd-hours').textContent = String(hours).padStart(2, '0');
document.getElementById('cd-minutes').textContent = String(minutes).padStart(2, '0');
document.getElementById('cd-seconds').textContent = String(seconds).padStart(2, '0');
}
updateCountdown();
setInterval(updateCountdown, 1000);
// ---- Конфетти ----
const CONFETTI_COLORS = ['#FFD166', '#06D6A0', '#9B5DE5', '#F15BB5', '#00BBF9', '#FF6B6B'];
const CONFETTI_SHAPES = ['border-radius:2px', 'border-radius:50%', 'clip-path:polygon(50% 0%,0% 100%,100% 100%)'];
// Фоновый дождь конфетти на герое
(function spawnConfetti() {
const container = document.getElementById('confetti');
for (let i = 0; i < 60; i++) {
const el = document.createElement('div');
el.className = 'confetti-piece';
const size = Math.random() * 8 + 6;
el.style.cssText = `
left: ${Math.random() * 100}%;
width: ${size}px;
height: ${size}px;
background: ${CONFETTI_COLORS[Math.floor(Math.random() * CONFETTI_COLORS.length)]};
${CONFETTI_SHAPES[Math.floor(Math.random() * CONFETTI_SHAPES.length)]};
animation-duration: ${Math.random() * 6 + 5}s;
animation-delay: ${Math.random() * 8}s;
`;
container.appendChild(el);
}
})();
// Взрыв конфетти из заданной точки
function burstConfetti(x, y, count = 40) {
for (let i = 0; i < count; i++) {
const el = document.createElement('div');
el.className = 'confetti-burst';
const size = Math.random() * 9 + 5;
const angle = Math.random() * 360;
const dist = Math.random() * 160 + 60;
const color = CONFETTI_COLORS[Math.floor(Math.random() * CONFETTI_COLORS.length)];
const shape = CONFETTI_SHAPES[Math.floor(Math.random() * CONFETTI_SHAPES.length)];
const dur = Math.random() * 600 + 700;
el.style.cssText = `
position: fixed;
left: ${x}px;
top: ${y}px;
width: ${size}px;
height: ${size}px;
background: ${color};
${shape};
pointer-events: none;
z-index: 9999;
transform: translate(-50%, -50%);
animation: burst ${dur}ms ease-out forwards;
--dx: ${Math.cos(angle * Math.PI / 180) * dist}px;
--dy: ${Math.sin(angle * Math.PI / 180) * dist}px;
`;
document.body.appendChild(el);
setTimeout(() => el.remove(), dur + 50);
}
}
// Взрыв при клике в любом месте страницы
document.addEventListener('click', (e) => {
burstConfetti(e.clientX, e.clientY, 35);
});
// ===== ДИСКО ШАР =====
(function initDiscoBall() {
const BALL_R = 45; // радиус шара (в 3 раза больше прежнего)
const ROPE_LEN = 18; // верёвка от низа шапки до верха шара
const WIRE_MAX = 26; // максимальная длина провода прожектора
const TILE_S = 7; // размер зеркального тайла
const TILE_G = 1; // зазор между тайлами
const LERP_SPD = 0.10; // скорость выезда/уезда прожектора (0..1)
const DISC_COLORS = ['#ff6b9d','#ffd166','#06d6a0','#00bbf9','#9b5de5','#ffffff','#ff9f43','#a8e6cf'];
const cvs = document.createElement('canvas');
cvs.id = 'disco-canvas';
cvs.style.cssText = 'position:fixed;top:0;left:0;pointer-events:none;z-index:998;';
document.body.appendChild(cvs);
const ctx = cvs.getContext('2d');
let CW = window.innerWidth, CH = 280;
cvs.width = CW; cvs.height = CH;
let rot = 0;
let glints = []; // разлетающиеся блики
let ballPos = null;
// Для каждой нав-ссылки храним прогресс выезда (0 = спрятан, 1 = полностью выехал)
// и флаг — стреляли ли мы уже бликами при этом появлении
const projState = new Map(); // link → { progress, target, glintFired }
function refreshPos() {
const logo = document.querySelector('.nav-logo');
const nav = document.getElementById('navbar');
if (!logo || !nav) return;
const lr = logo.getBoundingClientRect();
const nh = nav.getBoundingClientRect().height;
ballPos = {
x: lr.left + lr.width / 2,
ropeTopY: nh,
ballY: nh + ROPE_LEN + BALL_R,
navH: nh,
};
}
window.addEventListener('resize', () => { CW = cvs.width = window.innerWidth; refreshPos(); });
refreshPos();
// Инициализируем состояние для каждой ссылки
document.querySelectorAll('.nav-links a').forEach(link => {
projState.set(link, { progress: 0, target: 0, glintFired: false });
link.addEventListener('mouseenter', e => {
const s = projState.get(e.currentTarget);
if (s) { s.target = 1; }
});
link.addEventListener('mouseleave', e => {
const s = projState.get(e.currentTarget);
if (s) { s.target = 0; s.glintFired = false; }
});
});
// ---- корпус прожектора Fresnel в локальных координатах (линза смотрит вправо) ----
function drawFresnelBody() {
const BW = 46, BH = 32, FW = 20, LR = 11;
const FLAP_L = 22, FLAP_T = 5.5;
ctx.shadowColor = 'rgba(0,0,0,.7)';
ctx.shadowBlur = 14;
// ── Шторки-барндоры (рисуем ДО корпуса — они «выходят» из него) ─
// Верхняя шторка
ctx.save();
ctx.translate(BW / 2 - FW * 0.5, -BH / 2);
ctx.rotate(-0.52);
ctx.fillStyle = '#161616';
ctx.strokeStyle = '#353535';
ctx.lineWidth = 0.7;
ctx.beginPath();
ctx.rect(-3, -FLAP_T / 2, FLAP_L + 3, FLAP_T);
ctx.fill(); ctx.stroke();
// Шарнир
ctx.fillStyle = '#444'; ctx.strokeStyle = '#666'; ctx.lineWidth = 0.5;
ctx.beginPath(); ctx.arc(0, 0, 2.5, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
// Рёбра жёсткости
for (let i = 6; i < FLAP_L; i += 7) {
ctx.strokeStyle = '#0e0e0e'; ctx.lineWidth = 0.6;
ctx.beginPath(); ctx.moveTo(i, -FLAP_T / 2 + 1); ctx.lineTo(i, FLAP_T / 2 - 1); ctx.stroke();
}
ctx.restore();
// Нижняя шторка
ctx.save();
ctx.translate(BW / 2 - FW * 0.5, BH / 2);
ctx.rotate(0.52);
ctx.fillStyle = '#161616';
ctx.strokeStyle = '#353535'; ctx.lineWidth = 0.7;
ctx.beginPath(); ctx.rect(-3, -FLAP_T / 2, FLAP_L + 3, FLAP_T);
ctx.fill(); ctx.stroke();
ctx.fillStyle = '#444'; ctx.strokeStyle = '#666'; ctx.lineWidth = 0.5;
ctx.beginPath(); ctx.arc(0, 0, 2.5, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
for (let i = 6; i < FLAP_L; i += 7) {
ctx.strokeStyle = '#0e0e0e'; ctx.lineWidth = 0.6;
ctx.beginPath(); ctx.moveTo(i, -FLAP_T / 2 + 1); ctx.lineTo(i, FLAP_T / 2 - 1); ctx.stroke();
}
ctx.restore();
// Боковая шторка (правая, видна при боковом ракурсе)
ctx.save();
ctx.translate(BW / 2, 0);
ctx.rotate(-0.14);
ctx.fillStyle = '#1a1a1a'; ctx.strokeStyle = '#2e2e2e'; ctx.lineWidth = 0.7;
ctx.beginPath(); ctx.rect(0, -BH / 2 - 1, FLAP_L * 0.55, BH + 2);
ctx.fill(); ctx.stroke();
ctx.restore();
// ── Основной корпус ───────────────────────────────────────────
// Внешний кант (тень)
ctx.fillStyle = '#070707';
ctx.fillRect(-BW / 2 - 1.5, -BH / 2 - 1.5, BW + 3, BH + 3);
// Корпус с градиентом
const bg = ctx.createLinearGradient(-BW / 2, -BH / 2, -BW / 2, BH / 2);
bg.addColorStop(0, '#2f2f2f');
bg.addColorStop(0.42, '#1d1d1d');
bg.addColorStop(1, '#111');
ctx.fillStyle = bg;
ctx.fillRect(-BW / 2, -BH / 2, BW, BH);
// Металлическая фаска сверху
ctx.fillStyle = 'rgba(255,255,255,.08)';
ctx.fillRect(-BW / 2, -BH / 2, BW, 2.5);
// Тень снизу
ctx.fillStyle = 'rgba(0,0,0,.18)';
ctx.fillRect(-BW / 2, BH / 2 - 2.5, BW, 2.5);
// Задняя затемнённая секция (радиатор охлаждения)
ctx.fillStyle = 'rgba(0,0,0,.28)';
ctx.fillRect(-BW / 2, -BH / 2, BW * 0.22, BH);
// Рёбра радиатора
ctx.strokeStyle = '#0c0c0c'; ctx.lineWidth = 1.2;
for (let i = 0; i < 3; i++) {
const sx = -BW / 2 + 4 + i * 4.5;
ctx.beginPath(); ctx.moveTo(sx, -BH / 2 + 5); ctx.lineTo(sx, BH / 2 - 5); ctx.stroke();
}
// Вентиляционные прорези (средняя часть корпуса)
ctx.strokeStyle = '#0a0a0a'; ctx.lineWidth = 1.8;
for (let i = 0; i < 4; i++) {
const sx = -BW / 2 + 17 + i * 6;
if (sx > BW / 2 - FW - 5) break;
ctx.beginPath(); ctx.moveTo(sx, -BH / 2 + 6); ctx.lineTo(sx, BH / 2 - 6); ctx.stroke();
}
// Логотип / шильдик на корпусе
ctx.fillStyle = 'rgba(255,255,255,.04)';
ctx.fillRect(-BW / 2 + 18, BH / 2 - 9, 14, 5);
// Ручка/рукоятка сверху корпуса
ctx.fillStyle = '#252525'; ctx.strokeStyle = '#3e3e3e'; ctx.lineWidth = 0.9;
ctx.beginPath(); ctx.rect(-10, -BH / 2 - 10, 20, 10); ctx.fill(); ctx.stroke();
// Насечки на ручке
ctx.strokeStyle = '#4a4a4a'; ctx.lineWidth = 0.55;
for (let i = -7; i <= 7; i += 3.5) {
ctx.beginPath(); ctx.moveTo(i, -BH / 2 - 9); ctx.lineTo(i, -BH / 2 - 2); ctx.stroke();
}
// ── Передняя секция (корпус линзы + рамка шторок) ─────────────
ctx.fillStyle = '#0d0d0d';
ctx.fillRect(BW / 2 - FW, -BH / 2, FW, BH);
// Квадратный фланец под шторки
ctx.strokeStyle = '#2c2c2c'; ctx.lineWidth = 2;
ctx.strokeRect(BW / 2 - FW + 2, -BH / 2 + 2, FW - 4, BH - 4);
// Болты фланца
[
[BW / 2 - FW + 5, -BH / 2 + 5], [BW / 2 - FW + 5, BH / 2 - 5],
[BW / 2 - 4, -BH / 2 + 5], [BW / 2 - 4, BH / 2 - 5],
].forEach(([bx2, by2]) => {
ctx.beginPath(); ctx.arc(bx2, by2, 1.8, 0, Math.PI * 2);
ctx.fillStyle = '#404040'; ctx.fill();
ctx.strokeStyle = '#666'; ctx.lineWidth = 0.5; ctx.stroke();
// Крест болта
ctx.strokeStyle = '#555'; ctx.lineWidth = 0.4;
ctx.beginPath(); ctx.moveTo(bx2 - 1.2, by2); ctx.lineTo(bx2 + 1.2, by2); ctx.stroke();
ctx.beginPath(); ctx.moveTo(bx2, by2 - 1.2); ctx.lineTo(bx2, by2 + 1.2); ctx.stroke();
});
// ── Линза Fresnel ─────────────────────────────────────────────
const lx = BW / 2 - FW / 2, ly = 0;
// Чёрная оправа линзы
ctx.beginPath(); ctx.arc(lx, ly, LR + 3.5, 0, Math.PI * 2);
ctx.fillStyle = '#060606'; ctx.fill();
ctx.strokeStyle = '#424242'; ctx.lineWidth = 1.3; ctx.stroke();
// Внутренняя тень оправы
ctx.beginPath(); ctx.arc(lx, ly, LR + 2, 0, Math.PI * 2);
ctx.strokeStyle = '#1a1a1a'; ctx.lineWidth = 1; ctx.stroke();
// Концентрические кольца Fresnel (физическая текстура линзы)
for (let ri = LR - 0.5; ri > 1.5; ri -= 2.3) {
ctx.beginPath(); ctx.arc(lx, ly, ri, 0, Math.PI * 2);
const alpha = 0.05 + (LR - ri) * 0.05;
ctx.strokeStyle = `rgba(255,200,60,${alpha})`;
ctx.lineWidth = 1;
ctx.stroke();
}
// Свечение линзы — тёплый вольфрамовый цвет
const lg = ctx.createRadialGradient(lx - 2.5, ly - 2.5, 0, lx, ly, LR + 3.5);
lg.addColorStop(0, 'rgba(255,255,230,1)');
lg.addColorStop(0.22, 'rgba(255,230,140,.97)');
lg.addColorStop(0.55, 'rgba(255,150,20,.78)');
lg.addColorStop(0.82, 'rgba(210,60,0,.32)');
lg.addColorStop(1, 'rgba(150,30,0,0)');
ctx.beginPath(); ctx.arc(lx, ly, LR + 3.5, 0, Math.PI * 2);
ctx.fillStyle = lg; ctx.fill();
// Зеркальный блик на линзе
ctx.beginPath(); ctx.arc(lx - 4, ly - 4, 3, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(255,255,255,.75)'; ctx.fill();
ctx.beginPath(); ctx.arc(lx - 3, ly - 3, 1, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(255,255,255,.95)'; ctx.fill();
// ── Вилка-держатель (yoke / U-кронштейн) ─────────────────────
ctx.shadowBlur = 0;
ctx.strokeStyle = '#3c3c3c'; ctx.lineWidth = 3.5;
ctx.lineCap = 'round'; ctx.lineJoin = 'round';
ctx.beginPath();
ctx.moveTo(-BW / 4 + 2, -BH / 2 - 2);
ctx.lineTo(-BW / 4 + 2, -BH / 2 - 15);
ctx.lineTo( BW / 4 - 2, -BH / 2 - 15);
ctx.lineTo( BW / 4 - 2, -BH / 2 - 2);
ctx.stroke();
// Скоба кронштейна соединяется горизонтально с креплением
ctx.strokeStyle = '#4a4a4a'; ctx.lineWidth = 2.5;
ctx.beginPath();
ctx.moveTo(-BW / 4 + 2, -BH / 2 - 15);
ctx.lineTo( BW / 4 - 2, -BH / 2 - 15);
ctx.stroke();
// Шарнирный болт (точка поворота прожектора в кронштейне)
ctx.beginPath(); ctx.arc(0, -BH / 2 - 15, 4.5, 0, Math.PI * 2);
ctx.fillStyle = '#5a5a5a'; ctx.fill();
ctx.strokeStyle = '#888'; ctx.lineWidth = 0.9; ctx.stroke();
ctx.strokeStyle = '#777'; ctx.lineWidth = 0.8;
ctx.beginPath(); ctx.moveTo(-2.5, -BH / 2 - 15); ctx.lineTo(2.5, -BH / 2 - 15); ctx.stroke();
ctx.beginPath(); ctx.moveTo(0, -BH / 2 - 17.5); ctx.lineTo(0, -BH / 2 - 12.5); ctx.stroke();
ctx.shadowBlur = 0;
ctx.lineCap = 'butt';
}
// ---- рисуем прожектор с учётом прогресса (0..1) ----
function drawSpotlight(link, progress) {
if (progress < 0.01) return;
const r = link.getBoundingClientRect();
const px = r.left + r.width / 2;
const wireY0 = ballPos.navH;
const wireLen = WIRE_MAX * progress;
const projX = px;
const projY = wireY0 + wireLen;
const bx = ballPos.x, by = ballPos.ballY;
// Угол от центра прожектора к диско-шару
const angle = Math.atan2(by - projY, bx - projX);
// Позиция линзы в мировых координатах
const BW = 46, FW = 20;
const lensLocalX = BW / 2 - FW / 2;
const lensWX = projX + Math.cos(angle) * lensLocalX;
const lensWY = projY + Math.sin(angle) * lensLocalX;
ctx.save();
ctx.globalAlpha = Math.pow(progress, 0.65);
// ── Конус света (рисуем ДО корпуса, чтобы корпус был поверх) ──
const dx = bx - lensWX, dy = by - lensWY;
const beamLen = Math.sqrt(dx * dx + dy * dy);
const beamAng = Math.atan2(dy, dx);
const ha = 0.20;
const beam = ctx.createLinearGradient(lensWX, lensWY, bx, by);
beam.addColorStop(0, 'rgba(255,245,180,.60)');
beam.addColorStop(0.5, 'rgba(255,230,120,.15)');
beam.addColorStop(1, 'rgba(255,200,50,.02)');
ctx.beginPath();
ctx.moveTo(lensWX, lensWY);
ctx.lineTo(lensWX + Math.cos(beamAng - ha) * beamLen, lensWY + Math.sin(beamAng - ha) * beamLen);
ctx.lineTo(lensWX + Math.cos(beamAng + ha) * beamLen, lensWY + Math.sin(beamAng + ha) * beamLen);
ctx.closePath();
ctx.fillStyle = beam;
ctx.fill();
// ── Провод (прямо вниз, к центру корпуса) ────────────────────
ctx.strokeStyle = 'rgba(45,45,45,.92)';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(px, wireY0);
ctx.lineTo(projX, projY);
ctx.stroke();
// ── Корпус прожектора ─────────────────────────────────────────
ctx.translate(projX, projY);
ctx.rotate(angle);
drawFresnelBody();
ctx.restore();
}
// ---- рисуем шар ----
function drawBall() {
const cx = ballPos.x, cy = ballPos.ballY;
// Верёвка
ctx.save();
ctx.strokeStyle = 'rgba(110,85,60,.9)';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(cx, ballPos.ropeTopY);
ctx.lineTo(cx, cy - BALL_R);
ctx.stroke();
ctx.restore();
// Клип по кругу
ctx.save();
ctx.beginPath();
ctx.arc(cx, cy, BALL_R, 0, Math.PI * 2);
ctx.clip();
// Металлическая основа
const base = ctx.createRadialGradient(cx - BALL_R * 0.28, cy - BALL_R * 0.3, 0, cx, cy, BALL_R);
base.addColorStop(0, '#e8e8e8');
base.addColorStop(0.35,'#999');
base.addColorStop(1, '#0e0e0e');
ctx.fillStyle = base;
ctx.fillRect(cx - BALL_R, cy - BALL_R, BALL_R * 2, BALL_R * 2);
// Суммарный прогресс всех прожекторов (0 = ни одного, 1+ = полностью активны)
let totalProj = 0;
projState.forEach(s => { totalProj += s.progress; });
const colorMix = Math.min(1, totalProj); // 0 = только белый, 1 = полные цвета
// Вспомогательная функция: смешать hex-цвет с белым по коэффициенту t (0=белый, 1=цвет)
function mixWithWhite(hex, t) {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
const ri = Math.round(r + (255 - r) * (1 - t));
const gi = Math.round(g + (255 - g) * (1 - t));
const bi = Math.round(b + (255 - b) * (1 - t));
return `rgb(${ri},${gi},${bi})`;
}
// Зеркальные тайлы
const step = TILE_S + TILE_G;
for (let ty = -BALL_R; ty < BALL_R; ty += step) {
for (let tx = -BALL_R; tx < BALL_R; tx += step) {
if (tx * tx + ty * ty >= BALL_R * BALL_R) continue;
const angle = Math.atan2(ty, tx);
const dist = Math.sqrt(tx * tx + ty * ty) / BALL_R;
const ci = (
(Math.floor((angle + rot * 2.5) / (Math.PI / 4)) % DISC_COLORS.length + DISC_COLORS.length)
% DISC_COLORS.length + Math.floor(dist * 3)
) % DISC_COLORS.length;
const bright = 0.18 + 0.82 * Math.max(0, Math.cos(angle - rot * 1.5));
const boost = Math.min(0.35, totalProj * 0.25);
// Без прожектора — серебристо-белые тайлы; с прожектором — цветные
const tileColor = colorMix < 0.01
? `hsl(0,0%,${Math.round(55 + bright * 45)}%)`
: mixWithWhite(DISC_COLORS[ci], colorMix);
ctx.fillStyle = tileColor;
ctx.globalAlpha = Math.min(1, bright + boost) * 0.90;
ctx.fillRect(cx + tx, cy + ty, TILE_S, TILE_S);
}
}
ctx.globalAlpha = 1;
// Глянцевый блик (объём)
const shine = ctx.createRadialGradient(cx - BALL_R * 0.32, cy - BALL_R * 0.38, 0, cx, cy, BALL_R);
shine.addColorStop(0, 'rgba(255,255,255,.72)');
shine.addColorStop(0.25, 'rgba(255,255,255,.07)');
shine.addColorStop(1, 'rgba(0,0,0,.55)');
ctx.fillStyle = shine;
ctx.fillRect(cx - BALL_R, cy - BALL_R, BALL_R * 2, BALL_R * 2);
ctx.restore();
// Крепёжный колпачок
ctx.save();
ctx.fillStyle = '#bbb';
ctx.beginPath();
ctx.arc(cx, cy - BALL_R + 3, 5, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
// ---- орбитальные блики (масштаб зависит от суммы прожекторов) ----
function drawOrbitGlints() {
let totalProj = 0;
projState.forEach(s => { totalProj += s.progress; });
if (totalProj < 0.05) return;
const cx = ballPos.x, cy = ballPos.ballY;
const count = 12;
for (let i = 0; i < count; i++) {
const a = rot * 4 + i * ((Math.PI * 2) / count);
const r = BALL_R + 6 + 4 * Math.sin(rot * 5 + i * 1.1);
const sx = cx + Math.cos(a) * r;
const sy = cy + Math.sin(a) * r * 0.5;
const al = totalProj * (0.3 + 0.7 * Math.abs(Math.sin(rot * 9 + i * 1.5)));
ctx.beginPath();
ctx.arc(sx, sy, 2.2, 0, Math.PI * 2);
ctx.fillStyle = DISC_COLORS[i % DISC_COLORS.length];
ctx.globalAlpha = Math.min(1, al);
ctx.fill();
}
ctx.globalAlpha = 1;
}
// ---- основной цикл ----
function loop() {
ctx.clearRect(0, 0, CW, CH);
rot += 0.007;
if (!ballPos) { refreshPos(); requestAnimationFrame(loop); return; }
// Обновляем прогресс каждого прожектора (плавный выезд/заезд)
projState.forEach((s, link) => {
s.progress += (s.target - s.progress) * LERP_SPD;
// Стреляем бликами когда прожектор почти полностью выехал
if (s.progress > 0.85 && !s.glintFired) {
s.glintFired = true;
for (let i = 0; i < 16; i++) {
const a = Math.random() * Math.PI * 2;
glints.push({
x: ballPos.x, y: ballPos.ballY,
vx: Math.cos(a) * (Math.random() * 5 + 1.5),
vy: Math.sin(a) * (Math.random() * 5 + 1.5) - 1.5,
life: 1, decay: 0.022 + Math.random() * 0.025,
color: DISC_COLORS[Math.floor(Math.random() * DISC_COLORS.length)],
r: 1.5 + Math.random() * 2.5,
});
}
}
// Рисуем прожектор с текущим прогрессом
drawSpotlight(link, s.progress);
});
// Шар на верёвке
drawBall();
// Орбитальные блики (перед шаром)
drawOrbitGlints();
// Разлетающиеся блики
glints = glints.filter(g => g.life > 0);
glints.forEach(g => {
ctx.beginPath();
ctx.arc(g.x, g.y, g.r, 0, Math.PI * 2);
ctx.fillStyle = g.color;
ctx.globalAlpha = g.life;
ctx.fill();
ctx.globalAlpha = 1;
g.x += g.vx;
g.y += g.vy;
g.vy += 0.09;
g.life -= g.decay;
});
requestAnimationFrame(loop);
}
loop();
})();
// ---- Навигация: бургер-меню ----
const burger = document.getElementById('burger');
const navLinks = document.getElementById('nav-links');
burger.addEventListener('click', () => {
navLinks.classList.toggle('open');
});
// Закрыть меню при клике на ссылку
navLinks.querySelectorAll('a').forEach(link => {
link.addEventListener('click', () => navLinks.classList.remove('open'));
});
// ---- Навигация: тень при прокрутке ----
const navbar = document.getElementById('navbar');
window.addEventListener('scroll', () => {
navbar.classList.toggle('scrolled', window.scrollY > 20);
});
// ---- FAQ: аккордеон ----
function setFaqAnswerHeight(answer, open) {
if (open) {
answer.classList.add('open');
answer.style.maxHeight = answer.scrollHeight + 'px';
} else {
answer.classList.remove('open');
answer.style.maxHeight = '0px';
}
}
document.querySelectorAll('.faq-question').forEach(btn => {
btn.addEventListener('click', () => {
const expanded = btn.getAttribute('aria-expanded') === 'true';
const answer = btn.nextElementSibling;
document.querySelectorAll('.faq-question').forEach(b => {
b.setAttribute('aria-expanded', 'false');
setFaqAnswerHeight(b.nextElementSibling, false);
});
if (!expanded) {
btn.setAttribute('aria-expanded', 'true');
setFaqAnswerHeight(answer, true);
}
});
});
window.addEventListener('resize', () => {
document.querySelectorAll('.faq-question[aria-expanded="true"]').forEach(btn => {
const answer = btn.nextElementSibling;
if (answer.classList.contains('open')) {
answer.style.maxHeight = answer.scrollHeight + 'px';
}
});
});
// ---- Яндекс.Карта: проверка загрузки iframe ----
const ymapFrame = document.getElementById('ymap-frame');
const mapFallback = document.getElementById('map-fallback');
// Подставляем реальный iframe с картой через Yandex Maps short link
// Если embed не поддерживается — показываем кнопку
if (ymapFrame) {
mapFallback.style.display = 'none';
}
// ---- Галерея: лайтбокс ----
const lightbox = document.getElementById('lightbox');
const lightboxImg = lightbox?.querySelector('.lightbox-img');
const galleryItems = Array.from(document.querySelectorAll('.gallery-item'));
let lightboxIndex = 0;
function openLightbox(index) {
if (!lightbox || !lightboxImg || !galleryItems.length) return;
lightboxIndex = index;
const img = galleryItems[lightboxIndex].querySelector('img');
lightboxImg.src = img.src;
lightboxImg.alt = img.alt;
lightbox.hidden = false;
lightbox.setAttribute('aria-hidden', 'false');
document.body.style.overflow = 'hidden';
}
function closeLightbox() {
if (!lightbox) return;
lightbox.hidden = true;
lightbox.setAttribute('aria-hidden', 'true');
lightboxImg.src = '';
document.body.style.overflow = '';
}
function showLightboxStep(delta) {
if (!galleryItems.length) return;
lightboxIndex = (lightboxIndex + delta + galleryItems.length) % galleryItems.length;
const img = galleryItems[lightboxIndex].querySelector('img');
lightboxImg.src = img.src;
lightboxImg.alt = img.alt;
}
galleryItems.forEach((item, index) => {
item.addEventListener('click', () => openLightbox(index));
});
lightbox?.querySelector('.lightbox-close')?.addEventListener('click', closeLightbox);
lightbox?.querySelector('.lightbox-prev')?.addEventListener('click', () => showLightboxStep(-1));
lightbox?.querySelector('.lightbox-next')?.addEventListener('click', () => showLightboxStep(1));
lightbox?.addEventListener('click', (e) => {
if (e.target === lightbox) closeLightbox();
});
document.addEventListener('keydown', (e) => {
if (lightbox?.hidden) return;
if (e.key === 'Escape') closeLightbox();
if (e.key === 'ArrowLeft') showLightboxStep(-1);
if (e.key === 'ArrowRight') showLightboxStep(1);
});
// ---- Плавное появление секций при прокрутке ----
const observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('visible');
observer.unobserve(entry.target);
}
});
},
{ threshold: 0.1 }
);
document.querySelectorAll('.timeline-card, .faq-item, .gallery-item, .coordinator-card, .stat-card, .stats-two-col').forEach(el => {
el.style.opacity = '0';
el.style.transform = 'translateY(24px)';
el.style.transition = 'opacity 0.5s ease, transform 0.5s ease';
observer.observe(el);
});
document.addEventListener('animationend', () => {}, { once: true });
// Добавляем класс visible через CSS
const style = document.createElement('style');
style.textContent = '.visible { opacity: 1 !important; transform: none !important; }';
document.head.appendChild(style);

1084
services/burka_wed/style.css Normal file

File diff suppressed because it is too large Load Diff

View File

@ -109,6 +109,11 @@ truenews.sesur.dev {
root * /srv/vk-podcast-bot/data root * /srv/vk-podcast-bot/data
file_server file_server
} }
zaytsev-wedding.sesur.dev {
root * /opt/homelab/services/burka_wed
file_server
}
t.sesur.dev { t.sesur.dev {
root * /opt/homelab/services/mtproto_page root * /opt/homelab/services/mtproto_page