templates/index.html.twig line 1

Open in your IDE?
  1. {% extends 'base.html.twig' %}
  2. {% block title %}PlayPapa{% endblock %}
  3. {% block body_class %}preloader-active{% endblock %}
  4. {% block head_scripts %}
  5.     <script>
  6.       window.__preloaderStart = performance.now();
  7.       try {
  8.         if ("scrollRestoration" in history) history.scrollRestoration = "manual";
  9.         if (!window.location.hash || window.location.hash === "#") {
  10.           window.addEventListener("load", () => requestAnimationFrame(() => window.scrollTo(0, 0)), { once: true });
  11.         }
  12.       } catch {}
  13.     </script>
  14.     <script>
  15.       window.playpapaCatalog = {{ catalog_payload_json|default('{"products":[],"total":0}')|raw }};
  16.     </script>
  17.     <script>
  18.       tailwind = {
  19.         config: {
  20.           theme: {
  21.             extend: {
  22.               colors: {
  23.                 papa: "#268bf3",
  24.                 ink: "#202838",
  25.                 soft: "#f2f4f8",
  26.               },
  27.             },
  28.           },
  29.         },
  30.       };
  31.     </script>
  32.     <script src="https://cdn.tailwindcss.com" defer></script>
  33. {% endblock %}
  34. {% block body_start %}
  35. <div class="page-preloader" id="pagePreloader" role="status" aria-label="Загрузка">
  36.       <div class="preloader-inner">
  37.         <div class="preloader-dots" aria-hidden="true">
  38.           <span></span>
  39.           <span></span>
  40.           <span></span>
  41.         </div>
  42.         <div class="preloader-text">Загрузка...</div>
  43.       </div>
  44.     </div>
  45. {% endblock %}
  46. {% block content %}
  47. <section class="shell hero-section">
  48.         <picture class="hero-picture">
  49.           <source media="(max-width: 767px)" srcset="./assets/hero-steam-mobile.jpg" />
  50.           <img class="hero-img" src="./assets/hero-steam-cutout.jpg" alt="Пополнение Steam" />
  51.         </picture>
  52.         <div class="hero-arrows">
  53.           <button aria-label="Назад">
  54.             <svg viewBox="0 0 24 24" fill="none"><path d="M19 12H6m5-5-5 5 5 5" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" /></svg>
  55.           </button>
  56.           <button aria-label="Вперед">
  57.             <svg viewBox="0 0 24 24" fill="none"><path d="M5 12h13m-5-5 5 5-5 5" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" /></svg>
  58.           </button>
  59.         </div>
  60.         <div class="hero-dots" aria-hidden="true"><span></span><span></span><span></span><span></span><span></span><span></span></div>
  61.       </section>
  62.       <section class="shell services-card">
  63.         <div class="services-row" id="servicesRow"></div>
  64.         <div class="topup-row">
  65.           <div class="topup-info">
  66.             <img src="./assets/steam-topup.png" alt="" />
  67.             <div>
  68.               <div class="topup-title">
  69.                 Пополнение Steam
  70.                 <span>5%</span>
  71.               </div>
  72.               <button class="promo-small" type="button" aria-expanded="false" aria-controls="topupPromoSection">
  73.                 <span class="promo-small-label">Ввести промокод</span>
  74.                 <svg viewBox="0 0 16 16" fill="none"><path d="m4 6 4 4 4-4" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" /></svg>
  75.               </button>
  76.             </div>
  77.           </div>
  78.           <label class="form-field login-field">
  79.             <img src="./assets/profile-gray.svg" alt="" />
  80.             <input type="text" name="steam-login" placeholder="Логин Steam" />
  81.             <span class="field-info" aria-hidden="true"><img src="./assets/info-i.svg" alt="" /></span>
  82.           </label>
  83.           <div class="form-field amount-field">
  84.             <div class="amount-field-shell">
  85.               <span class="currency-mark"><span class="currency-icon currency-icon-ruble" aria-hidden="true"></span></span>
  86.               <label class="amount-control">
  87.                 <small>Сумма</small>
  88.                 <span class="amount-value">
  89.                   <input type="text" name="amount" inputmode="numeric" value="500" maxlength="6" aria-label="Сумма" aria-expanded="false" aria-controls="topupQuickPopover" />
  90.                   <span class="currency-icon currency-icon-ruble" aria-hidden="true"></span>
  91.                 </span>
  92.               </label>
  93.               <span class="currency-tabs" role="group" aria-label="Валюта">
  94.                 <button type="button" aria-label="Доллар" aria-pressed="false" data-topup-currency="USD"><span class="currency-icon currency-icon-dollar" aria-hidden="true"></span></button>
  95.                 <button type="button" aria-label="Тенге" aria-pressed="false" data-topup-currency="KZT"><span class="currency-icon currency-icon-tenge" aria-hidden="true"></span></button>
  96.                 <button class="is-active" type="button" aria-label="Рубль" aria-pressed="true" data-topup-currency="RUB"><span class="currency-icon currency-icon-ruble" aria-hidden="true"></span></button>
  97.               </span>
  98.             </div>
  99.             <div class="topup-quick-popover" id="topupQuickPopover" aria-hidden="true" inert>
  100.               <span class="topup-quick-popover-label">Быстрый выбор суммы</span>
  101.               <div class="topup-quick-popover-buttons" aria-label="Быстрый выбор суммы">
  102.                 <button class="is-active" type="button" data-quick-amount="500">500 ₽</button>
  103.                 <button type="button" data-quick-amount="1000">1 000 ₽</button>
  104.                 <button type="button" data-quick-amount="2500">2 500 ₽</button>
  105.                 <button type="button" data-quick-amount="5000">5 000 ₽</button>
  106.               </div>
  107.             </div>
  108.           </div>
  109.           <button class="pay-btn" type="button" aria-expanded="false" aria-controls="topupPaymentSection">Оплатить 500<span class="currency-icon currency-icon-ruble" aria-hidden="true"></span></button>
  110.           <div class="topup-expand" id="topupDetails" aria-hidden="true" inert>
  111.           <div class="topup-expand-clip">
  112.             <div class="topup-expand-content">
  113.               <section class="topup-expand-section topup-promo-section" id="topupPromoSection" aria-hidden="true" inert>
  114.                 <div class="topup-expand-section-inner">
  115.                   <label class="topup-detail-label" for="topupPromoInput">Промокод</label>
  116.                   <div class="topup-promo-row">
  117.                     <label class="topup-promo-input">
  118.                       <svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
  119.                         <path d="M4 10h16v10H4V10Zm0-4h16v4H4V6Zm8 0v14M12 6c-1.7-3.1-5.2-2.8-5.2-.5C6.8 7.2 9 7.5 12 6Zm0 0c1.7-3.1 5.2-2.8 5.2-.5C17.2 7.2 15 7.5 12 6Z" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round" />
  120.                       </svg>
  121.                       <input id="topupPromoInput" type="text" placeholder="Введите промокод" autocomplete="off" />
  122.                     </label>
  123.                     <button class="topup-promo-apply" type="button">Применить</button>
  124.                   </div>
  125.                 </div>
  126.               </section>
  127.               <div class="topup-promo-benefit" aria-live="polite" aria-hidden="true">
  128.                 <img src="./assets/discount-percent.png" alt="" />
  129.                 <strong>Скидка <span>5%</span> — вы экономите <b data-promo-saving>25 ₽</b></strong>
  130.                 <small><span>Вы платите примерно на 12% меньше,</span><span>чем на других площадках</span></small>
  131.               </div>
  132.               <section class="topup-expand-section topup-payment-section" id="topupPaymentSection" aria-hidden="true" inert>
  133.                 <div class="topup-expand-section-inner">
  134.                   <h3 class="topup-detail-label topup-payment-title">Способ оплаты</h3>
  135.                   <div class="topup-payment-methods" role="radiogroup" aria-label="Способ оплаты">
  136.                     <button class="topup-payment-method is-disabled" type="button" role="radio" aria-checked="false" aria-disabled="true" data-payment-method="sbp" data-fee="0" disabled>
  137.                       <span class="topup-payment-icon"><img src="./assets/payment-sbp.svg" alt="" /></span>
  138.                       <span class="topup-payment-copy"><b>СБП</b><small>Скоро</small></span>
  139.                       <span class="topup-payment-check" aria-hidden="true"><svg viewBox="0 0 20 20" fill="none"><path d="m5 10 3 3 7-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /></svg></span>
  140.                     </button>
  141.                     <button class="topup-payment-method is-disabled" type="button" role="radio" aria-checked="false" aria-disabled="true" data-payment-method="mir" data-fee="2" disabled>
  142.                       <span class="topup-payment-icon"><img src="./assets/payment-mir-option.svg" alt="" /></span>
  143.                       <span class="topup-payment-copy"><b>Карта МИР</b><small>Скоро</small></span>
  144.                       <span class="topup-payment-check" aria-hidden="true"><svg viewBox="0 0 20 20" fill="none"><path d="m5 10 3 3 7-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /></svg></span>
  145.                     </button>
  146.                     <button class="topup-payment-method is-selected" type="button" role="radio" aria-checked="true" data-payment-method="cryptobot" data-fee="0">
  147.                       <span class="topup-payment-icon"><img src="./assets/payment-btc.svg" alt="" /></span>
  148.                       <span class="topup-payment-copy"><b>CryptoBot</b><small>Telegram Crypto Bot</small></span>
  149.                       <span class="topup-payment-check" aria-hidden="true"><svg viewBox="0 0 20 20" fill="none"><path d="m5 10 3 3 7-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /></svg></span>
  150.                     </button>
  151.                   </div>
  152.                   <div class="topup-total-row">
  153.                     <div class="topup-total-copy">
  154.                       <small>К зачислению на Steam</small>
  155.                       <span><strong data-topup-credit>500 ₽</strong><b data-topup-fee-badge>Без комиссии</b></span>
  156.                     </div>
  157.                     <button class="topup-final-pay" type="button">Оплатить 500₽</button>
  158.                   </div>
  159.                   <div class="topup-checkout-result" data-topup-result hidden></div>
  160.                 </div>
  161.               </section>
  162.             </div>
  163.           </div>
  164.         </div>
  165.         </div>
  166.       </section>
  167.       <section class="shell product-section popular-section">
  168.         <div class="section-head compact-head">
  169.           <h2><img src="./assets/icon-popular.svg" alt="" />Популярные товары</h2>
  170.           <nav class="chip-row" aria-label="Категории">
  171.             <button class="chip is-active"><img src="./assets/cat-donate.svg" alt="" />Донат</button>
  172.             <button class="chip"><img src="./assets/cat-subscribes.svg" alt="" />Подписки</button>
  173.             <button class="chip"><img src="./assets/cat-items.svg" alt="" />Предметы</button>
  174.             <button class="chip"><img src="./assets/cat-accounts.svg" alt="" />Аккаунты</button>
  175.             <button class="chip"><img src="./assets/cat-keys.svg" alt="" />Ключи</button>
  176.             <button class="chip"><img src="./assets/cat-currency.svg" alt="" />Steam Gift</button>
  177.             <button class="chip"><img src="./assets/cat-other.svg" alt="" />Другое</button>
  178.           </nav>
  179.         </div>
  180.         <div class="product-grid" data-products="popular"></div>
  181.         <a class="show-all mobile-show-all" href="/catalog">Показать все</a>
  182.       </section>
  183.       <section class="shell product-section">
  184.         <div class="section-head">
  185.           <h2><img src="./assets/icon-recommended.svg" alt="" />Рекомендованные товары</h2>
  186.           <a class="show-all" href="/catalog">Показать все</a>
  187.         </div>
  188.         <div class="product-grid" data-products="recommended"></div>
  189.       </section>
  190.       <section class="shell blogger-section">
  191.         <div class="section-head">
  192.           <h2><img src="./assets/icon-recommended-alt.svg" alt="" />Подборка от блогера</h2>
  193.           <a class="show-all" href="/catalog">Показать все</a>
  194.         </div>
  195.         <div class="blogger-layout">
  196.           <article class="blogger-card">
  197.             <div class="blogger-card-head">
  198.               <div class="blogger-author">
  199.                 <img src="./assets/polka.png" alt="" />
  200.                 <strong>Рекомендует Polk Zone</strong>
  201.               </div>
  202.               <button class="promo-inside">
  203.                 <img src="./assets/blogger-promo-blob.svg" alt="" />
  204.                 <span>Промокод внутри</span>
  205.               </button>
  206.             </div>
  207.             <img class="blogger-banner" src="./assets/blogger-promo.png" alt="Личная подборка от блогера" />
  208.             <div class="blogger-stats">
  209.               <div class="coupon-stat">
  210.                 <span class="stat-icon"><img src="./assets/blogger-icon-coupon.svg" alt="" /></span>
  211.                 <span class="coupon-text">Промокод<br /><b>BLOGGER5</b></span>
  212.                 <span class="coupon-copy" aria-hidden="true"><img src="./assets/blogger-icon-copy.svg" alt="" /></span>
  213.               </div>
  214.               <div class="stat-item"><span class="stat-icon"><img src="./assets/blogger-icon-cart.svg" alt="" /></span><span class="stat-text"><b>12 товаров</b><small>в подборке</small></span></div>
  215.               <div class="stat-item wide"><span class="stat-icon"><img src="./assets/blogger-icon-services.svg" alt="" /></span><span class="stat-text"><b>Steam · Roblox · PUBG</b><small>и другие сервисы</small></span></div>
  216.             </div>
  217.           </article>
  218.           <aside class="blogger-side">
  219.             <article class="side-card steam-side">
  220.               <div class="side-title">
  221.                 <img src="./assets/steam-topup.png" alt="" />
  222.                 <div><b>Пополнение Steam</b><span>Без комиссии</span></div>
  223.               </div>
  224.               <div class="side-features">
  225.                 <div><img src="./assets/blogger-icon-wallet.svg" alt="" /><b>от 500 ₽</b></div>
  226.                 <div><img src="./assets/blogger-icon-lightning.svg" alt="" /><b>Моментально</b></div>
  227.               </div>
  228.               <button>Пополнить</button>
  229.             </article>
  230.             <article class="side-card keys-side">
  231.               <div class="side-title">
  232.                 <img src="./assets/blogger-playpapa-logo.svg" alt="" />
  233.                 <div><b>Топ ключи игр</b><span>Выбор подписчиков</span></div>
  234.               </div>
  235.               <div class="thumb-row">
  236.                 <img src="./assets/thumb-game-1.jpg" alt="" />
  237.                 <img src="./assets/thumb-game-2.png" alt="" />
  238.                 <img src="./assets/thumb-game-3.png" alt="" />
  239.                 <span>Ещё 7</span>
  240.               </div>
  241.               <button>Смотреть</button>
  242.             </article>
  243.           </aside>
  244.         </div>
  245.       </section>
  246.       <section class="shell product-section other-products">
  247.         <div class="section-head">
  248.           <h2><img src="./assets/icon-recommended-alt.svg" alt="" />Другие товары</h2>
  249.           <a class="show-all" href="/catalog">Показать все</a>
  250.         </div>
  251.         <div class="product-grid" data-products="other"></div>
  252.       </section>
  253.       <section class="shell articles-section">
  254.         <div class="section-head">
  255.           <h2><img src="./assets/icon-articles.svg" alt="" />Интересные статьи</h2>
  256.           <a class="show-all" href="/catalog">Показать все</a>
  257.         </div>
  258.         <div class="article-grid">
  259.           <article class="article-card">
  260.             <div class="article-image"><img src="./assets/article-paradise.png" alt="" /><span>Статьи</span></div>
  261.             <h3>Тайные игры EGS: Paradise Killer — детектив, который можно забрать прямо сейчас</h3>
  262.             <div class="article-meta"><span><img src="./assets/article-calendar.svg" alt="" />13 июня 2026</span><span><img src="./assets/article-eye.svg" alt="" />4 892</span></div>
  263.           </article>
  264.           <article class="article-card">
  265.             <div class="article-image"><img src="./assets/article-sale.png" alt="" /><span>Статьи</span></div>
  266.             <h3>Тайные игры EGS: Paradise Killer — детектив, который можно забрать прямо сейчас</h3>
  267.             <div class="article-meta"><span><img src="./assets/article-calendar.svg" alt="" />13 июня 2026</span><span><img src="./assets/article-eye.svg" alt="" />4 892</span></div>
  268.           </article>
  269.           <article class="article-card">
  270.             <div class="article-image"><img src="./assets/article-mobile.png" alt="" /><span>Статьи</span></div>
  271.             <h3>Тайные игры EGS: Paradise Killer — детектив, который можно забрать прямо сейчас</h3>
  272.             <div class="article-meta"><span><img src="./assets/article-calendar.svg" alt="" />13 июня 2026</span><span><img src="./assets/article-eye.svg" alt="" />4 892</span></div>
  273.           </article>
  274.         </div>
  275.       </section>
  276.       <section class="shell reviews-section">
  277.         <div class="section-head reviews-head">
  278.           <div>
  279.             <h2><img src="./assets/icon-reviews.svg" alt="" />Последние отзывы</h2>
  280.             <p>Все отзывы взяты с независимой площадки</p>
  281.           </div>
  282.           <a class="show-all" href="/catalog">Показать все</a>
  283.         </div>
  284.         <div class="review-grid">
  285.           <article class="review-card">
  286.             <div class="review-top">
  287.               <img src="./assets/review-avatar.png" alt="" />
  288.               <div><b>Bizidin</b><span>★★★★★ <strong>5.0</strong></span></div>
  289.               <time>Сегодня в 11:48</time>
  290.             </div>
  291.             <p class="review-text">Отзывчивый и приятный продавец, помог не только с товаром но и с другим вопросом. Рекомендую!</p>
  292.             <div class="review-product">
  293.               <img src="./assets/product-pubg.png" alt="" />
  294.               <b>🌸 FunTime | Полностью готовый сервер под ключ ⚡</b>
  295.               <span>139₽</span>
  296.             </div>
  297.           </article>
  298.           <article class="review-card">
  299.             <div class="review-top">
  300.               <img src="./assets/review-avatar.png" alt="" />
  301.               <div><b>Bizidin</b><span>★★★★★ <strong>5.0</strong></span></div>
  302.               <time>Сегодня в 11:48</time>
  303.             </div>
  304.             <p class="review-text">Отзывчивый и приятный продавец, помог не только с товаром но и с другим вопросом. Рекомендую!</p>
  305.             <div class="review-product">
  306.               <img src="./assets/product-pubg.png" alt="" />
  307.               <b>🌸 FunTime | Полностью готовый сервер под ключ ⚡</b>
  308.               <span>139₽</span>
  309.             </div>
  310.           </article>
  311.           <article class="review-card">
  312.             <div class="review-top">
  313.               <img src="./assets/review-avatar.png" alt="" />
  314.               <div><b>Bizidin</b><span>★★★★★ <strong>5.0</strong></span></div>
  315.               <time>Сегодня в 11:48</time>
  316.             </div>
  317.             <p class="review-text">Отзывчивый и приятный продавец, помог не только с товаром но и с другим вопросом. Рекомендую!</p>
  318.             <div class="review-product">
  319.               <img src="./assets/product-pubg.png" alt="" />
  320.               <b>🌸 FunTime | Полностью готовый сервер под ключ ⚡</b>
  321.               <span>139₽</span>
  322.             </div>
  323.           </article>
  324.         </div>
  325.       </section>
  326. {% endblock %}
  327. {% block body_extras %}
  328. {% include 'components/mobile_search.html.twig' %}
  329. <div data-mobile-panel></div>
  330. {% endblock %}
  331. {% block scripts %}
  332. <script src="/assets/mobile-panel.js"></script>
  333.     <script>
  334.       const root = document.documentElement;
  335.       const themeSwitch = document.querySelector(".theme-switch");
  336.       const themeButtons = document.querySelectorAll(".theme-btn[data-theme]");
  337.       const preloader = document.getElementById("pagePreloader");
  338.       if (preloader) {
  339.         const preloaderStart = window.__preloaderStart || performance.now();
  340.         const minPreloaderTime = 600;
  341.         const maxPreloaderTime = 3000;
  342.         let preloaderHidden = false;
  343.         const hidePreloader = () => {
  344.           if (preloaderHidden) return;
  345.           preloaderHidden = true;
  346.           preloader.classList.add("is-hiding");
  347.           document.body.classList.remove("preloader-active");
  348.           window.setTimeout(() => preloader.remove(), 260);
  349.         };
  350.         const requestPreloaderHide = () => {
  351.           const elapsed = performance.now() - preloaderStart;
  352.           window.setTimeout(hidePreloader, Math.max(minPreloaderTime - elapsed, 0));
  353.         };
  354.         window.setTimeout(hidePreloader, Math.max(maxPreloaderTime - (performance.now() - preloaderStart), 0));
  355.         if (document.readyState === "complete") {
  356.           requestPreloaderHide();
  357.         } else {
  358.           window.addEventListener("load", requestPreloaderHide, { once: true });
  359.         }
  360.       }
  361.       const setTheme = (theme, animate = false) => {
  362.         if (animate) {
  363.           document.body.classList.remove("theme-animating");
  364.           void document.body.offsetWidth;
  365.           document.body.classList.add("theme-animating");
  366.         }
  367.         root.dataset.theme = theme;
  368.         try {
  369.           localStorage.setItem("playpapa-theme", theme);
  370.         } catch {}
  371.         themeButtons.forEach((button) => {
  372.           const active = button.dataset.theme === theme;
  373.           button.classList.toggle("is-active", active);
  374.           button.setAttribute("aria-pressed", active ? "true" : "false");
  375.         });
  376.         if (animate) window.setTimeout(() => document.body.classList.remove("theme-animating"), 560);
  377.       };
  378.       setTheme(root.dataset.theme === "dark" ? "dark" : "light");
  379.       themeButtons.forEach((button) => {
  380.         button.addEventListener("click", () => setTheme(button.dataset.theme, true));
  381.       });
  382.       themeSwitch?.addEventListener("click", (event) => {
  383.         if (event.target.closest(".theme-btn")) return;
  384.         setTheme(root.dataset.theme === "dark" ? "light" : "dark", true);
  385.       });
  386.       const mobileSearchLayer = document.getElementById("mobileSearch");
  387.       const mobileSearchTrigger = document.querySelector("[data-mobile-search-trigger]");
  388.       const mobileSearchBack = document.getElementById("mobileSearchBack");
  389.       const setMobileSearchOpen = (open) => {
  390.         if (!mobileSearchLayer) return;
  391.         mobileSearchLayer.classList.toggle("is-open", open);
  392.         mobileSearchLayer.setAttribute("aria-hidden", open ? "false" : "true");
  393.         mobileSearchLayer.toggleAttribute("inert", !open);
  394.         mobileSearchTrigger?.setAttribute("aria-expanded", open ? "true" : "false");
  395.         document.body.classList.toggle("mobile-search-open", open);
  396.         if (open) mobileSearchLayer.scrollTop = 0;
  397.       };
  398.       mobileSearchTrigger?.addEventListener("click", (event) => {
  399.         event.preventDefault();
  400.         setProfileOpen(false);
  401.         setCatalogOpen(false);
  402.         setMobileSearchOpen(true);
  403.       });
  404.       mobileSearchBack?.addEventListener("click", () => {
  405.         setMobileSearchOpen(false);
  406.         mobileSearchTrigger?.focus();
  407.       });
  408.       mobileSearchLayer?.querySelector(".mobile-search-form")?.addEventListener("submit", (event) => event.preventDefault());
  409.       mobileSearchLayer?.querySelectorAll('a[href="#"]').forEach((link) => {
  410.         link.addEventListener("click", (event) => event.preventDefault());
  411.       });
  412.       document.addEventListener("keydown", (event) => {
  413.         if (event.key !== "Escape" || !mobileSearchLayer?.classList.contains("is-open")) return;
  414.         setMobileSearchOpen(false);
  415.         mobileSearchTrigger?.focus();
  416.       });
  417.       const profileMenu = document.querySelector(".profile-menu");
  418.       const profileButton = profileMenu?.querySelector(".profile-btn");
  419.       const profilePopover = document.getElementById("profilePopover");
  420.       const setProfileOpen = (open, returnFocus = false) => {
  421.         if (!profileMenu || !profilePopover || !profileButton) return;
  422.         profileMenu.classList.toggle("is-open", open);
  423.         profileButton.setAttribute("aria-expanded", open ? "true" : "false");
  424.         profilePopover.setAttribute("aria-hidden", open ? "false" : "true");
  425.         profilePopover.toggleAttribute("inert", !open);
  426.         document.body.classList.toggle("profile-open", open);
  427.         if (returnFocus) profileButton.focus();
  428.       };
  429.       profileButton?.addEventListener("click", (event) => {
  430.         event.stopPropagation();
  431.         const shouldOpen = !profileMenu?.classList.contains("is-open");
  432.         setProfileOpen(shouldOpen);
  433.       });
  434.       profilePopover?.querySelectorAll(".profile-menu-item:not(:disabled)").forEach((item) => {
  435.         item.addEventListener("click", () => setProfileOpen(false));
  436.       });
  437.       document.addEventListener("pointerdown", (event) => {
  438.         if (!profileMenu?.classList.contains("is-open") || profileMenu.contains(event.target)) return;
  439.         setProfileOpen(false);
  440.       });
  441.       document.addEventListener("keydown", (event) => {
  442.         if (event.key !== "Escape" || !profileMenu?.classList.contains("is-open")) return;
  443.         setProfileOpen(false, true);
  444.       });
  445.       const catalogLayer = document.getElementById("catalogMegaMenu");
  446.       const catalogButton = document.querySelector(".catalog-btn:not([href])");
  447.       const catalogTriggers = [catalogButton, ...document.querySelectorAll("[data-catalog-trigger]")].filter(Boolean);
  448.       const catalogSideLinks = document.querySelectorAll(".catalog-side-link");
  449.       const catalogFeaturedItems = [
  450.         ["service-steam.png", "Пополнение Steam"],
  451.         ["service-brawl.png", "Game Pass"],
  452.         ["service-steam.png", "Пополнение PSN"],
  453.         ["service-brawl.png", "PS Plus"],
  454.         ["service-pubg.png", "Grand Theft Auto V"],
  455.         ["service-roblox.png", "Minecraft"],
  456.         ["service-playstation.png", "Diablo 4"],
  457.         ["service-mobile.png", "ARC Raiders"],
  458.         ["service-chatgpt.png", "Dead by Daylight"],
  459.       ];
  460.       const catalogNewItems = [
  461.         ["article-paradise.png", "Gothic 1 Remake"],
  462.         ["article-sale.png", "Forza Horizon 6"],
  463.         ["product-pubg.png", "Assassin's Creed: Black..."],
  464.         ["article-mobile.png", "007 First Light"],
  465.         ["article-sale.png", "Call of Duty: Modern..."],
  466.         ["product-pubg.png", "Marvel's Wolverine"],
  467.         ["article-paradise.png", "UFC 6"],
  468.         ["article-sale.png", "Subnautica 2"],
  469.         ["product-pubg.png", "SILENT HILL: Townfall"],
  470.       ];
  471.       const catalogItemMarkup = ([image, label]) => `
  472.         <a class="catalog-showcase-item" href="#" aria-label="${label}">
  473.           <img src="./assets/${image}" alt="">
  474.           <span>${label}</span>
  475.         </a>
  476.       `;
  477.       const catalogFeatured = document.getElementById("catalogFeatured");
  478.       const catalogNew = document.getElementById("catalogNew");
  479.       if (catalogFeatured) catalogFeatured.innerHTML = catalogFeaturedItems.map(catalogItemMarkup).join("");
  480.       if (catalogNew) catalogNew.innerHTML = catalogNewItems.map(catalogItemMarkup).join("");
  481.       const setCatalogOpen = (open) => {
  482.         if (!catalogLayer) return;
  483.         catalogLayer.classList.toggle("is-open", open);
  484.         catalogLayer.setAttribute("aria-hidden", open ? "false" : "true");
  485.         catalogLayer.toggleAttribute("inert", !open);
  486.         document.body.classList.toggle("catalog-open", open);
  487.         catalogTriggers.forEach((trigger) => trigger.setAttribute("aria-expanded", open ? "true" : "false"));
  488.       };
  489.       catalogTriggers.forEach((trigger) => {
  490.         trigger.addEventListener("click", (event) => {
  491.           event.preventDefault();
  492.           setProfileOpen(false);
  493.           setMobileSearchOpen(false);
  494.           setCatalogOpen(!catalogLayer?.classList.contains("is-open"));
  495.         });
  496.       });
  497.       catalogSideLinks.forEach((link) => {
  498.         link.addEventListener("click", () => {
  499.           catalogSideLinks.forEach((item) => item.classList.toggle("is-active", item === link));
  500.         });
  501.       });
  502.       catalogLayer?.querySelectorAll('a[href="#"]').forEach((link) => {
  503.         link.addEventListener("click", (event) => event.preventDefault());
  504.       });
  505.       document.addEventListener("pointerdown", (event) => {
  506.         if (!catalogLayer?.classList.contains("is-open")) return;
  507.         if (catalogLayer.contains(event.target) || catalogTriggers.some((trigger) => trigger.contains(event.target))) return;
  508.         setCatalogOpen(false);
  509.       });
  510.       document.addEventListener("keydown", (event) => {
  511.         if (event.key !== "Escape" || !catalogLayer?.classList.contains("is-open")) return;
  512.         setCatalogOpen(false);
  513.         catalogButton?.focus();
  514.       });
  515.       const authModal = document.getElementById("authModal");
  516.       const authDialog = authModal?.querySelector(".auth-modal-dialog");
  517.       const authScreens = new Set(["login", "register", "recover", "success", "error"]);
  518.       let authReturnFocus = null;
  519.       const syncAuthScreenState = (screen = "login") => {
  520.         if (!authModal || !authScreens.has(screen)) return;
  521.         authModal.dataset.authScreen = screen;
  522.         authModal.querySelectorAll(".auth-tab").forEach((tab) => {
  523.           const active = tab.dataset.authScreenTarget === screen;
  524.           tab.setAttribute("aria-selected", active ? "true" : "false");
  525.           tab.tabIndex = active ? 0 : -1;
  526.         });
  527.         authModal.querySelectorAll(".auth-view").forEach((view) => {
  528.           const active = view.classList.contains(`auth-view-${screen}`);
  529.           view.setAttribute("aria-hidden", active ? "false" : "true");
  530.         });
  531.       };
  532.       const setAuthScreen = (screen = "login") => {
  533.         if (!authModal || !authScreens.has(screen)) return;
  534.         syncAuthScreenState(screen);
  535.       };
  536.       const setAuthOpen = (open, screen = "login", trigger = null) => {
  537.         if (!authModal) return;
  538.         if (open) {
  539.           authReturnFocus = trigger || document.activeElement;
  540.           setProfileOpen(false);
  541.           setCatalogOpen(false);
  542.           setMobileSearchOpen(false);
  543.           setAuthScreen(screen);
  544.         }
  545.         authModal.classList.toggle("is-open", open);
  546.         authModal.setAttribute("aria-hidden", open ? "false" : "true");
  547.         authModal.toggleAttribute("inert", !open);
  548.         document.body.classList.toggle("auth-modal-open", open);
  549.         if (open) {
  550.           window.setTimeout(() => authModal.querySelector("[data-auth-close]")?.focus(), 80);
  551.         } else if (authReturnFocus && typeof authReturnFocus.focus === "function") {
  552.           authReturnFocus.focus();
  553.           authReturnFocus = null;
  554.         }
  555.       };
  556.       document.querySelectorAll("[data-auth-open]").forEach((trigger) => {
  557.         trigger.addEventListener("click", (event) => {
  558.           event.preventDefault();
  559.           setAuthOpen(true, trigger.dataset.authOpen || "login", trigger);
  560.         });
  561.       });
  562.       authModal?.querySelectorAll("[data-auth-screen-target]").forEach((control) => {
  563.         control.addEventListener("click", () => setAuthScreen(control.dataset.authScreenTarget || "login"));
  564.       });
  565.       authModal?.querySelectorAll("[data-auth-close]").forEach((control) => {
  566.         control.addEventListener("click", () => setAuthOpen(false));
  567.       });
  568.       authModal?.addEventListener("pointerdown", (event) => {
  569.         if (event.target !== authModal || authDialog?.contains(event.target)) return;
  570.         setAuthOpen(false);
  571.       });
  572.       authModal?.querySelectorAll(".auth-form").forEach((form) => {
  573.         form.addEventListener("submit", (event) => {
  574.           event.preventDefault();
  575.           if (form.dataset.authForm === "recover") {
  576.             const email = form.querySelector('input[type="email"]')?.value.trim().toLowerCase() || "";
  577.             setAuthScreen(email.includes("error") || email.includes("fail") ? "error" : "success");
  578.           }
  579.         });
  580.       });
  581.       document.addEventListener("keydown", (event) => {
  582.         if (event.key !== "Escape" || !authModal?.classList.contains("is-open")) return;
  583.         setAuthOpen(false);
  584.       });
  585.       const initialAuthScreen = (() => {
  586.         const param = new URLSearchParams(window.location.search).get("auth");
  587.         const hash = window.location.hash.startsWith("#auth-") ? window.location.hash.replace("#auth-", "") : "";
  588.         return authScreens.has(param) ? param : authScreens.has(hash) ? hash : "";
  589.       })();
  590.       setAuthScreen("login");
  591.       if (initialAuthScreen) setAuthOpen(true, initialAuthScreen);
  592.       if (window.matchMedia("(pointer: coarse)").matches) {
  593.         let lastTouchEnd = 0;
  594.         const stopZoom = (event) => {
  595.           event.preventDefault();
  596.         };
  597.         const stopMultiTouch = (event) => {
  598.           if (event.touches && event.touches.length > 1) event.preventDefault();
  599.         };
  600.         document.addEventListener("gesturestart", stopZoom, { passive: false });
  601.         document.addEventListener("gesturechange", stopZoom, { passive: false });
  602.         document.addEventListener("gestureend", stopZoom, { passive: false });
  603.         document.addEventListener("touchstart", stopMultiTouch, { passive: false });
  604.         document.addEventListener("touchmove", stopMultiTouch, { passive: false });
  605.         document.addEventListener(
  606.           "touchend",
  607.           (event) => {
  608.             const now = Date.now();
  609.             if (now - lastTouchEnd < 320) event.preventDefault();
  610.             lastTouchEnd = now;
  611.           },
  612.           { passive: false }
  613.         );
  614.       }
  615.       const amountInput = document.querySelector('input[name="amount"]');
  616.       const steamLoginInput = document.querySelector('input[name="steam-login"]');
  617.       const amountField = document.querySelector(".amount-field");
  618.       const servicesCard = document.querySelector(".services-card");
  619.       const topupQuickPopover = document.getElementById("topupQuickPopover");
  620.       const payButton = document.querySelector(".pay-btn");
  621.       const payCurrencyIcon = payButton?.querySelector(".currency-icon");
  622.       const topupMarkCurrencyIcon = document.querySelector(".currency-mark .currency-icon");
  623.       const topupAmountCurrencyIcon = document.querySelector(".amount-value .currency-icon");
  624.       const promoToggleButton = document.querySelector(".promo-small");
  625.       const promoToggleLabel = promoToggleButton?.querySelector(".promo-small-label");
  626.       const topupDetails = document.getElementById("topupDetails");
  627.       const topupPromoSection = document.getElementById("topupPromoSection");
  628.       const topupPaymentSection = document.getElementById("topupPaymentSection");
  629.       const topupPromoInput = document.getElementById("topupPromoInput");
  630.       const topupPromoApply = document.querySelector(".topup-promo-apply");
  631.       const topupPromoBenefit = document.querySelector(".topup-promo-benefit");
  632.       const topupPromoSaving = document.querySelector("[data-promo-saving]");
  633.       const topupQuickAmounts = document.querySelectorAll("[data-quick-amount]");
  634.       const topupPaymentMethods = document.querySelectorAll(".topup-payment-method");
  635.       const topupCurrencyButtons = document.querySelectorAll("[data-topup-currency]");
  636.       const topupCredit = document.querySelector("[data-topup-credit]");
  637.       const topupFeeBadge = document.querySelector("[data-topup-fee-badge]");
  638.       const topupFinalPay = document.querySelector(".topup-final-pay");
  639.       const topupResult = document.querySelector("[data-topup-result]");
  640.       const topupEndpoint = "{{ path('payment_cryptobot_create') }}";
  641.       const topupRequiresAuth = {{ user ? 'false' : 'true' }};
  642.       const maxTopupAmount = 100000;
  643.       let promoSectionOpen = false;
  644.       let paymentSectionOpen = false;
  645.       let promoApplied = false;
  646.       let selectedPaymentFee = 0;
  647.       let selectedPaymentMethod = document.querySelector(".topup-payment-method.is-selected")?.dataset.paymentMethod || "cryptobot";
  648.       let selectedTopupCurrency = document.querySelector("[data-topup-currency].is-active")?.dataset.topupCurrency || "RUB";
  649.       let quickPopoverOpen = false;
  650.       const minSteamTopupRubAmount = 50;
  651.       const topupCurrencySettings = {
  652.         RUB: { symbol: "₽", iconClass: "currency-icon-ruble", quickAmounts: [500, 1000, 2500, 5000] },
  653.         USD: { symbol: "$", iconClass: "currency-icon-dollar", quickAmounts: [5, 10, 25, 50] },
  654.         KZT: { symbol: "₸", iconClass: "currency-icon-tenge", quickAmounts: [1000, 2500, 5000, 10000] },
  655.       };
  656.       const topupCurrencyIconClasses = Object.values(topupCurrencySettings).map((settings) => settings.iconClass);
  657.       const formatNumber = (value) => new Intl.NumberFormat("ru-RU").format(Math.max(0, Math.round(value)));
  658.       const formatTopupAmount = (value) => {
  659.         const settings = topupCurrencySettings[selectedTopupCurrency] || topupCurrencySettings.RUB;
  660.         return `${formatNumber(value)} ${settings.symbol}`;
  661.       };
  662.       const setTopupCurrencyIcon = (icon, currency) => {
  663.         if (!icon) return;
  664.         const settings = topupCurrencySettings[currency] || topupCurrencySettings.RUB;
  665.         icon.classList.remove(...topupCurrencyIconClasses);
  666.         icon.classList.add(settings.iconClass);
  667.       };
  668.       const topupEscape = (value) =>
  669.         String(value ?? "")
  670.           .replaceAll("&", "&amp;")
  671.           .replaceAll("<", "&lt;")
  672.           .replaceAll(">", "&gt;")
  673.           .replaceAll('"', "&quot;")
  674.           .replaceAll("'", "&#039;");
  675.       const openTopupAuth = (trigger = topupFinalPay) => {
  676.         const returnPath = `${window.location.pathname}${window.location.search}${window.location.hash}`;
  677.         if (typeof setAuthOpen === "function") {
  678.           window.PlayPapaAuth?.setReturnPath?.(returnPath);
  679.           setAuthOpen(true, "login", trigger);
  680.           return;
  681.         }
  682.         if (window.PlayPapaAuth?.openModal?.("login", trigger, returnPath)) return;
  683.         const url = new URL("/", window.location.origin);
  684.         url.searchParams.set("auth", "login");
  685.         url.searchParams.set("redirect", returnPath);
  686.         url.searchParams.set("reason", "checkout");
  687.         window.location.assign(`${url.pathname}${url.search}`);
  688.       };
  689.       const renderTopupResult = (order, error = "") => {
  690.         if (!topupResult) return;
  691.         topupResult.hidden = false;
  692.         topupResult.classList.toggle("is-error", Boolean(error));
  693.         topupResult.classList.toggle("is-success", !error && order?.status === "completed");
  694.         if (error) {
  695.           topupResult.innerHTML = `<strong>Не получилось оформить пополнение</strong><p>${topupEscape(error)}</p>`;
  696.           return;
  697.         }
  698.         topupResult.innerHTML = `
  699.           <strong>${topupEscape(order?.status_label || "Счёт создан")}</strong>
  700.           <p>${topupEscape(order?.message || "Открываем страницу статуса и оплаты.")}</p>
  701.           ${order?.partner_order_id ? `<dl><div><dt>Partner ID</dt><dd>${topupEscape(order.partner_order_id)}</dd></div></dl>` : ""}
  702.         `;
  703.       };
  704.       const syncQuickPopover = () => {
  705.         topupQuickPopover?.classList.toggle("is-open", quickPopoverOpen);
  706.         topupQuickPopover?.setAttribute("aria-hidden", quickPopoverOpen ? "false" : "true");
  707.         topupQuickPopover?.toggleAttribute("inert", !quickPopoverOpen);
  708.         amountInput?.setAttribute("aria-expanded", quickPopoverOpen ? "true" : "false");
  709.         servicesCard?.classList.toggle("has-quick-popover", quickPopoverOpen);
  710.       };
  711.       const syncTopupExpansion = () => {
  712.         const detailsOpen = promoSectionOpen || paymentSectionOpen;
  713.         topupDetails?.classList.toggle("is-open", detailsOpen);
  714.         topupDetails?.setAttribute("aria-hidden", detailsOpen ? "false" : "true");
  715.         topupDetails?.toggleAttribute("inert", !detailsOpen);
  716.         topupPromoSection?.classList.toggle("is-visible", promoSectionOpen);
  717.         topupPromoSection?.setAttribute("aria-hidden", promoSectionOpen ? "false" : "true");
  718.         topupPromoSection?.toggleAttribute("inert", !promoSectionOpen);
  719.         promoToggleButton?.setAttribute("aria-expanded", promoSectionOpen ? "true" : "false");
  720.         if (promoToggleLabel) promoToggleLabel.textContent = promoSectionOpen ? "Скрыть промокод" : "Ввести промокод";
  721.         topupPaymentSection?.classList.toggle("is-visible", paymentSectionOpen);
  722.         topupPaymentSection?.setAttribute("aria-hidden", paymentSectionOpen ? "false" : "true");
  723.         topupPaymentSection?.toggleAttribute("inert", !paymentSectionOpen);
  724.         payButton?.setAttribute("aria-expanded", paymentSectionOpen ? "true" : "false");
  725.         servicesCard?.classList.toggle("is-payment-open", paymentSectionOpen);
  726.       };
  727.       const syncAmount = () => {
  728.         if (!amountInput) return;
  729.         let value = amountInput.value.replace(/\D/g, "");
  730.         if (value) value = String(Math.min(Number(value), maxTopupAmount));
  731.         amountInput.value = value;
  732.         amountInput.style.setProperty("--amount-digits", String(Math.max(value.length, 3)));
  733.         const numericAmount = Number(value || 0);
  734.         setTopupCurrencyIcon(topupMarkCurrencyIcon, selectedTopupCurrency);
  735.         setTopupCurrencyIcon(topupAmountCurrencyIcon, selectedTopupCurrency);
  736.         setTopupCurrencyIcon(payCurrencyIcon, selectedTopupCurrency);
  737.         if (payButton && payCurrencyIcon) {
  738.           payButton.textContent = `Оплатить ${formatNumber(numericAmount)}`;
  739.           payButton.append(payCurrencyIcon);
  740.         }
  741.         topupQuickAmounts.forEach((button) => {
  742.           button.classList.toggle("is-active", Number(button.dataset.quickAmount) === numericAmount);
  743.         });
  744.         if (topupCredit) topupCredit.textContent = formatTopupAmount(numericAmount);
  745.         const amountToPay = numericAmount * (1 + selectedPaymentFee / 100);
  746.         if (topupFinalPay) topupFinalPay.textContent = `Оплатить ${formatTopupAmount(amountToPay)}`;
  747.         if (topupFeeBadge) {
  748.           topupFeeBadge.textContent = selectedPaymentFee ? `+${selectedPaymentFee}% комиссия` : "Без комиссии";
  749.           topupFeeBadge.classList.toggle("has-fee", selectedPaymentFee > 0);
  750.         }
  751.         if (topupPromoSaving) topupPromoSaving.textContent = formatTopupAmount(numericAmount * 0.05);
  752.       };
  753.       const syncTopupCurrency = () => {
  754.         const settings = topupCurrencySettings[selectedTopupCurrency] || topupCurrencySettings.RUB;
  755.         topupCurrencyButtons.forEach((button) => {
  756.           const selected = button.dataset.topupCurrency === selectedTopupCurrency;
  757.           button.classList.toggle("is-active", selected);
  758.           button.setAttribute("aria-pressed", selected ? "true" : "false");
  759.         });
  760.         topupQuickAmounts.forEach((button, index) => {
  761.           const amount = settings.quickAmounts[index] || settings.quickAmounts[0];
  762.           button.dataset.quickAmount = String(amount);
  763.           button.textContent = formatTopupAmount(amount);
  764.         });
  765.         syncAmount();
  766.       };
  767.       amountInput?.addEventListener("input", syncAmount);
  768.       amountInput?.addEventListener("focus", () => {
  769.         quickPopoverOpen = true;
  770.         syncQuickPopover();
  771.       });
  772.       amountInput?.addEventListener("click", () => {
  773.         quickPopoverOpen = true;
  774.         syncQuickPopover();
  775.       });
  776.       amountInput?.addEventListener("keydown", (event) => {
  777.         if (event.key !== "Escape") return;
  778.         quickPopoverOpen = false;
  779.         syncQuickPopover();
  780.         amountInput.blur();
  781.       });
  782.       amountInput?.addEventListener("blur", () => {
  783.         if (!amountInput.value) amountInput.value = selectedTopupCurrency === "RUB" ? String(minSteamTopupRubAmount) : "500";
  784.         if (selectedTopupCurrency === "RUB" && Number(amountInput.value) < minSteamTopupRubAmount) {
  785.           amountInput.value = String(minSteamTopupRubAmount);
  786.         }
  787.         syncAmount();
  788.       });
  789.       promoToggleButton?.addEventListener("click", () => {
  790.         promoSectionOpen = !promoSectionOpen;
  791.         syncTopupExpansion();
  792.       });
  793.       payButton?.addEventListener("click", () => {
  794.         quickPopoverOpen = false;
  795.         syncQuickPopover();
  796.         paymentSectionOpen = true;
  797.         syncTopupExpansion();
  798.       });
  799.       topupCurrencyButtons.forEach((button) => {
  800.         button.addEventListener("click", () => {
  801.           selectedTopupCurrency = button.dataset.topupCurrency || "RUB";
  802.           const settings = topupCurrencySettings[selectedTopupCurrency] || topupCurrencySettings.RUB;
  803.           if (amountInput) amountInput.value = String(settings.quickAmounts[0]);
  804.           syncTopupCurrency();
  805.         });
  806.       });
  807.       topupQuickAmounts.forEach((button) => {
  808.         button.addEventListener("click", () => {
  809.           if (!amountInput) return;
  810.           amountInput.value = button.dataset.quickAmount || "500";
  811.           syncAmount();
  812.           if (button.closest(".topup-quick-popover") && (!paymentSectionOpen || window.matchMedia("(min-width: 768px)").matches)) {
  813.             quickPopoverOpen = false;
  814.             syncQuickPopover();
  815.           }
  816.         });
  817.       });
  818.       document.addEventListener("pointerdown", (event) => {
  819.         if (!quickPopoverOpen || amountField?.contains(event.target)) return;
  820.         if (paymentSectionOpen && window.matchMedia("(max-width: 767px)").matches) return;
  821.         quickPopoverOpen = false;
  822.         syncQuickPopover();
  823.       });
  824.       topupPaymentMethods.forEach((method) => {
  825.         method.addEventListener("click", () => {
  826.           if (method.disabled || method.getAttribute("aria-disabled") === "true") return;
  827.           selectedPaymentFee = Number(method.dataset.fee || 0);
  828.           selectedPaymentMethod = method.dataset.paymentMethod || "cryptobot";
  829.           topupPaymentMethods.forEach((item) => {
  830.             const selected = item === method;
  831.             item.classList.toggle("is-selected", selected);
  832.             item.setAttribute("aria-checked", selected ? "true" : "false");
  833.           });
  834.           syncAmount();
  835.         });
  836.       });
  837.       topupPromoApply?.addEventListener("click", () => {
  838.         if (!topupPromoInput?.value.trim()) {
  839.           topupPromoInput?.focus();
  840.           return;
  841.         }
  842.         promoApplied = true;
  843.         topupPromoBenefit?.classList.add("is-visible");
  844.         topupPromoBenefit?.setAttribute("aria-hidden", "false");
  845.         topupPromoApply.textContent = "Применено";
  846.         syncAmount();
  847.       });
  848.       topupPromoInput?.addEventListener("input", () => {
  849.         if (!promoApplied) return;
  850.         promoApplied = false;
  851.         topupPromoBenefit?.classList.remove("is-visible");
  852.         topupPromoBenefit?.setAttribute("aria-hidden", "true");
  853.         if (topupPromoApply) topupPromoApply.textContent = "Применить";
  854.       });
  855.       topupFinalPay?.addEventListener("click", async () => {
  856.         if (topupRequiresAuth) {
  857.           openTopupAuth(topupFinalPay);
  858.           return;
  859.         }
  860.         const steamLogin = steamLoginInput?.value.trim() || "";
  861.         const amount = amountInput?.value.trim() || "";
  862.         if (!steamLogin) {
  863.           renderTopupResult(null, "Введите логин Steam.");
  864.           steamLoginInput?.focus();
  865.           return;
  866.         }
  867.         if (!amount || Number(amount) <= 0) {
  868.           renderTopupResult(null, "Введите сумму пополнения.");
  869.           amountInput?.focus();
  870.           return;
  871.         }
  872.         if (selectedTopupCurrency === "RUB" && Number(amount) < minSteamTopupRubAmount) {
  873.           renderTopupResult(null, `Минимальная сумма Steam-пополнения: ${minSteamTopupRubAmount} ₽.`);
  874.           amountInput?.focus();
  875.           return;
  876.         }
  877.         if (selectedPaymentMethod !== "cryptobot") {
  878.           renderTopupResult(null, "Этот способ оплаты пока не подключен.");
  879.           return;
  880.         }
  881.         const originalText = topupFinalPay.textContent;
  882.         topupFinalPay.disabled = true;
  883.         topupFinalPay.textContent = "Создаем счёт...";
  884.         renderTopupResult({ status_label: "Проверяем Steam", message: "Проверяем логин и сумму в GalaxyLink." });
  885.         try {
  886.           const response = await fetch(topupEndpoint, {
  887.             method: "POST",
  888.             headers: {
  889.               Accept: "application/json",
  890.               "Content-Type": "application/json",
  891.             },
  892.             body: JSON.stringify({
  893.               service: "steam_topup",
  894.               steam_login: steamLogin,
  895.               amount,
  896.               currency: selectedTopupCurrency,
  897.               payment: selectedPaymentMethod,
  898.               promo_code: topupPromoInput?.value.trim() || "",
  899.             }),
  900.           });
  901.           const payload = await response.json().catch(() => ({}));
  902.           if (response.status === 401 && payload.auth_required) {
  903.             openTopupAuth(topupFinalPay);
  904.             return;
  905.           }
  906.           if (!response.ok || !payload.ok) throw new Error(payload.error || `HTTP ${response.status}`);
  907.           if (payload.status_url) {
  908.             renderTopupResult({ status_label: "Счёт создан", message: "Открываем страницу статуса. Там будет кнопка оплаты и автообновление GalaxyLink." });
  909.             window.location.href = payload.status_url;
  910.             return;
  911.           }
  912.           if (payload.redirect_url) {
  913.             renderTopupResult({ status_label: "Переходим к оплате", message: "Открываем CryptoBot." });
  914.             window.location.href = payload.redirect_url;
  915.             return;
  916.           }
  917.           renderTopupResult(payload.order);
  918.         } catch (error) {
  919.           renderTopupResult(null, error.message || "Неизвестная ошибка оформления.");
  920.         } finally {
  921.           topupFinalPay.disabled = false;
  922.           topupFinalPay.textContent = originalText;
  923.         }
  924.       });
  925.       syncTopupExpansion();
  926.       syncQuickPopover();
  927.       syncTopupCurrency();
  928.       const services = [
  929.         ["service-steam.png", "Steam"],
  930.         ["service-telegram.png", "Telegram"],
  931.         ["service-roblox.png", "Roblox"],
  932.         ["service-brawl.png", "Brawl Stars"],
  933.         ["service-pubg.png", "PUBG Mob..."],
  934.         ["service-appstore.png", "App Store"],
  935.         ["service-chatgpt.png", "ChatGPT"],
  936.         ["service-playstation.png", "PlayStation"],
  937.         ["service-tiktok.png", "TikTok"],
  938.         ["service-mobile.png", "Mobile Leg..."],
  939.       ];
  940.       const servicesRow = document.getElementById("servicesRow");
  941.       if (servicesRow) {
  942.         services.forEach(([icon, label, active]) => {
  943.           servicesRow.insertAdjacentHTML(
  944.             "beforeend",
  945.             `<button class="service-item ${active ? "is-active" : ""}"><img src="./assets/${icon}" alt=""><span>${label}</span></button>`
  946.           );
  947.         });
  948.         servicesRow.insertAdjacentHTML(
  949.           "beforeend",
  950.           `<button class="service-item more-service"><span class="more-icon"><img class="more-icon-img" src="./assets/more-service.svg" alt=""></span><span>еще 841</span></button>`
  951.         );
  952.       }
  953.       const discountPattern = ["red", "orange", "red", "green", "red"];
  954.       const escapeHtml = (value) =>
  955.         String(value ?? "")
  956.           .replaceAll("&", "&amp;")
  957.           .replaceAll("<", "&lt;")
  958.           .replaceAll(">", "&gt;")
  959.           .replaceAll('"', "&quot;")
  960.           .replaceAll("'", "&#039;");
  961.       const catalogProducts = Array.isArray(window.playpapaCatalog?.products) ? window.playpapaCatalog.products : [];
  962.       const fallbackProducts = [
  963.         {
  964.           name: "DOOM 2016 STEAM KEY РФ+СНГ",
  965.           image_url: "./assets/product-pubg.png",
  966.           price_label: "990 ₽",
  967.           url: "/product",
  968.         },
  969.       ];
  970.       const renderProducts = catalogProducts.length ? catalogProducts : fallbackProducts;
  971.       const productMarkup = (product, color, index) => `
  972.         <article class="product-card">
  973.           <div class="product-cover">
  974.             <img src="${escapeHtml(product.image_url || "./assets/product-pubg.png")}" alt="">
  975.             ${product.discount_label ? `<span class="discount ${color}">${escapeHtml(product.discount_label)}</span>` : ""}
  976.           </div>
  977.           <div class="product-body">
  978.             <h3>${escapeHtml(product.name || "Товар PlayPapa")}</h3>
  979.             <div class="price-line"><strong>${escapeHtml(product.price_label || "Цена уточняется")}</strong></div>
  980.             <a href="${escapeHtml(product.url || "/product")}">Купить</a>
  981.           </div>
  982.         </article>
  983.       `;
  984.       document.querySelectorAll("[data-products]").forEach((grid) => {
  985.         const items = Array.from({ length: 5 }, (_, i) =>
  986.           productMarkup(renderProducts[i % renderProducts.length], discountPattern[i % discountPattern.length], i)
  987.         ).join("");
  988.         grid.innerHTML = items;
  989.       });
  990.     </script>
  991. {% endblock %}