PageSourceSearch

https://discours.netlify.app/dogma

html discours.netlify.app collected 2026-10-03 10:39:25 UTC 179,832 bytes, 2,173 lines download raw bytes

1<!doctype html>
2<html class="no-js" lang="ru" id="discours-app" ng-controller="HeadCtrl as headCtrl" hreflang-tags>
3  <head>
4    <!-- Core meta: charset must be first -->
5    <meta charset="utf-8">
6
7    <!-- Storage shim: в Chrome (Block-third-party-cookies строгий режим), Brave, in-app
8         браузерах (Android WebView, Facebook, DuckDuckGo) и iframe-sandbox обращение к
9         localStorage/sessionStorage кидает SecurityError DOMException.code=18.
10         Vendor-библиотеки (Angular core, Asteroid) читают storage без try/catch и
11         падают с unhandled error — Sentry FRONTEND-KR/MB. Подменяем на in-memory shim
12         ДО любого другого скрипта (включая инлайн ниже и main bundle), чтобы все
13         читатели работали без падений. Persistence теряется в пределах одной сессии,
14         что приемлемо для privacy-mode/in-app сценариев. -->
15    
15<script>
16      (function () {
17        function makeMemoryStorage() {
18          var store = Object.create(null);
19          return {
20            getItem: function (k) {
21              return Object.prototype.hasOwnProperty.call(store, k) ? store[k] : null;
22            },
23            setItem: function (k, v) {
24              store[k] = String(v);
25            },
26            removeItem: function (k) {
27              delete store[k];
28            },
29            clear: function () {
30              store = Object.create(null);
31            },
32            key: function (i) {
33              return Object.keys(store)[i] || null;
34            },
35            get length() {
36              return Object.keys(store).length;
37            },
38          };
39        }
40        function isUsable(name) {
41          try {
42            var s = window[name];
43            if (!s) return false;
44            var probe = '__d_probe__';
45            s.setItem(probe, probe);
46            s.removeItem(probe);
47            return true;
48          } catch (_) {
49            return false;
50          }
51        }
52        function patch(name) {
53          if (isUsable(name)) return;
54          try {
55            Object.defineProperty(window, name, {
56              value: makeMemoryStorage(),
57              writable: false,
58              configurable: true,
59            });
60          } catch (_) {
61            /* в редких браузерах defineProperty запрещён — Sentry ignoreErrors поймает */
62          }
63        }
64        patch('localStorage');
65        patch('sessionStorage');
66      })();
67    </script>
67
68
69    <!-- Assets/media domains: определяем ДО preconnect и body-скриптов (LCP-оптимизация) -->
70    <!-- MUST run before any inline script that reads window.discours.ASSETS_DOMAIN -->
71    <!-- MEDIA_DOMAIN — прямые файлы (аудио, документы) с R2 custom domain;
72         на зеркалах используется незаблокированный media.dscrs.site (та же логика,
73         что ASSETS_DOMAIN). Потребляется generate-config.js → constant S3_SERVER. -->
74    
74<script>
75      window.discours = window.discours || {};
76      (function () {
77        var main = false;
78        try {
79          var h = window.location.hostname;
80          main = /(?:^|\.)discours\.io$/.test(h) || /^(?:localhost|127\.0\.0\.1)$/.test(h);
81        } catch (_) {}
82        window.discours.ASSETS_DOMAIN = main
83          ? 'https://assets.discours.io'
84          : 'https://assets.dscrs.site';
85        window.discours.MEDIA_DOMAIN = main
86          ? 'https://media.discours.io'
87          : 'https://media.dscrs.site';
88      })();
89    </script>
89
90
91    <!-- Performance: Preconnect to critical origins FIRST (before any other resources) -->
92    <!-- Placed immediately after charset for maximum benefit - starts TCP/TLS handshake early -->
93    <link rel="preconnect" href="https://api.discours.io" crossorigin>
94    <!-- Assets: NO crossorigin — images загружаются через <img> (non-CORS).
95         crossorigin preconnect создаёт CORS-соединение, непригодное для non-CORS image requests.
96         Подключаем только нужный домен — лишний preconnect тратит TCP-соединение впустую. -->
97    
97<script>
98      (function () {
99        var l = document.createElement('link');
100        l.rel = 'preconnect';
101        l.href = window.discours.ASSETS_DOMAIN;
102        document.head.appendChild(l);
103      })();
104    </script>
104
105    <!-- Локаль ДО загрузки Angular.
106         В разметке lang="ru" захардкожен, а hreflangTagsDirective.js выставляет
107         реальную локаль только после bootstrap. До этого момента страница /en
108         объявлена скринридерам и краулерам как русская. Ставим lang сразу —
109         значение то же, что вернёт localeService.getLocale(), так что позже
110         директива просто подтвердит его (идемпотентно). -->
111    
111<script>
112      (function () {
113        var p = window.location.pathname;
114        if (p === '/en' || p.indexOf('/en/') === 0) {
115          document.documentElement.lang = 'en';
116        }
117      })();
118    </script>
118
119
120    <!-- dns-prefetch не нужен для api/assets: preconnect выше уже включает DNS resolution. -->
121    <!-- dns-prefetch для видеосервисов: разрешаем DNS заранее, чтобы ускорить первый Play -->
122    <link rel="dns-prefetch" href="https://www.youtube.com">
123    <link rel="dns-prefetch" href="https://player.vimeo.com">
124    <link rel="dns-prefetch" href="https://img.youtube.com">
125
126    <!-- Core meta continued -->
127    <meta name="viewport" content="width=device-width,initial-scale=1">
128    <!-- Только light: тёмная тема CSS не готова, "light dark" вызывает белые вспышки при загрузке -->
129    <meta name="color-scheme" content="light">
130    <meta content="IE=edge" http-equiv="X-UA-Compatible">
131    <!-- Referer наружу — только origin, без пути и параметров. Для современных браузеров
132         это их же поведение по умолчанию, а для старых (Android 8-9, Windows 7 — заметная
133         часть читателей из-под блокировок) закрывает утечку: с 20.08.2026 портал VPN
134         открывает переписку по ссылке из письма `/vpn?ticket=<uuid>`, а этот номер даёт
135         доступ к разговору. По старому дефолту `no-referrer-when-downgrade` он уехал бы
136         целиком в логи любого внешнего сайта, на который человек кликнет со страницы. -->
137    <meta name="referrer" content="strict-origin-when-cross-origin">
138    <base href="/">
139
140    <!-- SEO: Primary meta tags -->
141    <title>Дискурс – открытый журнал о культуре, науке и обществе</title>
142    <meta name="description" content="Независимая медиаплатформа о культуре, науке, искусстве и обществе с открытой горизонтальной редакцией.">
143    <meta name="keywords" content="журнал, Дискурс, наука, культура, искусство, общество, статьи, интервью, репортажи, исследования, сообщество, литература, музыка, видео, фотография, живопись, discours.io">
144
145    <!-- SEO: Google Discover optimization - allow large previews for better visibility -->
146    <!-- max-image-preview:large - enables large image thumbnails in Discover -->
147    <!-- max-snippet:-1 - allows full text snippets (no character limit) -->
148    <!-- max-video-preview:-1 - allows full video previews (for expo/video content) -->
149    <meta name="robots" content="max-image-preview:large, max-snippet:-1, max-video-preview:-1">
150
151    <!-- SEO: Open Graph -->
152    <!-- og:site_name — один из сигналов, по которым Google выбирает «имя сайта»
153         в выдаче (строка над заголовком сниппета). Сигналов четыре: WebSite.name
154         из JSON-LD, этот тег, <title> главной и заголовки. Расходятся —
155         Google показывает домен, и до 18.08.2026 так и было: «discours.io»
156         вместо «Дискурс», потому что здесь стояло латинское «Discours», а
157         WebSite.name (воркер, json-ld.js) отдавал кириллическое «Дискурс».
158         Менять только парой с WebSite.name; латинская форма живёт в
159         alternateName и подставляется сюда скриптом для /en ниже.
160         Сторож — scripts/sitename-signals.test.js. -->
161    <meta property="og:site_name" content="Дискурс">
162    <meta property="og:title" content="Дискурс – открытый журнал о культуре, науке и обществе">
163    <meta property="og:description" content="Независимая медиаплатформа о культуре, науке, искусстве и обществе с открытой горизонтальной редакцией.">
164    <!-- Со слэшем — ровно как rel=canonical ниже. Адрес главной фигурирует в
165         четырёх местах (canonical, og:url, Organization.url, WebSite.url в
166         воркере); Google сверяет их строкой, поэтому «io» и «io/» для него
167         разные адреса.
168         Тег внутри комментария не пишем намеренно: grep и внешние сканеры не
169         отличают его от настоящего — на превью PR #505 такой комментарий уже
170         прочитался как второй, пустой canonical. -->
171    <meta property="og:url" content="https://discours.io/">
172    <meta property="og:image" content="https://discours.io/images/opengraph-logo-black.png">
173    <meta property="og:locale" content="ru_RU">
174    <meta property="og:type" content="website">
175    <meta property="fb:app_id" content="3004103336427826">
176    <meta property="fb:pages" content="1430627470564984">
177
178    <!-- SEO: Twitter Card (обновляется Worker'ом для статей) -->
179    <meta name="twitter:card" content="summary">
180    <meta name="twitter:title" content="Дискурс – открытый журнал о культуре, науке и обществе">
181    <meta name="twitter:description" content="Независимая медиаплатформа о культуре, науке, искусстве и обществе с открытой горизонтальной редакцией.">
182    <meta name="twitter:image" content="https://discours.io/images/opengraph-logo-black.png">
183
184    <!-- SEO: RSS -->
185    <link rel="alternate" type="application/rss+xml" title="RSS лента" href="/rss">
186    <link rel="image_src" href="/images/opengraph-logo-black.png">
187
188    <!-- Дефолтные <title>/<meta> и заголовок RSS на английском для /en.
189         Эти элементы читаются браузером и краулером до bootstrap Angular,
190         поэтому фильтр `t` здесь неприменим — он показал бы сырую интерполяцию
191         во вкладке. Подменяем сразу после того, как элементы разобраны парсером;
192         lang уже выставлен скриптом выше. На конкретной странице <title> и
193         описания всё равно перезапишет update-meta — это значение только для
194         первой отрисовки и для страниц без собственных мет. -->
195    
195<script>
196      (function () {
197        if (document.documentElement.lang !== 'en') return;
198
199        var TITLE = 'Discours — an open journal on culture, science, and society';
200        var DESC =
201          'An independent media platform on culture, science, art, and society with an open horizontal editorial board.';
202        var KEYWORDS =
203          'journal, Discours, science, culture, art, society, articles, interviews, reports, research, community, literature, music, video, photography, painting, discours.io';
204
205        document.title = TITLE;
206        var rss = document.querySelector('link[rel="alternate"][type="application/rss+xml"]');
207        if (rss) rss.setAttribute('title', 'RSS feed');
208
209        var META = [
210          ['name', 'description', DESC],
211          ['name', 'keywords', KEYWORDS],
212          ['property', 'og:title', TITLE],
213          ['property', 'og:description', DESC],
214          ['property', 'og:locale', 'en_US'],
215          // Латинская форма имени — только для англоязычной версии. В русской
216          // выдаче имя сайта кириллическое, см. комментарий у og:site_name выше.
217          ['property', 'og:site_name', 'Discours'],
218          ['name', 'twitter:title', TITLE],
219          ['name', 'twitter:description', DESC],
220        ];
221        for (var i = 0; i < META.length; i++) {
222          var el = document.querySelector('meta[' + META[i][0] + '="' + META[i][1] + '"]');
223          if (el) el.setAttribute('content', META[i][2]);
224        }
225      })();
226    </script>
226
227
228    <!-- SEO: Canonical (обновляется динамически через headCtrl.js) -->
229    <link rel="canonical" href="https://discours.io/">
230
231    <!-- SEO: Organization (статично, на всех страницах). @id связывает сюда все
232         Article/CollectionPage/Person (publisher, worksFor) — один узел Knowledge
233         Graph вместо конкурирующих описаний. Разбор каждого поля и замеры —
234         docs/seo-investigations/plan-2026-08-04.md, §2.7. Гейт —
235         scripts/organization-jsonld.test.js (пути живые и не под редиректом,
236         якорь на месте, размеры логотипа совпадают с файлом).
237
238         Три вещи, без которых правку ломают повторно:
239         1. logo корневой Organization и publisher.logo ВНУТРИ Article — разные
240            поля с разными требованиями. Здесь годится квадратный logo-small.png
241            (283×288); logo.png — OG-обложка 1200×630, не подходит. В воркере и в
242            angular-json-ld.js publisher БЕЗ logo, и это верно: они ссылаются по @id.
243         2. Каждое свойство доверия ведёт на страницу, которая его действительно
244            описывает. Правдоподобный, но нерелевантный URL хуже отсутствия поля.
245         3. /help-us даёт 301 на /help — ссылка на редирект деградирует в
246            «Page with redirect», поэтому ownershipFundingInfo указывает на /help.
247         4. В alternateName не должно быть доменоподобных форм. «Discours.io»
248            стояло здесь до 18.08.2026 и работало против нас: Google выбирает
249            имя сайта из наших же кандидатов, и мы сами предлагали ему адрес
250            вместо названия. Остаётся одна латинская форма — «Discours». -->
251    
251<script type="application/ld+json">
252      {
253        "@context": "https://schema.org",
254        "@type": "NewsMediaOrganization",
255        "@id": "https://discours.io/#organization",
256        "name": "Дискурс",
257        "alternateName": ["Discours"],
258        "url": "https://discours.io/",
259        "logo": {
260          "@type": "ImageObject",
261          "url": "https://discours.io/images/logo-small.png",
262          "width": 283,
263          "height": 288
264        },
265        "description": "Независимое издание с открытой горизонтальной редакцией о культуре, обществе, науке и искусстве. С 2015 года более 1100 авторов из 80+ городов публикуют статьи, эссе, литературу, музыку, видео и изображения.",
266        "foundingDate": "2015",
267        "knowsLanguage": ["ru-RU", "en"],
268        "sameAs": [
269          "https://t.me/discoursio",
270          "https://vk.com/discoursio",
271          "https://facebook.com/discoursio",
272          "https://twitter.com/discours_io",
273          "https://youtube.com/@discoursio",
274          "https://instagram.com/discours.io"
275        ],
276        "publishingPrinciples": "https://discours.io/dogma",
277        "ethicsPolicy": "https://discours.io/dogma",
278        "diversityPolicy": "https://discours.io/principles",
279        "correctionsPolicy": "https://discours.io/dogma",
280        "verificationFactCheckingPolicy": "https://discours.io/dogma",
281        "actionableFeedbackPolicy": "https://discours.io/guide#editing",
282        "missionCoveragePrioritiesPolicy": "https://discours.io/manifest",
283        "masthead": "https://discours.io/guide",
284        "ownershipFundingInfo": "https://discours.io/help",
285        "contactPoint": {
286          "@type": "ContactPoint",
287          "email": "[email protected]",
288          "contactType": "editorial",
289          "availableLanguage": "ru-RU"
290        }
291      }
292    </script>
292
293
294    <!-- Prerender status for Angular routes -->
295    <meta name="prerender-status-code" content="{{ $root.statusCode }}" ng-if="$root.statusCode">
296
297    <!-- Favicon -->
298    <link rel="apple-touch-icon" sizes="180x180" href="/images/favicon/apple-touch-icon.png">
299    <link rel="icon" type="image/png" sizes="32x32" href="/images/favicon/favicon-32x32.png">
300    <link rel="icon" type="image/png" sizes="192x192" href="/images/favicon/android-chrome-192x192.png">
301    <link rel="icon" type="image/png" sizes="16x16" href="/images/favicon/favicon-16x16.png">
302    <link rel="manifest" href="/images/favicon/site.webmanifest">
303    <link rel="mask-icon" href="/images/favicon/safari-pinned-tab.svg" color="#2e2e2e">
304    <meta name="msapplication-TileColor" content="#ffffff">
305    <meta name="msapplication-TileImage" content="/images/favicon/mstile-144x144.png">
306    <meta name="theme-color" content="#ffffff">
307
308    <!-- DNS prefetch only for media and GTM — preconnect removed per PageSpeed audit:
309         - media: прямые файлы (аудио/документы) с media-домена нужны не на каждой
310           странице, поэтому лёгкий dns-prefetch вместо preconnect; домен зависит
311           от зеркальности (MEDIA_DOMAIN из head-скрипта выше)
312         - GTM: loaded via requestIdleCallback after page load; preconnect expires (~10s)
313           before the request is made, wasting a TCP/TLS handshake -->
314    
314<script>
315      (function () {
316        var l = document.createElement('link');
317        l.rel = 'dns-prefetch';
318        l.href = window.discours.MEDIA_DOMAIN;
319        document.head.appendChild(l);
320      })();
321    </script>
321
322    <link rel="dns-prefetch" href="https://www.googletagmanager.com">
323    <link rel="dns-prefetch" href="https://cloud.mave.digital">
324
325    <!-- Критические шрифты: preload разрывает цепочку CSS → fonts (Lighthouse chain ~4.4s).
326         Early Hints в worker обеспечивают параллельную загрузку, но Lighthouse их не видит.
327         Preload даёт гарантию начала загрузки до парсинга CSS, ускоряя FCP на ~500ms.
328         Service Worker (CacheFirst) на повторных визитах обслуживает из кеша — preload
329         не вызовет warning, т.к. шрифты реально используются в @font-face. -->
330    <link rel="preload" href="/fonts/Muller/MullerRegular.woff2" as="font" type="font/woff2" crossorigin>
331    <link rel="preload" href="/fonts/Spectral/Spectral-Regular.woff2" as="font" type="font/woff2" crossorigin>
332    <!-- Muller Bold: заголовок H1 статей и карточек — above-fold на каждой странице.
333         Preload разрывает цепочку HTML→CSS→font (PageSpeed: fonts стартовали на ~1с позже CSS). -->
334    <link rel="preload" href="/fonts/Muller/Muller-Bold.woff2" as="font" type="font/woff2" crossorigin>
335    <!-- Muller Medium, Spectral Bold/Italic/SemiBold: не preload — secondary шрифты, swap приемлем;
336         каждый лишний preload конкурирует с LCP hero-картинкой за полосу. -->
337    <!-- Iconfont: не preload — иконки не критичны для LCP -->
338    <!-- FontAwesome: не preload — меню-иконки не нужны для первого paint -->
339
340    <!-- footer.webp preload removed — loading overlay image is decorative, not LCP.
341         Preloading it competes with the hero article image (actual LCP element) for bandwidth.
342         The overlay image loads naturally from the <picture> element in body. -->
343    <!-- Logo preloads removed — small SVGs (~1-2 KB) that load fast without preload.
344         Each preload hint consumes a connection slot and competes with the LCP hero image.
345         On mobile with limited bandwidth, fewer preloads = faster LCP. -->
346
347    <!-- LCP hero-image preload инжектит Cloudflare Worker
348         (cloudflare/workers/src/rewriters.js: HomepageHeadHandler / ArticleHeadHandler).
349         Netlify edge functions удалены — инжектили дублирующий preload с 2560x,
350         которой нет в Angular hero-preset, и давали warning в консоли. -->
351
352    <!-- CSS preload убран: <link rel="stylesheet"> уже в <head> (строка ~268),
353         preload-scanner браузера находит его мгновенно. Дублирующий preload вызывал
354         "preloaded but not used" warning при работе Service Worker. -->
355
356    <!-- FontAwesome: Hide icons until font is loaded to prevent broken squares -->
357    <style>.fa,.fab,.far,.fas{visibility:hidden}.fa-loaded .fa,.fa-loaded .fab,.fa-loaded .far,.fa-loaded .fas{visibility:visible}</style>
358    
358<script>
359      (function () {
360        if (document.fonts && document.fonts.load) {
361          Promise.all([
362            document.fonts.load('900 1em "Font Awesome 5 Free"'),
363            document.fonts.load('400 1em "Font Awesome 5 Brands"'),
364          ])
365            .then(function () {
366              document.documentElement.classList.add('fa-loaded');
367            })
368            .catch(function () {
369              document.documentElement.classList.add('fa-loaded');
370            });
371          setTimeout(function () {
372            document.documentElement.classList.add('fa-loaded');
373          }, 3000);
374        } else {
375          document.documentElement.classList.add('fa-loaded');
376        }
377      })();
378    </script>
378
379    <!-- Pre-Angular auth detection: предсказываем compact header для залогиненных
380         пользователей на контентных страницах (не главная). Предотвращает CLS-прыжок
381         header'а с 120px → 75px при загрузке Angular. Удаляется в header.js. -->
382    
382<script>
383      (function () {
384        try {
385          var ls = window.localStorage;
386          var p = window.location.pathname;
387          if (
388            ls &&
389            ls.getItem('ls.userId') &&
390            ls.getItem('ls.authToken') &&
391            p !== '/' &&
392            p.indexOf('/edit/') !== 0 &&
393            p.indexOf('/connect') !== 0 &&
394            p.indexOf('/vpn') !== 0 &&
395            p !== '/quotes'
396          ) {
397            document.documentElement.classList.add('auth-hint');
398          }
399          // На не-главных страницах theme-menu не нужен — прячем до Angular boot
400          // чтобы ~45px пустого пространства не исчезало при ng-show=false
401          if (p !== '/') {
402            document.documentElement.classList.add('hide-theme-menu');
403          }
404          // Restore cached compact header height — устраняет 5px jump
405          // на следующих загрузках после первого измерения в header.js
406          var rawH = ls && ls.getItem('compactHeaderH');
407          if (rawH) {
408            try {
409              var parsed = JSON.parse(rawH);
410              // Инвалидируем кэш при смене версии (deploy) или через 2 дня
411              var isVersionMatch = !parsed.v || parsed.v === '12.17.14';
412              var isFresh = Date.now() - parsed.t < 172800000; // 48 часов
413              var isValidRange = parsed.h >= 40 && parsed.h <= 200;
414              if (parsed && parsed.h && isVersionMatch && isFresh && isValidRange) {
415                document.documentElement.style.setProperty('--compact-header-h', parsed.h + 'px');
416              }
417            } catch (_e) {
418              // Legacy формат (plain number) — применяем и перезапишем
419              if (/^\d+$/.test(rawH)) {
420                document.documentElement.style.setProperty('--compact-header-h', rawH + 'px');
421              }
422            }
423          }
424        } catch (e) {
425          /* localStorage может быть недоступен в private mode */
426        }
427      })();
428    </script>
428
429
430    <!-- JS preload hints REMOVED to free bandwidth for LCP hero image.
431         With `defer`, scripts download in parallel with HTML parsing automatically.
432         Preloading them steals bandwidth from the hero image on mobile connections,
433         delaying LCP by 200-500ms. The browser's priority scheduler handles defer
434         scripts well without preload hints. -->
435
436    <!-- Print styles: loaded only when printing (non-render-blocking) -->
437    <link rel="stylesheet" href="/styles/print.css" media="print">
438
439    <!-- CSS: Non-critical vendor styles (async loading to avoid render blocking) -->
440    <!-- CSP: onload-обработчики заменены на скрипт ниже (async-css класс) -->
441    <!-- Slick, rn-carousel и rzslider — один хешированный файл вместо трёх
442         <link> на /node_modules/... (пути без хеша нельзя кешировать надолго).
443         Источник: app/styles/vendor-async.scss. Гейт: vendor-css-pipeline.test.js. -->
444    <link rel="stylesheet" href="/styles/vendor-async-805ff4f7.css" media="print" class="async-css">
445    <!-- animate.css removed - only fadeIn was used, now in animation.scss (saves 71KB) -->
446    <!-- Notifications: base colors + layout теперь в partials/components/ui-notification.scss;
447         старый vendor CSS удалён вместе с angular-ui-notification npm package. -->
448    <!-- Loading bar: styles already in main.css via angular-loading-bar.scss -->
449    <!-- Font Awesome: now bundled as subset in main.css (~5KB vs ~40KB), saves 35KB -->
450
451    <!-- rzslider переехал в vendor-async выше (был отдельным запросом). -->
452    <!-- CodeMirror CSS убран отсюда: 10.9 КБ на каждой странице ради редактора,
453         который открывают единицы. codemirror.css грузит loadEditorScripts.js по
454         требованию; monokai.css не нужен вовсе — html-editor-modal.js инжектит
455         тему сам, но отключался, найдя тут link[href*="monokai"]. -->
456    <!-- videogular удалён в Phase 5 (2026-04-19); native HTML5 <video> не требует
457         дополнительного CSS — браузер даёт стандартные controls и layout. -->
458
459    <!-- Активация async CSS: заменяет inline onload-обработчики для совместимости с nonce-based CSP.
460         window.__armAsyncCss переиспользуется скриптом secondary.css ниже — при правке
461         паттерна (retry, телеметрия, CSP-nonce) поведение меняется в одном месте. -->
462    
462<script>
463      (function () {
464        window.__armAsyncCss = function (link) {
465          if (link.sheet) {
466            link.media = 'all';
467            return;
468          }
469          var activate = function () {
470            link.onload = link.onerror = null;
471            link.media = 'all';
472          };
473          link.onload = activate;
474          // onerror: если CSS не загрузился — всё равно переключаем media,
475          // чтобы браузер мог показать fallback и не зависал на media="print"
476          link.onerror = activate;
477        };
478        var links = document.querySelectorAll('link.async-css');
479        for (var i = 0; i < links.length; i++) {
480          window.__armAsyncCss(links[i]);
481        }
482      })();
483    </script>
483
484    <!-- noscript CSS fallback удалён: приложение — Angular SPA, без JS не работает.
485         Блок добавлял ~800 байт в HTML без пользы. -->
486
487    <!-- Critical inline styles for first paint (CLS prevention).
488         ВАЖНО: этот <style> блок ДОЛЖЕН быть ДО внешних <link> таблиц стилей!
489         Причина: CSS cascade — при одинаковой специфичности побеждает позднее правило.
490         Если inline-стили (напр. .tw\:w-full) идут ПОСЛЕ tailwind.css, они перебивают
491         responsive-варианты (tw:md:w-7/12), ломая grid-раскладку на десктопе.
492         Preload scanner браузера найдёт <link> теги даже после <style> блока. -->
493    <style>*,::after,::before{box-sizing:border-box}html{-webkit-text-size-adjust:100%;overflow-y:scroll;scrollbar-gutter:stable}@media (max-width:767px){html{overflow-y:auto;scrollbar-gutter:auto;overflow-x:hidden}}body{margin:0;background-color:#fff}.ng-cloak,[data-ng-cloak],[ng-cloak]{display:none!important}.ng-hide:not(.ng-hide-animate){display:none!important}html.has-ssr-content .loading-footer-overlay{display:none!important}html.has-ssr-content .main-content{min-height:auto}html.has-ssr-content .expo-item-title-image,html.has-ssr-content .open-post-title-image{opacity:1!important;transition:none!important}html.ssr-just-swapped img{opacity:1!important;transition:none!important}html.ssr-just-swapped .loading-footer-overlay{display:none!important}#ssr-about.ssr-hiding,#ssr-article.ssr-hiding,#ssr-author.ssr-hiding,#ssr-homepage.ssr-hiding,#ssr-tag.ssr-hiding{display:none!important}#ssr-about:not(.ssr-hiding)~ng-view,#ssr-article:not(.ssr-hiding)~ng-view,#ssr-author:not(.ssr-hiding)~ng-view,#ssr-homepage:not(.ssr-hiding)~ng-view,#ssr-tag:not(.ssr-hiding)~ng-view{display:none!important}html.has-ssr-content .author-post .article-image img{opacity:1!important;transition:none!important}#ssr-h1{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);clip-path:inset(50%);border:0;white-space:nowrap}#ssr-tag .ssr-tag-header{text-align:center;margin-bottom:20px}#ssr-tag .ssr-tag-title{font-family:Spectral,Georgia,'Times New Roman',Times,serif;font-weight:400;font-size:42px;font-style:italic;text-align:center;margin:50px 0 16px;color:#000}@media (max-width:662px){#ssr-tag .ssr-tag-title{font-size:30px}}#ssr-tag .ssr-tag-count{font-weight:400;font-family:Muller,'Helvetica Neue',Helvetica,Arial,sans-serif;font-style:normal;font-size:14px;vertical-align:super}#ssr-tag .ssr-tag-desc{margin:0 auto 16px;max-width:700px;text-align:center}@media (max-width:767px){#ssr-tag .ssr-tag-desc{margin:0 5px 16px}}#ssr-tag .ssr-tag-stats{display:flex;align-items:center;justify-content:center;gap:30px;flex-wrap:wrap;min-height:40px}#ssr-tag .ssr-tag-stat{font-family:Muller,'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:14px;font-weight:500;color:#000;white-space:nowrap}#ssr-tag .ssr-tag-actions{display:flex;justify-content:center;align-items:center;gap:12px;margin-top:16px}#ssr-tag .ssr-tag-write{display:flex;justify-content:center;align-items:center;font-family:Muller,'Helvetica Neue',Helvetica,Arial,sans-serif;font-weight:500;font-size:15px;letter-spacing:-.01em;height:40px;padding:0 20px;border-radius:12px;background:#f7f7f8;color:#000;text-decoration:none;white-space:nowrap;box-sizing:border-box}#ssr-tag .ssr-tag-follow-reserve{display:inline-block;width:150px;height:40px}#ssr-tag .ssr-filter-bar{display:flex;align-items:center;justify-content:space-between;max-width:1280px;margin:20px auto 0;padding:12px 20px 16px;box-sizing:border-box;width:100%;background:#fff}@media (max-width:767px){#ssr-tag .ssr-filter-bar{display:none}}#ssr-tag .ssr-filter-tabs{display:flex;align-items:center;gap:24px}#ssr-tag .ssr-filter-tab{font-family:Muller,'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;font-weight:500;color:#000;text-decoration:none;padding:4px 0;line-height:inherit}#ssr-tag .ssr-filter-tab--active{font-weight:700}#ssr-tag .ssr-filter-dropdowns{display:flex;align-items:center;gap:16px}#ssr-tag .ssr-filter-dropdown{font-family:Muller,'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:14px;color:#000;white-space:nowrap}#ssr-tag .ssr-filter-dropdown::after{content:' \25BE';opacity:.6}#ssr-tag .ssr-grid{display:flex;flex-wrap:wrap;margin-bottom:-30px}#ssr-tag .ssr-card{box-sizing:border-box;display:block;margin-bottom:50px;padding:0 15px;width:100%}@media (min-width:660px){#ssr-tag .ssr-card:nth-child(5n-2),#ssr-tag .ssr-card:nth-child(5n-3),#ssr-tag .ssr-card:nth-child(5n-4){width:33.33%}#ssr-tag .ssr-card:nth-child(5n),#ssr-tag .ssr-card:nth-child(5n-1){width:50%}}#ssr-tag .ssr-card__img-link{display:block;position:relative;overflow:hidden;margin-bottom:10px;aspect-ratio:100/65;background-color:#f6f6f6;text-decoration:none}@supports not (aspect-ratio:1){#ssr-tag .ssr-card__img-link{height:0;padding-bottom:65%}}#ssr-tag .ssr-card__img-link img{position:absolute;width:100%;height:100%;left:0;top:0;object-fit:cover;object-position:center center}#ssr-tag .ssr-card__category{font-size:12px;font-weight:700;letter-spacing:.15em;text-transform:uppercase;display:inline-block;line-height:1}#ssr-tag .ssr-card__title{font-family:Muller,'Helvetica Neue',Helvetica,Arial,sans-serif;font-weight:700;font-size:24px;line-height:1.2;margin:.4em 0 0}#ssr-tag .ssr-card__title a{color:inherit;text-decoration:none}#ssr-tag .ssr-card__author{font-family:Spectral,Georgia,'Times New Roman',Times,serif;font-size:16px;font-style:italic;margin-top:5px;
493display:block}#ssr-tag .ssr-loadmore-wrap{width:100%;padding:0 15px;box-sizing:border-box}@media (min-width:1200px){#ssr-tag .ssr-loadmore-inner{width:66.666%;margin-left:16.666%}}#ssr-tag .ssr-loadmore-inner{margin-top:40px;margin-bottom:20px;text-align:center}#ssr-tag .ssr-loadmore{display:inline-block;font-family:Spectral,Georgia,'Times New Roman',Times,serif;font-weight:400;font-size:26px;font-style:italic;color:#000;background:#fff;text-decoration:none;padding:6px 16px}@media (max-width:767px){#ssr-tag .ssr-loadmore{font-size:24px}}header{min-height:120px;margin-bottom:10px}header.is-short{min-height:90px}header.is-logo-only{min-height:60px;margin-bottom:0}header.is-force-compact{min-height:calc(var(--compact-header-h,65px) + 10px);margin-bottom:10px}html.auth-hint header:not(.is-logo-only){min-height:calc(var(--compact-header-h,65px) + 10px)!important}@media (prefers-reduced-motion:reduce){header{transition:none}}.header{position:fixed;top:0;width:100%;z-index:1002;background:#fff}@media (max-width:767px){header{min-height:80px}header.is-short{min-height:60px}header.is-logo-only{min-height:55px}}.skip-to-content{position:absolute;left:-9999px;top:auto;z-index:1100;padding:8px 16px;background:#2638d9;color:#fff;font-family:sans-serif;font-size:14px;text-decoration:none;border-radius:0 0 4px 0}.skip-to-content:focus{left:0;top:0}.main-content{display:block;min-height:calc(100vh - 490px)}html.defer-footer-top .main-content{min-height:100vh}html.defer-footer-top .theme-menu-container,html.has-ssr-content .theme-menu-container,html.hide-theme-menu .theme-menu-container{display:none!important}@media (min-width:768px){html.has-ssr-content.has-ssr-tag .theme-menu-wrapper{display:block!important;min-height:25px;margin-top:-50px;margin-bottom:20px}}@media (min-width:992px){html.has-ssr-content.has-ssr-tag .theme-menu-wrapper{margin-top:-15px}}@media (min-width:768px){html.auth-hint.has-ssr-content.has-ssr-tag .theme-menu-wrapper,html.has-ssr-content.has-ssr-tag header.is-force-compact~.theme-menu-wrapper{margin-top:0}}html.is-editor-page .footer-wrapper{display:none!important}.loading-footer-overlay{position:fixed;top:0;right:0;bottom:0;left:0;inset:0;z-index:999;display:flex;flex-direction:column;justify-content:flex-end;background:#fff;overflow:hidden}.loading-footer-overlay__content,.loading-footer-overlay__image,.loading-footer-overlay__punchline{opacity:0;transition:opacity .15s ease-out}.loading-footer-overlay.footer-loaded .loading-footer-overlay__content,.loading-footer-overlay.footer-loaded .loading-footer-overlay__image,.loading-footer-overlay.footer-loaded .loading-footer-overlay__punchline{opacity:1}.instant-hero-article__img{display:block;width:100%;height:100%;object-fit:cover;object-position:center;position:absolute;top:0;left:0;z-index:2;color:transparent}.is-landscape .instant-hero-article__img{object-fit:cover;object-position:center}.is-portrait .instant-hero-article__img{object-fit:cover;object-position:center top}.expo-item-title-image-wrapper.is-image-loaded .instant-hero-article__img,.open-post-title-image-wrapper.is-image-loaded .instant-hero-article__img{display:none}.article--main-cover.is-image-loaded .instant-hero-homepage__img{display:none}.article{display:block;position:relative;width:100%;padding:0 15px;box-sizing:border-box}@media (max-width:767px){.article{padding:0}.article .article__inner{margin:0 15px}}.article.article--main-cover{overflow:hidden}@media (max-width:767px){.article.article--main-cover{margin-bottom:40px}}@media (min-width:992px){.article.article--main-cover{width:66.66%}}.article--main-cover .article__inner{position:relative}.article--main-cover .article__image-wrapper{display:block;position:relative;overflow:hidden;margin-bottom:0;padding-bottom:80%;height:0;background-color:#f6f6f6}@media (min-width:660px){.article--main-cover .article__image-wrapper{padding-bottom:54%}}.article--main-cover .article__image-inner{position:absolute;top:-2px;left:-2px;width:calc(100% + 4px);height:calc(100% + 4px);min-width:calc(100% + 4px);min-height:calc(100% + 4px)}.article--main-cover .article__image{position:absolute;top:-2px;left:-2px;width:calc(100% + 4px);height:calc(100% + 4px);min-width:calc(100% + 4px);min-height:calc(100% + 4px);object-fit:cover;object-position:center;color:transparent}@keyframes lcpShimmer{0%{background-position:-468px 0}100%{background-position:468px 0}}.article.article--main-cover:not(.is-image-loaded) .article__image-wrapper{background:linear-gradient(to right,#f6f6f6 0,#ececec 20%,#f6f6f6 40%,#f6f6f6 100%);background-size:800px 104px;animation:lcpShimmer 2.25s infinite ease-in-out}@media (prefers-reduced-motion:reduce){.article.article--main-cover:not(.is-image-loaded) .article__image-wrapper{animation:none}}.container{margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}@media (min-width:1300px){.container{width:1280px}}.tw\:flex{display:flex}.tw\:flex-wrap{flex-wrap:wrap}.tw\:flow-root{display:flow-root}.tw\:relative{position:relative}.tw\:block{display:block}.tw\:hidden{display:none}.tw\:w-full{width:100%}.tw\:w-3\/12{width:25%}.tw\:w-9\/12{width:75%}.tw\:h-auto{height:auto}.tw\:max-w-full{max-width:100%}.tw\:items-stretch{align-items:stretch}.tw\:mx-auto{margin-left:auto;margin-right:auto}.ds-container{margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}@media (min-width:768px){.ds-container{width:750px}}@media (min-width:992px){.ds-container{width:970px}}@media (min-width:1200px){.ds-container{width:1170px}}@media (min-width:1300px){.ds-container{width:1280px}}.ds-content-col{padding-left:15px;padding-right:15px;min-width:0;flex-shrink:0;width:100%}@media (min-width:992px){.ds-content-col{width:58.333%;margin-left:16.667%}}.expo-item .audio-wrapper{position:relative;padding-top:30px}.expo-item .audio-container{display:flex;align-items:stretch;justify-content:space-between;gap:20px}.expo-item .audio-player-list{flex:1 50%;order:2;min-width:0}.expo-item .audio-img{order:3;margin-right:0;align-self:flex-start;flex:0 0 auto;width:320px;aspect-ratio:1/1;background-color:#e0e0e0}.expo-item .audio-img .ligthbox-img{display:block;width:100%;height:auto;margin-left:0}.ssr-video-carousel-frame{width:100%}.expo-video-carousel-frame{display:grid;grid-template-columns:minmax(0,1fr)}.expo-video-carousel-frame::before{content:'';grid-area:1/1;width:100%;aspect-ratio:16/9}.expo-video-carousel-frame>ul[rn-carousel]{grid-area:1/1;min-width:0}@media (max-width:991px){.expo-video-carousel-frame::before{width:calc(100% + 2 * var(--grid-gutter-half,15px));margin-left:calc(-1 * var(--grid-gutter-half,15px))}.ssr-video-carousel-frame{margin-left:calc(-1 * var(--grid-gutter-half,15px));margin-right:calc(-1 * var(--grid-gutter-half,15px));width:calc(100% + 2 * var(--grid-gutter-half,15px))}}@media (max-width:767px){.expo-item .audio-wrapper{padding-top:0}.expo-item .audio-container{flex-wrap:wrap;gap:10px}}@media (max-width:669px){.expo-item .audio-img{order:1;width:100%}}.tw\:row-flex{display:flex;flex-wrap:wrap;margin-left:-15px;margin-right:-15px}.tw\:gutter-half{padding-left:15px;padding-right:15px}.tw\:mt-\[30px\]{margin-top:30px}.tw\:mt-2\.5{margin-top:10px}.tw\:mt-20{margin-top:80px}.tw\:mt-section{margin-top:30px}.tw\:mb-5{margin-bottom:20px}.tw\:mb-section{margin-bottom:30px}.tw\:mb-section-lg{margin-bottom:50px}.tw\:pt-content-top{padding-top:100px}.tw\:sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);clip-path:inset(50%);border:0;white-space:nowrap}@media (max-width:767.98px){.tw\:max-sm\:hidden{display:none!important}.tw\:max-sm\:inline{display:inline!important}}@media (min-width:768px){.tw\:sm\:hidden{display:none!important}.tw\:sm\:w-3\/12{width:25%}.tw\:sm\:w-4\/12{width:33.333333%}.tw\:sm\:w-5\/12{width:41.666667%}.tw\:sm\:w-6\/12{width:50%}.tw\:sm\:w-7\/12{width:58.333333%}.tw\:sm\:w-8\/12{width:66.666667%}.tw\:sm\:w-9\/12{width:75%}.tw\:sm\:w-10\/12{width:83.333333%}.tw\:sm\:w-11\/12{width:91.666667%}.tw\:sm\:ml-offset-2{margin-left:16.666667%}.tw\:sm\:ml-\[8\.333\%\]{margin-left:8.333%}.tw\:sm\:ml-\[16\.667\%\]{margin-left:16.667%}}@media (max-width:991.98px){.tw\:max-md\:hidden{display:none!important}}@media (min-width:992px){.tw\:md\:hidden{display:none!important}.tw\:md\:flex{display:flex}.tw\:md\:w-1\/2{width:50%}.tw\:md\:w-2\/12{width:16.666667%}.tw\:md\:w-3\/12{width:25%}.tw\:md\:w-4\/12{width:33.333333%}.tw\:md\:w-5\/12{width:41.666667%}.tw\:md\:w-6\/12{width:50%}.tw\:md\:w-7\/12{width:58.333333%}.tw\:md\:w-8\/12{width:66.666667%}.tw\:md\:w-10\/12{width:83.333333%}.tw\:md\:ml-offset-1{margin-left:8.333333%}.tw\:md\:ml-offset-2{margin-left:16.666667%}.tw\:md\:ml-offset-3{margin-left:25%}.tw\:md\:ml-offset-5{margin-left:41.666667%}.tw\:md\:ml-\[16\.667\%\]{margin-left:16.667%}.tw\:md\:mx-auto{margin-left:auto;margin-right:auto}}@media (min-width:1200px){.tw\:lg\:w-3\/12{width:25%}.tw\:lg\:w-5\/12{width:41.666667%}.tw\:lg\:w-7\/12{width:58.333333%}.tw\:lg\:w-8\/12{width:66.666667%}.tw\:lg\:w-9\/12{width:75%}.tw\:lg\:w-10\/12{width:83.333333%}.tw\:lg\:ml-offset-2{margin-left:16.666667%}.tw\:lg\:ml-offset-6{margin-left:50%}.tw\:lg\:inline{display:inline!important}}@media (min-width:768px) and (max-width:991px){.editor-loading-skeleton__body-col{width:100%!important;margin-left:0!important}}@media (min-width:992px) and (max-width:1199px){.editor-loading-skeleton__body-col{width:83.333%!important;margin-left:8.333%!important}}.open-post{display:flow-root}.expo-item .title,.open-post .title{font-family:Muller,'Helvetica Neue',Helvetica,Arial,sans-serif;font-weight:700;font-size:40px;line-height:44px;margin-bottom:25px;margin-left:0}@media (min-width:1200px){.expo-item .title,.open-post .title{margin-left:-130px}}@media screen and (max-width:767.98px){.expo-item .title,.open-post .title{font-size:25px;line-height:32px}}.expo-item .author-category,.open-post .author-category{position:relative;display:flex;justify-content:space-between}.expo-item .author-category::after,.open-post .author-category::after{position:absolute;z-index:-1;right:0;bottom:0;left:0;width:100%;height:7px;content:' ';background-color:#000}@media (max-width:767.98px){.expo-item .author-category::after,.open-post .author-category::after{height:0}.expo-item .author-category,.open-post .author-category{display:block}}.expo-item .author-category .category,.open-post .author-category .category{position:relative;bottom:8px}.expo-item .author-category .category a,.open-post .author-category .category a{font-weight:700;text-transform:lowercase;text-decoration:none;color:#000}.expo-item .author-category .author,.open-post .author-category .author{font-family:Spectral,Georgia,'Times New Roman',Times,serif;position:relative;padding-right:15px;padding-left:15px;height:18px;line-height:12px;bottom:-7px;left:-30px;border-right:7px solid #000;border-left:7px solid #000;vertical-align:bottom;background-color:#fff;margin-left:auto}.expo-item .author-category .author a,.open-post .author-category .author a{text-decoration:none;color:#000}@media (max-width:767px){.expo-item .author-category .category,.open-post .author-category .category{bottom:0}.expo-item .author-category .category a,.open-post .author-category .category a{color:#2638d9}.expo-item .author-category .author,.open-post .author-category .author{font-size:20px;margin-left:0;height:auto;border:none;padding:5px 0 0 0;left:0;line-height:1.1}}.expo-item .expo-item-title-image-wrapper,.open-post .open-post-title-image-wrapper{position:relative;width:calc(100% + 30px);margin-left:-15px;margin-bottom:25px;overflow:hidden;aspect-ratio:var(--cover-ratio,16 / 9);background:#f0f0f0}@supports not (aspect-ratio:1){.expo-item .expo-item-title-image-wrapper,.open-post .open-post-title-image-wrapper{height:0;padding-bottom:56.25%}}@media (min-width:768px){.expo-item .expo-item-title-image-wrapper,.open-post .open-post-title-image-wrapper{width:calc(100% + 20px);margin-left:-10px}}.expo-item .expo-item-title-image,.open-post .open-post-title-image{display:block;width:100%;height:100%;object-fit:cover;object-position:center;position:absolute;top:0;left:0;color:transparent}.expo-item-title-image-wrapper.is-image-loaded .expo-item-title-image,.open-post-title-image-wrapper.is-image-loaded .open-post-title-image{opacity:1}.post-body,.post-body p{font-family:Spectral,Georgia,'Times New Roman',Times,serif;font-size:20px;line-height:27px;letter-spacing:-.01em}.post-body{contain:layout style}@media (max-width:500px){.post-body,.post-body p{font-size:18px;line-height:24px}}
493.expo-item .image-description,.open-post .image-description{font-size:13px;contain:layout style;min-height:1.2em}.footer-post hr{border-color:#000}.footer-post .post-meta-footer{display:flex;flex-wrap:wrap;align-items:center;gap:15px;font-family:Spectral,Georgia,'Times New Roman',Times,serif;margin:10px 0}.footer-post .post-meta-footer__author{display:flex;align-items:center;gap:15px}.footer-post .author-title a,.footer-post .author-title span{color:#000;font-weight:600;white-space:nowrap}.footer-post .date-time{flex-shrink:0;white-space:nowrap;margin:0 10px}@media (max-width:767.98px){.footer-post .post-meta-footer__author{width:100%;justify-content:space-between}.footer-post .date-time{margin:0}}</style>
494
495    <!-- CSS: Main styles (render-blocking).
496         Загружаются ПОСЛЕ inline <style> — внешние стили перебивают inline при одинаковой
497         специфичности, обеспечивая корректную работу responsive-вариантов (tw:md:w-7/12 и др.).
498         Preload scanner браузера начнёт загрузку до полного парсинга inline-блока.
499         Early Hints в Link header (worker) обеспечивают предзагрузку на первом визите. -->
500    <link rel="stylesheet" href="/styles/main-ced8eafc.css">
501    <!-- Tailwind CSS v4: дизайн-система, после main.css (утилиты перебивают legacy) -->
502    <!-- Production: инлайн (~6 KB) убирает render-blocking запрос. Dev: <link> для hot reload -->
503    
504    <style>/*! tailwindcss v4.2.2 | MIT License | https://tailwindcss.com */@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,::backdrop,:after,:before{--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial}}}:host,:root{--tw-color-gray-600:#696969;--tw-spacing:.25rem;--tw-text-xs:.75rem;--tw-text-xs--line-height:calc(1 / .75);--tw-text-sm:.875rem;--tw-text-sm--line-height:calc(1.25 / .875);--tw-text-lg:1.125rem;--tw-text-lg--line-height:calc(1.75 / 1.125);--tw-text-xl:1.25rem;--tw-text-xl--line-height:calc(1.75 / 1.25);--tw-font-weight-light:300;--tw-font-weight-bold:700;--tw-leading-relaxed:1.4;--tw-radius-md:4px;--tw-radius-lg:6px;--tw-radius-2xl:12px;--tw-color-error:#c62828;--tw-color-error-bg:#fce4e4;--tw-color-error-border:#e0b4b4;--tw-color-success:#2e7d32;--tw-color-muted:#555;--tw-spacing-section:30px;--tw-spacing-content-top:100px;--tw-spacing-section-lg:50px;--tw-spacing-section-xl:60px}.tw\:invisible{visibility:hidden}.tw\:sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.tw\:relative{position:relative}.tw\:m-0{margin:calc(var(--tw-spacing) * 0)}.tw\:mx-auto{margin-inline:auto}.tw\:-mt-2\.5{margin-top:calc(var(--tw-spacing) * -2.5)}.tw\:-mt-\[5px\]{margin-top:-5px}.tw\:-mt-section{margin-top:calc(var(--tw-spacing-section) * -1)}.tw\:mt-2{margin-top:calc(var(--tw-spacing) * 2)}.tw\:mt-2\.5{margin-top:calc(var(--tw-spacing) * 2.5)}.tw\:mt-4{margin-top:calc(var(--tw-spacing) * 4)}.tw\:mt-5{margin-top:calc(var(--tw-spacing) * 5)}.tw\:mt-6{margin-top:calc(var(--tw-spacing) * 6)}.tw\:mt-8{margin-top:calc(var(--tw-spacing) * 8)}.tw\:mt-10{margin-top:calc(var(--tw-spacing) * 10)}.tw\:mt-20{margin-top:calc(var(--tw-spacing) * 20)}.tw\:mt-\[30px\]{margin-top:30px}.tw\:mt-content-top{margin-top:var(--tw-spacing-content-top)}.tw\:mt-section{margin-top:var(--tw-spacing-section)}.tw\:mt-section-lg{margin-top:var(--tw-spacing-section-lg)}.tw\:mt-section-xl{margin-top:var(--tw-spacing-section-xl)}.tw\:row-flex{margin-left:calc(var(--grid-gutter-half,15px) * -1);margin-right:calc(var(--grid-gutter-half,15px) * -1);flex-wrap:wrap;align-items:flex-start;display:flex}.tw\:mr-section{margin-right:var(--tw-spacing-section)}.tw\:mb-0{margin-bottom:calc(var(--tw-spacing) * 0)}.tw\:mb-2\.5{margin-bottom:calc(var(--tw-spacing) * 2.5)}.tw\:mb-3{margin-bottom:calc(var(--tw-spacing) * 3)}.tw\:mb-4{margin-bottom:calc(var(--tw-spacing) * 4)}.tw\:mb-5{margin-bottom:calc(var(--tw-spacing) * 5)}.tw\:mb-6{margin-bottom:calc(var(--tw-spacing) * 6)}.tw\:mb-10{margin-bottom:calc(var(--tw-spacing) * 10)}.tw\:mb-section{margin-bottom:var(--tw-spacing-section)}.tw\:mb-section-lg{margin-bottom:var(--tw-spacing-section-lg)}.tw\:block{display:block}.tw\:flex{display:flex}.tw\:flow-root{display:flow-root}.tw\:hidden{display:none}.tw\:h-3\.5{height:calc(var(--tw-spacing) * 3.5)}.tw\:h-5{height:calc(var(--tw-spacing) * 5)}.tw\:h-9{height:calc(var(--tw-spacing) * 9)}.tw\:h-\[100px\]{height:100px}.tw\:h-\[180px\]{height:180px}.tw\:h-\[280px\]{height:280px}.tw\:h-auto{height:auto}.tw\:w-3\/5{width:60%}.tw\:w-3\/12{width:25%}.tw\:w-4\/5{width:80%}.tw\:w-9\/12{width:75%}.tw\:w-\[120px\]{width:120px}.tw\:w-full{width:100%}.tw\:max-w-full{max-width:100%}.tw\:gutter-half{padding-left:var(--grid-gutter-half,15px);padding-right:var(--grid-gutter-half,15px);flex:none;min-width:0}.tw\:shrink-0{flex-shrink:0}.tw\:cursor-pointer{cursor:pointer}.tw\:list-none{list-style-type:none}.tw\:flex-row{flex-direction:row}.tw\:flex-wrap{flex-wrap:wrap}.tw\:items-center{align-items:center}.tw\:justify-center{justify-content:center}.tw\:gap-4{gap:calc(var(--tw-spacing) * 4)}.tw\:gap-x-3{column-gap:calc(var(--tw-spacing) * 3)}.tw\:gap-y-2{row-gap:calc(var(--tw-spacing) * 2)}.tw\:overflow-hidden{overflow:hidden}.tw\:overflow-x-hidden{overflow-x:hidden}.tw\:rounded-2xl{border-radius:var(--tw-radius-2xl)}.tw\:rounded-lg{border-radius:var(--tw-radius-lg)}.tw\:rounded-md{border-radius:var(--tw-radius-md)}.tw\:border{border-style:var(--tw-border-style);border-width:1px}.tw\:border-error-border{border-color:var(--tw-color-error-border)}.tw\:bg-error-bg{background-color:var(--tw-color-error-bg)}.tw\:p-0{padding:calc(var(--tw-spacing) * 0)}.tw\:p-4{padding:calc(var(--tw-spacing) * 4)}.tw\:py-3{padding-block:calc(var(--tw-spacing) * 3)}.tw\:py-10{padding-block:calc(var(--tw-spacing) * 10)}.tw\:pt-2{padding-top:calc(var(--tw-spacing) * 2)}.tw\:pt-20{padding-top:calc(var(--tw-spacing) * 20)}.tw\:pb-20{padding-bottom:calc(var(--tw-spacing) * 20)}.tw\:pl-0{padding-left:calc(var(--tw-spacing) * 0)}.tw\:text-center{text-align:center}.tw\:text-left{text-align:left}.tw\:text-right{text-align:right}.tw\:align-baseline{vertical-align:baseline}.tw\:text-lg{font-size:var(--tw-text-lg);line-height:var(--tw-leading,var(--tw-text-lg--line-height))}.tw\:text-sm{font-size:var(--tw-text-sm);line-height:var(--tw-leading,var(--tw-text-sm--line-height))}.tw\:text-xl{font-size:var(--tw-text-xl);line-height:var(--tw-leading,var(--tw-text-xl--line-height))}.tw\:text-xs{font-size:var(--tw-text-xs);line-height:var(--tw-leading,var(--tw-text-xs--line-height))}.tw\:leading-relaxed{--tw-leading:var(--tw-leading-relaxed);line-height:var(--tw-leading-relaxed)}.tw\:font-bold{--tw-font-weight:var(--tw-font-weight-bold);font-weight:var(--tw-font-weight-bold)}.tw\:font-light{--tw-font-weight:var(--tw-font-weight-light);font-weight:var(--tw-font-weight-light)}.tw\:text-error{color:var(--tw-color-error)}.tw\:text-gray-600{color:var(--tw-color-gray-600)}.tw\:text-muted{color:var(--tw-color-muted)}.tw\:text-success{color:var(--tw-color-success)}@media not all and (min-width:1200px){.tw\:max-\[1200px\]\:hidden{display:none}}@media not all and (min-width:992px){.tw\:max-md\:block{display:block}.tw\:max-md\:hidden{display:none}}@media not all and (min-width:768px){.tw\:max-sm\:block{display:block}.tw\:max-sm\:hidden{display:none}.tw\:max-sm\:inline{display:inline}}@media (min-width:768px){.tw\:sm\:mx-auto{margin-inline:auto}.tw\:sm\:ml-offset-1{margin-left:8.333%}.tw\:sm\:ml-offset-2{margin-left:16.667%}.tw\:sm\:ml-offset-3{margin-left:25%}.tw\:sm\:ml-offset-4{margin-left:33.333%}.tw\:sm\:hidden{display:none}.tw\:sm\:w-1\/2{width:50%}.tw\:sm\:w-2\/12{width:16.6667%}.tw\:sm\:w-3\/12{width:25%}.tw\:sm\:w-4\/12{width:33.3333%}.tw\:sm\:w-5\/12{width:41.6667%}.tw\:sm\:w-6\/12{width:50%}.tw\:sm\:w-7\/12{width:58.3333%}.tw\:sm\:w-8\/12{width:66.6667%}.tw\:sm\:w-9\/12{width:75%}.tw\:sm\:w-10\/12{width:83.3333%}.tw\:sm\:w-11\/12{width:91.6667%}@media not all and (min-width:1200px){.tw\:sm\:max-\[1200px\]\:hidden{display:none}}}@media (min-width:992px){.tw\:md\:mt-content-top{margin-top:var(--tw-spacing-content-top)}.tw\:md\:ml-offset-1{margin-left:8.333%}.tw\:md\:ml-offset-2{margin-left:16.667%}.tw\:md\:ml-offset-3{margin-left:25%}.tw\:md\:hidden{display:none}.tw\:md\:w-1\/12{width:8.33333%}.tw\:md\:w-2\/12{width:16.6667%}.tw\:md\:w-3\/12{width:25%}.tw\:md\:w-4\/12{width:33.3333%}.tw\:md\:w-5\/12{width:41.6667%}.tw\:md\:w-6\/12{width:50%}.tw\:md\:w-7\/12{width:58.3333%}.tw\:md\:w-8\/12{width:66.6667%}.tw\:md\:w-9\/12{width:75%}.tw\:md\:w-10\/12{width:83.3333%}.tw\:md\:w-\[60\%\]{width:60%}}@media (min-width:1200px){.tw\:lg\:ml-offset-2{margin-left:16.667%}.tw\:lg\:inline{display:inline}.tw\:min-\[1200px\]\:hidden{display:none}.tw\:lg\:w-3\/12{width:25%}.tw\:lg\:w-5\/12{width:41.6667%}.tw\:lg\:w-7\/12{width:58.3333%}.tw\:lg\:w-8\/12{width:66.6667%}.tw\:lg\:w-9\/12{width:75%}.tw\:lg\:w-10\/12{width:83.3333%}}:root{--color-surface:#fff;--color-surface-alt:#fafafa;--color-on-surface:#000;--color-on-surface-muted:#555;--color-divider:#e0e0e0;--color-overlay:#00000080;--alpha-black-04:#0000000a;--alpha-black-05:#0000000d;--alpha-black-06:#0000000f;--alpha-black-07:#00000012;--alpha-black-08:#00000014;--alpha-black-10:#0000001a;--alpha-black-11:#0000001c;--alpha-black-12:#0000001f;--alpha-black-13:#00000021;--alpha-black-15:#00000026;--alpha-black-20:#0003;--alpha-black-25:#00000040;--alpha-black-30:#0000004d;--alpha-black-35:#00000059;--alpha-black-40:#0006;--alpha-black-45:#00000073;--alpha-black-50:#00000080;--alpha-black-55:#0000008c;--alpha-black-60:#0009;--alpha-black-65:#000000a6;--alpha-black-70:#000000b3;--alpha-black-75:#000000bf;--alpha-black-80:#000c;--alpha-black-85:#000000d9;--alpha-black-90:#000000e6;--alpha-white-07:#ffffff12;--alpha-white-09:#ffffff17;--alpha-white-10:#ffffff1a;--alpha-white-13:#ffffff21;--alpha-white-14:#ffffff24;--alpha-white-15:#ffffff26;--alpha-white-20:#fff3;--alpha-white-25:#ffffff40;--alpha-white-30:#ffffff4d;--alpha-white-35:#ffffff59;--alpha-white-40:#fff6;--alpha-white-45:#ffffff73;--alpha-white-50:#ffffff80;--alpha-white-55:#ffffff8c;--alpha-white-60:#fff9;--alpha-white-70:#ffffffb3;--alpha-white-75:#ffffffbf;--alpha-white-80:#fffc;--alpha-white-85:#ffffffd9;--alpha-white-90:#ffffffe6;--color-suggestion-add:#28a745;--color-suggestion-add-rgb:40,167,69;--color-suggestion-delete:#dc3545;--color-suggestion-delete-rgb:220,53,69;--color-suggestion-secondary:#4cb050;--color-suggestion-secondary-rgb:76,175,80;--color-suggestion-resolve:#e67e22;--color-suggestion-resolve-rgb:230,126,34;--color-comment-highlight:#ffb673;--color-comment-highlight-rgb:255,182,115;--color-format-change:#ffc107;--color-format-change-rgb:255,193,7;--color-search-match:#ea580c;--color-search-match-rgb:234,88,12;--color-messages-blue:#4a90d9;--color-messages-blue-rgb:74,144,217;--color-warning-accent:#f9a825;--color-warning-accent-rgb:249,168,37;--color-warning-gold:#b8860b;--color-warning-gold-rgb:184,134,11;--color-delete-text:#721c24;--color-delete-text-rgb:114,28,36;--grid-gutter:30px;--grid-gutter-half:15px;--container-width-sm:750px;--container-width-md:970px;--container-width-lg:1170px;--bp-xs:480px;--bp-sm:768px;--bp-md:992px;--bp-lg:1200px;--bp-xl:1300px;--bp-narrow:500px;--bp-mobile:600px;--bp-phablet:660px;--bp-tablet:991px;--bp-tablet-landscape:1024px;--btn-font-weight:normal;--btn-border-radius:4px;--btn-padding-x:12px;--btn-padding-y:6px;--btn-disabled-opacity:.65;--input-bg:#fff;--input-color:#000;--input-border:#ccc;--input-border-focus:#333;--input-border-radius:4px;--input-placeholder:#999;
504--input-auth-height:56px;--input-auth-border:#e9e9ee;--input-auth-border-focus:#000;--input-auth-border-error:#c62828;--input-auth-radius:2px;--input-auth-placeholder:#858585}[data-theme=dark]{--color-surface:#1a1a1a;--color-surface-alt:#242424;--color-on-surface:#e8e8e8;--color-on-surface-muted:#999;--color-divider:#333;--color-overlay:#000000b3;--alpha-black-04:#0000001a;--alpha-black-05:#0000001f;--alpha-black-06:#00000026;--alpha-black-07:#0000002e;--alpha-black-08:#0003;--alpha-black-11:#00000040;--alpha-black-12:#00000047;--alpha-black-13:#0000004d;--alpha-black-15:#00000059;--alpha-black-20:#0006;--alpha-black-25:#00000080;--alpha-black-35:#0000008c;--alpha-black-55:#000000b3;--alpha-black-70:#000000d9;--alpha-black-80:#000000e6;--alpha-white-25:#ffffff26;--alpha-white-30:#fff3;--alpha-white-40:#ffffff40;--alpha-white-60:#fff6;--alpha-white-80:#fff9;--alpha-white-90:#ffffffb3;--color-suggestion-add:#34d058;--color-suggestion-add-rgb:52,208,88;--color-suggestion-delete:#f97583;--color-suggestion-delete-rgb:249,117,131;--color-suggestion-secondary:#5cd65c;--color-suggestion-secondary-rgb:92,214,92;--color-suggestion-resolve:#f0a04b;--color-suggestion-resolve-rgb:240,160,75;--color-comment-highlight:#fc9;--color-comment-highlight-rgb:255,204,153;--color-format-change:#ffd54f;--color-format-change-rgb:255,213,79;--color-search-match:#f28c5e;--color-search-match-rgb:242,140,94;--color-messages-blue:#6db3f8;--color-messages-blue-rgb:109,179,248;--color-warning-accent:#fdd835;--color-warning-accent-rgb:253,216,53;--color-warning-gold:#d4a017;--color-warning-gold-rgb:212,160,23;--color-delete-text:#f5a0a8;--color-delete-text-rgb:245,160,168}@media (prefers-reduced-motion:reduce){:not([data-allow-motion]),:not([data-allow-motion]):after,:not([data-allow-motion]):before{scroll-behavior:auto!important;transition-duration:1ms!important;animation-duration:1ms!important;animation-iteration-count:1!important}}.ds-container{padding-left:var(--grid-gutter-half,15px);padding-right:var(--grid-gutter-half,15px);margin-left:auto;margin-right:auto}@media (min-width:768px){.ds-container{width:750px}}@media (min-width:992px){.ds-container{width:970px}}@media (min-width:1200px){.ds-container{width:1170px}}@media (min-width:1300px){.ds-container{width:1280px}}.ds-content-col{flex-shrink:0;width:100%;min-width:0;padding-left:15px;padding-right:15px}@media (min-width:992px){.ds-content-col{width:58.333%;margin-left:16.667%}}@layer components{.text-invert{color:#fff;background-color:#000}.wrapped{white-space:nowrap}@media (max-width:991px){.wrapped{white-space:normal;overflow-wrap:break-word;word-break:break-word}}.zoom-in{cursor:zoom-in}.big-button{box-sizing:border-box;cursor:pointer;color:#000;text-align:center;background:#fff;border:5px solid #000;flex-direction:column;justify-content:center;align-items:center;gap:8px;width:100%;height:177px;font-size:14px;display:flex;position:relative}.big-button:disabled{opacity:.5;cursor:not-allowed}.big-button.small{height:auto;padding:10px;line-height:1.3}.big-button.thin{border-width:1px}.big-button:after{content:" ";background-color:#fff;border-top:0;border-bottom:5px solid #000;border-left:5px solid #000;border-right:0;width:50px;height:50px;display:block;position:absolute;top:-5px;right:-5px}.big-button.small:after{width:10px;height:10px}.big-button.thin:after{border-width:1px;top:-1px;right:-1px}.big-button.drag-over{background-color:rgba(var(--color-tag-rgb),.08);border-style:dashed;border-color:#2638d9}.linklike{font:inherit;cursor:pointer;color:#000;background:linear-gradient(120deg,#000 0 100%) 0 93%/100% 2px no-repeat;border:0;margin:0;padding:0;font-family:Spectral,Georgia,Times New Roman,Times,serif;font-style:italic;text-decoration:none}.linklike:focus-visible,.linklike:hover{color:#fff;background-color:#000;background-size:100% 95%}.linklike:focus-visible{outline-offset:2px;outline:2px solid}.linklike:focus:not(:focus-visible){outline:0}.border-top-7px{border-top:7px solid #000}.border-right-7px{border-right:7px solid #000}.border-bottom-7px{border-bottom:7px solid #000}.border-left-7px{border-left:7px solid #000}.margin-top-20px{margin-top:20px}.margin-top-100px{margin-top:100px}.ds-btn{font-weight:var(--btn-font-weight,normal);text-align:center;vertical-align:middle;touch-action:manipulation;cursor:pointer;white-space:nowrap;padding:var(--btn-padding-y,6px) var(--btn-padding-x,12px);border-radius:var(--btn-border-radius,4px);-webkit-user-select:none;user-select:none;background-image:none;border:1px solid #0000;margin-bottom:0;font-size:18px;line-height:1.45;transition:background-color .15s ease-in-out,color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;display:inline-block}.ds-btn:focus-visible{box-shadow:0 0 0 2px var(--alpha-black-35);outline:0}.ds-btn:focus:not(:focus-visible){box-shadow:none;outline:0}.ds-btn:focus,.ds-btn:hover{color:#333;text-decoration:none}.ds-btn:active{background-image:none;outline:0;box-shadow:inset 0 3px 5px #00000020}.ds-btn.disabled,.ds-btn:disabled,.ds-btn[disabled],fieldset[disabled] .ds-btn{cursor:not-allowed;opacity:var(--btn-disabled-opacity,.65);box-shadow:none}a.ds-btn.disabled,a.ds-btn:disabled,fieldset[disabled] :is(a.ds-btn){pointer-events:none}.ds-btn-primary{color:#fff;background-color:#2638d9;border-color:#222fb8}.ds-btn-primary:focus,.ds-btn-primary:hover{color:#fff;background-color:#1e2cb3;border-color:#18238f}.ds-btn-primary:focus-visible{box-shadow:0 0 0 2px rgba(var(--color-tag-rgb),.4)}.ds-btn-primary.active,.ds-btn-primary:active{color:#fff;background-color:#1e2cb3;border-color:#18238f;box-shadow:inset 0 3px 5px #00000020}.ds-btn-default{color:#333;background-color:#fff;border-color:#ccc}.ds-btn-default:focus,.ds-btn-default:hover{color:#333;background-color:#e6e6e6;
504border-color:#adadad}.ds-btn-default.active,.ds-btn-default:active{color:#333;background-color:#e6e6e6;border-color:#adadad;box-shadow:inset 0 3px 5px #00000020}.ds-btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.ds-btn-success:focus,.ds-btn-success:hover{color:#fff;background-color:#449d44;border-color:#398439}.ds-btn-success.active,.ds-btn-success:active{color:#fff;background-color:#449d44;border-color:#398439;box-shadow:inset 0 3px 5px #00000020}.ds-btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.ds-btn-danger:focus,.ds-btn-danger:hover{color:#fff;background-color:#c9302c;border-color:#ac2925}.ds-btn-danger.active,.ds-btn-danger:active{color:#fff;background-color:#c9302c;border-color:#ac2925;box-shadow:inset 0 3px 5px #00000020}.ds-btn-warning{color:#fff;background-color:#e09b3d;border-color:#d48f31}.ds-btn-warning:focus,.ds-btn-warning:hover{color:#fff;background-color:#c9850f;border-color:#b47a0e}.ds-btn-warning.active,.ds-btn-warning:active{color:#fff;background-color:#ec971f;border-color:#d58512;box-shadow:inset 0 3px 5px #00000020}.ds-btn-link{color:#2638d9;background-color:#0000;border-color:#0000;border-radius:0;font-weight:400}.ds-btn-link:focus,.ds-btn-link:hover{color:#1c2aa3;background-color:#0000;border-color:#0000;text-decoration:underline}.ds-btn-link:disabled,.ds-btn-link[disabled]{color:#999}:is(.ds-btn-link:disabled,.ds-btn-link[disabled]):focus,:is(.ds-btn-link:disabled,.ds-btn-link[disabled]):hover{color:#999;text-decoration:none}.ds-btn-sm{border-radius:3px;padding:5px 10px;font-size:16px;line-height:1.5}.ds-btn-lg{border-radius:6px;padding:10px 16px;font-size:23px;line-height:1.333}.ds-input{width:100%;height:37px;color:var(--input-color,#000);background-color:var(--input-bg,#fff);border:1px solid var(--input-border,#ccc);border-radius:var(--input-border-radius,4px);box-shadow:none;background-image:none;padding:6px 12px;font-size:18px;line-height:1.45;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out;display:block}.ds-input:hover:not(:disabled):not([readonly]){border-color:var(--input-border-focus,#333)}.ds-input:focus{border-color:var(--input-border-focus,#333);box-shadow:none;outline:0}.ds-input:focus-visible{outline-offset:-1px;box-shadow:none;outline:2px solid}.ds-input::placeholder{color:var(--input-placeholder,#999);opacity:1}.ds-input:disabled,.ds-input[readonly]{opacity:1;background-color:#eee}.ds-input:disabled{cursor:not-allowed}textarea.ds-input{height:auto}.has-error .ds-input{border-color:#a94442;box-shadow:inset 0 0 0 1px #a94442}.has-error .ds-input:focus,.has-error .ds-input:hover{border-color:#843534;box-shadow:inset 0 0 0 1px #843534}.ds-error-text{color:#a94442;font-size:var(--font-size-sm);margin-top:4px;display:block}.ds-btn-border{color:#000;cursor:pointer;background-color:#0000;border:1px solid #000;border-radius:0;padding:5px 10px;font-family:Muller,Helvetica Neue,Helvetica,Arial,sans-serif;font-size:12px;transition:background-color .15s ease-in-out,color .15s ease-in-out,border-color .15s ease-in-out}.ds-btn-border:hover{color:#fff;background-color:#000}.ds-btn-border.active:focus,.ds-btn-border:active,.ds-btn-border:active:focus,.ds-btn-border:focus{box-shadow:none;outline:0}.ds-btn-border:focus-visible{outline-offset:2px;outline:2px solid}.ds-btn-border:disabled{opacity:.35;cursor:not-allowed}.ds-btn-border.btn-lg{font-size:var(--font-size-base,16px)}.ds-btn-border.solid{border-width:3px}.ds-btn-border.full-width{text-align:center;width:100%}.ds-btn-border.active{color:#fff;background-color:#000}.ds-btn-border.btn-inverse{color:#fff;background-color:#000;border-color:#fff}.ds-btn-border.btn-inverse:active,.ds-btn-border.btn-inverse:focus,.ds-btn-border.btn-inverse:hover{color:#000;background:#fff}.ds-btn-border.btn-border-bold{border-width:2px}.ds-btn-border.media-move-btn{justify-content:center;align-items:center;min-width:40px;min-height:36px;padding:8px 12px;transition:background-color .2s,color .2s,border-color .2s;display:inline-flex}.ds-btn-border.media-move-btn:hover:not(:disabled){color:#fff;background-color:#000}.ds-btn-border.media-move-btn:hover:not(:disabled) i{color:#fff}.ds-btn-border.media-move-btn:disabled{opacity:.35;cursor:not-allowed;color:#999;border-color:#999}.ds-btn-border.media-move-btn:disabled i{color:#999}.ds-btn-border.media-move-btn i{font-size:var(--font-size-sm,14px);color:#000;line-height:1;display:inline-block}.ds-loadmore{color:#000;cursor:pointer;background-color:#fff;border:5px solid #000;padding:10px 20px;font-family:Muller,Helvetica Neue,Helvetica,Arial,sans-serif;font-size:27px;font-style:italic;font-weight:400;transition:color .3s ease-in-out,background-color .3s ease-in-out,border-color .3s ease-in-out;position:relative}.ds-loadmore:hover{color:#fff;background-color:#000}
504.ds-paper-button{cursor:pointer;letter-spacing:.12em;text-transform:uppercase;color:#000;background-color:#fff;border:5px solid #000;margin-right:10px;padding:10px;transition:all .3s ease-in-out;display:inline-block;position:relative}.ds-paper-button:after{content:" ";background-color:#fff;border-top:none;border-bottom:5px solid #000;border-left:5px solid #000;border-right:none;width:15px;height:15px;display:block;position:absolute;top:-5px;right:-5px}.ds-paper-button:hover{color:#fff;background-color:#000}.ds-paper-button:disabled{color:#ccc;cursor:not-allowed;border-color:#ccc}}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}</style>
505    
506
507    <!-- Secondary CSS: стили редких страниц (подкасты, /connect, /quotes, ЛК,
508         viewer-лайтбокс и др.), вынесенные из main.css (см. app/styles/secondary.scss).
509         На маршрутах, где эти стили нужны для ПЕРВОГО рендера — блокирующий <link>
510         (иначе FOUC); на остальных (статьи, главная, фид) — async (media="print"
511         → onload → media="all"): файл прогревается в кэш для SPA-навигации.
512         При добавлении нового маршрута в secondary.scss — дополнить SECONDARY_SYNC_ROUTES
513         (проверяется тестом scripts/secondary-sync-routes.test.js).
514         Осознанный трейдофф: link вставляется скриптом, preload-scanner его не видит —
515         на sync-маршрутах загрузка стартует на несколько десятков мс позже статического
516         <link>. Статический вариант невозможен: HTML один на все маршруты, а статический
517         preload поднял бы приоритет secondary на статьях в ущерб LCP-картинке. -->
518    
518<script>
519      (function () {
520        // Анкер (\/|$) обязателен каждому пути — без него /connect матчил бы /connection.
521        // /edit/authors и /edit/profile — страницы настроек профиля (settings-page
522        // в secondary); редактор материалов /edit/<type> сюда НЕ входит: его модалки
523        // (inviteCollaborators) открываются кликом, async-загрузка успевает.
524        var SECONDARY_SYNC_ROUTES =
525          /^\/(podcasts|connect|quotes|maps|almanac|newsletter|verify-email|login|register|messages|drafts|notifications|events|edit\/authors|edit\/profile)(\/|$)/;
526        var l = document.createElement('link');
527        l.rel = 'stylesheet';
528        l.href = '/styles/secondary-5e4678de.css';
529        if (!SECONDARY_SYNC_ROUTES.test(location.pathname)) {
530          l.media = 'print';
531          window.__armAsyncCss(l);
532        }
533        document.head.appendChild(l);
534      })();
535    </script>
535
536
537    <!-- Remove no-js class when JS is available -->
538    
538<script>
539      (function (h) {
540        h.className = h.className.replace(/\bno-js\b/, 'js');
541      })(document.documentElement);
542    </script>
542
543
544    <!-- Overlay visibility controlled by siteLoading.js directive (adds .footer-loaded class) -->
545  </head>
546
547  <body class="{{ bodyClass }}">
548    <!-- SVG-спрайт: централизованные иконки, используемые через <svg-icon name="..."> -->
549    <!-- SVG-спрайт: централизованное хранение иконок.
550     Используется через <svg-icon name="article"></svg-icon>
551     или <svg class="icon"><use href="#icon-article"/></svg> -->
552<svg xmlns="http://www.w3.org/2000/svg" style="display:none">
553  <!-- Тип контента: статья -->
554  <symbol id="icon-article" viewBox="0 0 24 24">
555    <path d="M4 4.5C4 3.67157 4.67157 3 5.5 3H15.3787C15.7765 3 16.158 3.15804 16.4393 3.43934L19.5607 6.56066C19.842 6.84197 20 7.22346 20 7.62132V19.5C20 20.3284 19.3284 21 18.5 21H5.5C4.67157 21 4 20.3284 4 19.5V4.5Z" stroke="currentColor" stroke-width="1.5" fill="none"/>
556    <path d="M15 3V7.5C15 7.77614 15.2239 8 15.5 8H20" stroke="currentColor" stroke-width="1.5" fill="none"/>
557    <path d="M8 13H16M8 17H13" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none"/>
558  </symbol>
559
560  <!-- Тип контента: изображение -->
561  <symbol id="icon-image" viewBox="0 0 24 24">
562    <rect x="3" y="5" width="18" height="14" rx="2" stroke="currentColor" stroke-width="1.5" fill="none"/>
563    <path d="M3 15L7.5 11.5C8.32843 10.6716 9.67157 10.6716 10.5 11.5L15 16" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none"/>
564    <path d="M12 14.5L14.5 12C15.3284 11.1716 16.6716 11.1716 17.5 12L21 15.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none"/>
565    <circle cx="8.5" cy="9.5" r="1.5" fill="currentColor"/>
566  </symbol>
567
568  <!-- Тип контента: музыка -->
569  <symbol id="icon-music" viewBox="0 0 24 24">
570    <path d="M9 18V6L21 4V16" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
571    <circle cx="6" cy="18" r="3" stroke="currentColor" stroke-width="1.5" fill="none"/>
572    <circle cx="18" cy="16" r="3" stroke="currentColor" stroke-width="1.5" fill="none"/>
573  </symbol>
574
575  <!-- Тип контента: видео -->
576  <symbol id="icon-video" viewBox="0 0 24 24">
577    <rect x="3" y="5" width="14" height="14" rx="2" stroke="currentColor" stroke-width="1.5" fill="none"/>
578    <path d="M17 9L21 7V17L17 15" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
579  </symbol>
580
581  <!-- Тип контента: литература -->
582  <symbol id="icon-literature" viewBox="0 0 24 24">
583    <path d="M4 19.5C4 18.837 4.26339 18.2011 4.73223 17.7322C5.20107 17.2634 5.83696 17 6.5 17H20" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
584    <path d="M6.5 2H20V22H6.5C5.83696 22 5.20107 21.7366 4.73223 21.2678C4.26339 20.7989 4 20.163 4 19.5V4.5C4 3.83696 4.26339 3.20107 4.73223 2.73223C5.20107 2.26339 5.83696 2 6.5 2Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
585  </symbol>
586
587  <!-- Галочка (чекбокс в dropdown) -->
588  <symbol id="icon-check" viewBox="0 0 14 10">
589    <path d="M1 5L5 9L13 1" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
590  </symbol>
591
592  <!-- Стрелка dropdown -->
593  <symbol id="icon-chevron-down" viewBox="0 0 11 7">
594    <path d="M1 1L5.5 5.5L10 1" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
595  </symbol>
596
597  <!-- Профиль -->
598  <symbol id="icon-profile" viewBox="0 0 15 16">
599    <path d="M12.73 15.75H1.72C.53 15.75-.31 14.56.1 13.45 1.08 10.82 3.94 9.05 7.23 9.05c3.28 0 6.14 1.77 7.12 4.4.41 1.11-.43 2.3-1.62 2.3zM7.23 10.55c-2.62 0-4.97 1.41-5.72 3.42-.03.08.02.28.21.28h11.01c.21 0 .24-.2.21-.28-.75-2.01-3.1-3.42-5.71-3.42z" fill="currentColor"/>
600    <path d="M7.23 7.88c-2.07 0-3.75-1.77-3.75-3.94C3.48 1.77 5.16 0 7.23 0c2.07 0 3.75 1.77 3.75 3.94 0 2.17-1.68 3.94-3.75 3.94zm0-6.38c-1.24 0-2.25 1.1-2.25 2.44s1.01 2.44 2.25 2.44 2.25-1.1 2.25-2.44S8.47 1.5 7.23 1.5z" fill="currentColor"/>
601  </symbol>
602
603  <!-- Черновики -->
604  <symbol id="icon-drafts" viewBox="0 0 16 16">
605    <path d="M13.44 0c-.61 0-1.22.23-1.69.7L1.32 11.13.03 14.96c-.17.51.35 1 .86.83l3.83-1.28L15.13 4.07c.93-.93.93-2.44 0-3.38C14.66.23 14.05 0 13.44 0zm-.06 1.56c.23 0 .45.09.63.26.34.35.34.91 0 1.25l-.52.52-1.25-1.25.52-.52c.17-.17.4-.26.62-.26zm-2.2 1.83l1.25 1.25-7.42 7.42-1.26-1.25 7.43-7.42zM2.69 11.88l1.25 1.26-1.51.72c-.13.06-.29.04-.39-.06-.
60511-.1-.14-.26-.08-.4l.73-1.52z" fill="currentColor"/>
606  </symbol>
607
608  <!-- Подписки -->
609  <symbol id="icon-subscriptions" viewBox="0 0 17 15">
610    <path d="M4.88 0C3.84 0 3 .84 3 1.88V3.75H1.88C.84 3.75 0 4.59 0 5.63v7.12C0 13.99 1.01 15 2.25 15h12c1.24 0 2.25-1.01 2.25-2.25V1.88C16.5.84 15.66 0 14.63 0H4.88zm0 1.5h9.75c.2 0 .37.17.37.38v10.87c0 .41-.34.75-.75.75H4.37c.08-.23.13-.49.13-.75V1.88c0-.21.17-.38.38-.38zM6.38 3c-.41 0-.75.34-.75.75s.34.75.75.75h6.75c.41 0 .75-.34.75-.75S13.54 3 13.13 3H6.38zM1.88 5.25H3v7.5c0 .41-.34.75-.75.75s-.75-.34-.75-.75V5.63c0-.21.17-.38.38-.38zm4.5.38c-.41 0-.75.34-.75.75s.34.75.75.75h6.75c.41 0 .75-.34.75-.75s-.34-.75-.75-.75H6.38zm0 2.62c-.41 0-.75.34-.75.75v2.63c0 .41.34.75.75.75h2.25c.41 0 .75-.34.75-.75V9c0-.41-.34-.75-.75-.75H6.38zm4.87 0c-.41 0-.75.34-.75.75s.34.75.75.75h1.88c.41 0 .75-.34.75-.75s-.34-.75-.75-.75H11.25zm0 2.63c-.41 0-.75.34-.75.75s.34.75.75.75h1.88c.41 0 .75-.34.75-.75s-.34-.75-.75-.75H11.25z" fill="currentColor"/>
611  </symbol>
612
613  <!-- Комментарии / уведомления -->
614  <symbol id="icon-notifications" viewBox="0 0 21 17">
615    <path d="M3.88 0C3.03 0 2.32.15 1.74.45 1.17.75.73 1.19.44 1.76.15 2.34 0 3.05 0 3.88v2.65c0 .83.15 1.54.44 2.11.29.58.73 1.02 1.3 1.32.58.3 1.22.45 2.06.45v2.06c0 .36.09.64.26.85.18.21.43.32.75.32.23 0 .44-.06.62-.18.19-.12.42-.31.7-.56l2.78-2.48h2.24c.85 0 1.56-.15 2.14-.45.57-.3 1.01-.74 1.3-1.32.29-.57.44-1.28.44-2.11V3.88c0-.84-.15-1.54-.44-2.12-.29-.57-.73-1.01-1.3-1.31C12.71.15 12 0 11.15 0H3.88zm0 1.5h7.27c.83 0 1.44.2 1.82.6.38.4.57.99.57 1.78v2.65c0 .79-.2 1.39-.57 1.79-.38.4-.99.6-1.82.6H8.83c-.25 0-.45.02-.6.07-.16.06-.32.17-.48.35l-2.55 2.6V9.6c0-.26-.05-.43-.16-.54-.1-.1-.28-.16-.53-.16h-.63c-.84 0-1.44-.2-1.82-.6-.38-.4-.57-.99-.57-1.79V3.88c0-.79.19-1.39.57-1.78.38-.4.99-.6 1.82-.6zM16.1 3c.04.28.06.57.06.88V4.5h.11c.84 0 1.45.2 1.82.6.38.39.57.99.57 1.78v2.71c0 .8-.19 1.39-.57 1.79-.38.4-.99.6-1.82.6h-.63c-.25 0-.42.05-.53.16-.11.1-.16.29-.16.54v2.33l-2.54-2.6c-.17-.18-.33-.3-.48-.35-.16-.05-.36-.08-.6-.08-2.52 0-2.36 0-2.4-.01l-1.45 1.3c.31.09.8.2 1.53.2h2.23l2.78 2.48c.28.25.52.44.7.56.19.12.4.18.63.18.32 0 .57-.1.75-.31.18-.21.26-.5.26-.85v-2.06c.85 0 1.48-.15 2.06-.45.58-.3 1.01-.74 1.3-1.32.29-.57.44-1.28.44-2.11V6.88c0-.84-.15-1.54-.44-2.12-.3-.57-.73-1.01-1.3-1.31-.58-.3-1.29-.45-2.14-.45H16.1z" fill="currentColor"/>
616  </symbol>
617
618  <!-- Выйти -->
619  <symbol id="icon-logout" viewBox="0 0 18 18">
620    <path d="M12.93 0c-1.04 0-1.88.84-1.88 1.88s.84 1.87 1.88 1.87 1.87-.84 1.87-1.87S13.96 0 12.93 0zm-1.5 4.5c-.27 0-.53.06-.77.17L7.67 6.08c-.15.07-.26.18-.33.33L5.87 9.68c-.04.08-.07.16-.07.26 0 .31.25.56.56.56.23 0 .44-.14.52-.34v0l1.57-2.68.78-.28-.74 2.43.01 0c-.04.16-.08.33-.08.5 0 .71.41 1.33.99 1.65l0 0 2.92 2.13 1 3.56h0c.1.31.38.53.72.53.41 0 .75-.34.75-.75 0-.08-.02-.15-.04-.22h0l-.75-3.76c-.02-.12-.07-.24-.14-.35l-1.65-2.71 1-3.29-.02-.01c.06-.18.1-.36.1-.56 0-1.03-.84-1.88-1.88-1.88zm2.49 2.98l-.57 1.54.44.32c.04.03.07.05.12.07l2.73 1.04c.01.01.02.01.03.01l.02.01c.06.02.12.03.18.03.31 0 .56-.25.56-.56 0-.22-.13-.41-.32-.5l-2.44-1.28-.75-.68zM5.66 10.88c-.22-.02-.44.09-.55.29l-.93 1.76-1.33-.71c-.36-.19-.82-.06-1.01.31L.09 15.85c-.19.36-.05.82.32 1.01l.83.45c-.01-.26.05-.52.18-.75.26-.49.77-.8 1.33-.8.24 0 .48.06.7.18.11.06.22.13.31.22l.99-1.86 0 0 .23-.44.21-.39v0l.93-1.76c.15-.28.04-.61-.23-.76-.07-.04-.14-.05-.21-.06zm2.94 1.42l-.36 1.67-1.57 2.86-.03.06c-.06.11-.1.23-.1.36 0 .41.34.75.75.75.27 0 .51-.15.64-.37l2.54-3.96-1.87-1.37zm-5.8 4.21c-.29-.02-.58.13-.73.4-.2.37-.05.82.32 1.02.36.19.82.06 1.01-.31.19-.37.06-.82-.31-1.02-.09-.05-.18-.07-.29-.09z" fill="currentColor"/>
621  </symbol>
622
623  <!-- Настройки -->
624  <symbol id="icon-settings" viewBox="0 0 17 17">
625    <path d="M7.15 0c-.5 0-.93.36-1.02.86L5.91 2.13c-.11.04-.23.09-.34.14L4.52 1.52c-.46-.32-1.01-.2-1.32.1L1.64 3.2c-.36.35-.4.91-.11 1.32l.74 1.05c-.05.11-.1.22-.14.34L.86 6.13C.36 6.21 0 6.64 0 7.15v2.21c0 .5.36.93.86 1.01l1.26.22c.04.11.09.23.14.34l-.74 1.05c-.29.41-.24.97.1 1.32l1.56 1.56c.41.4.97.37 1.32.11l1.05-.74c.11.05.23.1.34.14l.22 1.27c.08.49.51.85 1.02.85h2.21c.5 0 .93-.36 1.02-.86l.22-1.27c.11-.04.23-.09.34-.14l1.05.74c.31.23.9.31 1.32-.11l1.56-1.56c.36-.36.4-.91.11-1.32l-.74-1.05c.05-.11.1-.23.14-.34l1.27-.22c.49-.08.86-.51.86-1.02V7.15c0-.5-.36-.93-.86-1.02l-1.27-.22c-.04-.11-.09-.23-.14-.34l.74-1.05c.29-.41.24-.97-.11-1.32l-1.56-1.56c-.4-.39-.95-.38-1.32-.11l-1.
62505.74c-.11-.05-.23-.1-.34-.14L10.37.86C10.29.36 9.86 0 9.35 0H7.15zm.43 1.5h1.34c.02 0 .04.02.05.04l.29 1.68c.66.24 1.28.49 1.84.76l1.39-.98c.02-.01.04-.01.06.01l.95.95c.02.02.02.04.01.06l-.98 1.39c.28.6.54 1.22.76 1.84l1.68.29c.02 0 .04.02.04.05v1.34c0 .02-.02.04-.04.05l-1.68.29c-.22.63-.47 1.24-.76 1.84l.98 1.39c.01.02.01.04-.01.06l-.95.95c-.02.02-.04.02-.06.01l-1.39-.98c-.61.29-1.23.54-1.84.76l-.29 1.68c0 .03-.02.04-.05.04H7.58c-.02 0-.04-.02-.05-.04l-.29-1.68c-.6-.21-1.21-.46-1.84-.76l-1.39.98c-.02.01-.04.01-.06-.01l-.95-.95c-.02-.02-.02-.04-.01-.06l.98-1.39c-.29-.63-.55-1.24-.76-1.84l-1.68-.29C1.52 8.96 1.5 8.94 1.5 8.92V7.58c0-.02.02-.04.04-.05l1.68-.29c.22-.61.47-1.22.76-1.84l-.98-1.39c-.01-.02-.01-.04.01-.06l.95-.95c.02-.02.04-.02.06-.01l1.39.98c.58-.28 1.2-.53 1.84-.76l.29-1.68c0-.02.02-.04.05-.04zM8.25 4.88c-1.86 0-3.38 1.51-3.38 3.37 0 1.86 1.52 3.38 3.38 3.38s3.38-1.52 3.38-3.38c0-1.86-1.52-3.37-3.38-3.37zm0 1.5c1.04 0 1.88.84 1.88 1.87 0 1.04-.84 1.88-1.88 1.88s-1.88-.84-1.88-1.88c0-1.03.84-1.87 1.88-1.87z" fill="currentColor"/>
626  </symbol>
627
628  <!-- Комментарии (feed card) -->
629  <symbol id="icon-comments" viewBox="0 0 21 17">
630    <path d="M3.88 0C3.03 0 2.32.15 1.74.45 1.17.75.73 1.19.44 1.76.15 2.34 0 3.05 0 3.88v2.65c0 .83.15 1.54.44 2.11.29.58.73 1.02 1.3 1.32.58.3 1.22.45 2.06.45v2.06c0 .36.09.64.26.85.18.21.43.32.75.32.23 0 .44-.06.62-.18.19-.12.42-.31.7-.56l2.78-2.48h2.24c.85 0 1.56-.15 2.14-.45.57-.3 1.01-.74 1.3-1.32.29-.57.44-1.28.44-2.11V3.88c0-.84-.15-1.54-.44-2.12-.29-.57-.73-1.01-1.3-1.31C12.71.15 12 0 11.15 0H3.88zm0 1.5h7.27c.83 0 1.44.2 1.82.6.38.4.57.99.57 1.78v2.65c0 .79-.2 1.39-.57 1.79-.38.4-.99.6-1.82.6H8.83c-.25 0-.45.02-.6.07-.16.06-.32.17-.48.35l-2.55 2.6V9.6c0-.26-.05-.43-.16-.54-.1-.1-.28-.16-.53-.16h-.63c-.84 0-1.44-.2-1.82-.6-.38-.4-.57-.99-.57-1.79V3.88c0-.79.19-1.39.57-1.78.38-.4.99-.6 1.82-.6zM16.1 3c.04.28.06.57.06.88V4.5h.11c.84 0 1.45.2 1.82.6.38.39.57.99.57 1.78v2.71c0 .8-.19 1.39-.57 1.79-.38.4-.99.6-1.82.6h-.63c-.25 0-.42.05-.53.16-.11.1-.16.29-.16.54v2.33l-2.54-2.6c-.17-.18-.33-.3-.48-.35-.16-.05-.36-.08-.6-.08-2.52 0-2.36 0-2.4-.01l-1.45 1.3c.31.09.8.2 1.53.2h2.23l2.78 2.48c.28.25.52.44.7.56.19.12.4.18.63.18.32 0 .57-.1.75-.31.18-.21.26-.5.26-.85v-2.06c.85 0 1.48-.15 2.06-.45.58-.3 1.01-.74 1.3-1.32.29-.57.44-1.28.44-2.11V6.88c0-.84-.15-1.54-.44-2.12-.3-.57-.73-1.01-1.3-1.31-.58-.3-1.29-.45-2.14-.45H16.1z" fill="currentColor"/>
631  </symbol>
632
633  <!-- Просмотры (feed card) -->
634  <symbol id="icon-eye" viewBox="0 0 20 14">
635    <path d="M10 1C5.8 1 2.1 3.5.5 7c1.6 3.5 5.3 6 9.5 6s7.9-2.5 9.5-6c-1.6-3.5-5.3-6-9.5-6z" stroke="currentColor" stroke-width="1.5" fill="none"/>
636    <circle cx="10" cy="7" r="3" stroke="currentColor" stroke-width="1.5" fill="none"/>
637  </symbol>
638
639  <!-- Фильтр (слайдеры) -->
640  <symbol id="icon-filter-sliders" viewBox="0 0 24 20">
641    <path d="M4.5 4.5H1C.45 4.5 0 4.05 0 3.5S.45 2.5 1 2.5h3.5c.55 0 1 .45 1 1s-.45 1-1 1z" fill="currentColor"/>
642    <path d="M23 4.5H10c-.55 0-1-.45-1-1s.45-1 1-1h13c.55 0 1 .45 1 1s-.45 1-1 1z" fill="currentColor"/>
643    <path d="M14.5 11H1c-.55 0-1-.45-1-1s.45-1 1-1h13.5c.55 0 1 .45 1 1s-.45 1-1 1z" fill="currentColor"/>
644    <path d="M23 11h-3.5c-.55 0-1-.45-1-1s.45-1 1-1H23c.55 0 1 .45 1 1s-.45 1-1 1z" fill="currentColor"/>
645    <path d="M6 17.5H1c-.55 0-1-.45-1-1s.45-1 1-1h5c.55 0 1 .45 1 1s-.45 1-1 1z" fill="currentColor"/>
646    <path d="M23 17.5H11.5c-.55 0-1-.45-1-1s.45-1 1-1H23c.55 0 1 .45 1 1s-.45 1-1 1z" fill="currentColor"/>
647    <path d="M9 20c-1.93 0-3.5-1.57-3.5-3.5S7.07 13 9 13s3.5 1.57 3.5 3.5S10.93 20 9 20zm0-5c-.83 0-1.5.67-1.5 1.5S8.17 18 9 18s1.5-.67 1.5-1.5S9.83 15 9 15z" fill="currentColor"/>
648    <path d="M17 13.5c-1.93 0-3.5-1.57-3.5-3.5s1.57-3.5 3.5-3.5 3.5 1.57 3.5 3.5-1.57 3.5-3.5 3.5zm0-5c-.83 0-1.5.67-1.5 1.5s.67 1.5 1.5 1.5 1.5-.67 1.5-1.5-.67-1.5-1.5-1.5z" fill="currentColor"/>
649    <path d="M7 7C5.07 7 3.5 5.43 3.5 3.5S5.07 0 7 0s3.5 1.57 3.5 3.5S8.93 7 7 7zm0-5c-.83 0-1.5.67-1.5 1.5S6.17 5 7 5s1.5-.67 1.5-1.5S7.83 2 7 2z" fill="currentColor"/>
650  </symbol>
651
652  <!-- Сброс фильтра (воронка + X) -->
653  <symbol id="icon-filter-reset" viewBox="0 0 24 24">
654    <path d="M3 4H14L10 10V16L8 18V10L4 4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
655    <path d="M19 8L15 12M15 8L19 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="none"/>
656  </symbol>
657
658  <!-- Закрыть (X) -->
659  <symbol id="icon-close" viewBox="0 0 24 24">
660    <path d="M18 6L6 18M6 6L18 18" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
661  </symbol>
662
663  <!-- VK (Font Awesome 5 стиль — классический «ВК» знак) -->
664  <symbol id="icon-vk" viewBox="0 0 576 512">
665    <path fill="currentColor" d="M545 117.7c3.7-12.5 0-21.7-17.8-21.7h-58.9c-15 0-21.9 7.9-25.6 16.7 0 0-30 73.1-72.4 120.5-13.7 13.7-20 18.1-27.5 18.1-3.7 0-9.4-4.4-9.4-16.9V117.7c0-15-4.2-21.7-16.6-21.7h-92.6c-9.4 0-15 7-15 13.5 0 14.2 21.2 17.5 23.4 57.5v86.8c0 19-3.4 22.5-10.9 22.5-20 0-68.6-73.4-97.4-157.4-5.8-16.3-11.5-22.9-26.6-22.9H38.8c-16.8 0-20.2 7.9-20.2 16.7 0 15.6 20 93.1 93.1 195.5C160.4 378.1 229 416 291.4 416c37.5 0 42.1-8.4 42.1-22.9 0-66.8-3.4-73.1 15.4-73.1 8.7 0 23.7 3.5 58.7 34.1 40 40 46.6 57.9 69 57.9h58.9c16.8 0 25.3-8.4 20.4-25-11.2-34.9-86.9-106.7-90.3-111.5-8.7-11.2-6.2-16.2 0-26.2.1-.1 72-101.3 79.4-135.6z"/>
666  </symbol>
667
668  <!-- Telegram (Font Awesome 5 стиль — бумажный самолёт) -->
669  <symbol id="icon-telegram" viewBox="0 0 448 512">
670    <path fill="currentColor" d="M446.7 98.6l-67.6 318.8c-5.1 22.5-18.4 28.1-37.3 17.5l-103-75.9-49.7 47.8c-5.5 5.5-10.1 10.1-20.7 10.1l7.4-104.9 190.9-172.5c8.3-7.4-1.8-11.5-12.9-4.1L117.8 284 16.2 252.2c-22.1-6.9-22.5-22.1 4.6-32.7L418.2 66.4c18.4-6.9 34.5 4.1 28.5 32.2z"/>
671  </symbol>
672
673  <!-- Twitter (Font Awesome 5 стиль) -->
674  <symbol id="icon-twitter" viewBox="0 0 512 512">
675    <path fill="currentColor" d="M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z"/>
676  </symbol>
677
678  <!-- Facebook-f (Font Awesome 5 стиль) -->
679  <symbol id="icon-facebook" viewBox="0 0 320 512">
680    <path fill="currentColor" d="M279.14 288l14.22-92.66h-88.91v-60.13c0-25.35 12.42-50.06 52.24-50.06h40.42V6.26S260.43 0 225.36 0c-73.22 0-121.08 44.38-121.08 124.72v70.62H22.89V288h81.39v224h100.17V288z"/>
681  </symbol>
682
683  <!-- Instagram (Font Awesome 5 стиль) -->
684  <symbol id="icon-instagram" viewBox="0 0 448 512">
685    <path fill="currentColor" d="M224.1 141c-63.6 0-114.9 51.3-114.9 114.9s51.3 114.9 114.9 114.9S339 319.5 339 255.9 287.7 141 224.1 141zm0 189.6c-41.1 0-74.7-33.5-74.7-74.7s33.5-74.7 74.7-74.7 74.7 33.5 74.7 74.7-33.6 74.7-74.7 74.7zm146.4-194.3c0 14.9-12 26.8-26.8 26.8-14.9 0-26.8-12-26.8-26.8s12-26.8 26.8-26.8 26.8 12 26.8 26.8zm76.1 27.2c-1.7-35.9-9.9-67.7-36.2-93.9-26.2-26.2-58-34.4-93.9-36.2-37-2.1-147.9-2.1-184.9 0-35.8 1.7-67.6 9.9-93.9 36.1s-34.4 58-36.2 93.9c-2.1 37-2.1 147.9 0 184.9 1.7 35.9 9.9 67.7 36.2 93.9s58 34.4 93.9 36.2c37 2.1 147.9 2.1 184.9 0 35.9-1.7 67.7-9.9 93.9-36.2 26.2-26.2 34.4-58 36.2-93.9 2.1-37 2.1-147.8 0-184.8zM398.8 388c-7.8 19.6-22.9 34.7-42.6 42.6-29.5 11.7-99.5 9-132.1 9s-102.7 2.6-132.1-9c-19.6-7.8-34.7-22.9-42.6-42.6-11.7-29.5-9-99.5-9-132.1s-2.6-102.7 9-132.1c7.8-19.6 22.9-34.7 42.6-42.6 29.5-11.7 99.5-9 132.1-9s102.7-2.6 132.1 9c19.6 7.8 34.7 22.9 42.6 42.6 11.7 29.5 9 99.5 9 132.1s2.7 102.7-9 132.1z"/>
686  </symbol>
687
688  <!-- YouTube (Font Awesome 5 стиль) -->
689  <symbol id="icon-youtube" viewBox="0 0 576 512">
690    <path fill="currentColor" d="M549.655 124.083c-6.281-23.65-24.787-42.276-48.284-48.597C458.781 64 288 64 288 64S117.22 64 74.629 75.486c-23.497 6.322-42.003 24.947-48.284 48.597-11.412 42.867-11.412 132.305-11.412 132.305s0 89.438 11.412 132.305c6.281 23.65 24.787 41.5 48.284 47.821C117.22 448 288 448 288 448s170.78 0 213.371-11.486c23.497-6.321 42.003-24.171 48.284-47.821 11.412-42.867 11.412-132.305 11.412-132.305s0-89.438-11.412-132.305zm-317.51 213.508V175.185l142.739 81.205-142.739 81.201z"/>
691  </symbol>
692
693  <!-- Envelope (Font Awesome 5 Solid стиль) -->
694  <symbol id="icon-envelope" viewBox="0 0 512 512">
695    <path fill="currentColor" d="M502.3 190.8c3.9-3.1 9.7-.2 9.7 4.7V400c0 26.5-21.5 48-48 48H48c-26.5 0-48-21.5-48-48V195.6c0-5 5.7-7.8 9.7-4.7 22.4 17.4 52.1 39.5 154.1 113.6 21.1 15.4 56.7 47.8 92.2 47.6 35.7.3 72-32.8 92.3-47.6 102-74.1 131.6-96.3 154-113.7zM256 320c23.2.4 56.6-29.2 73.4-41.4 132.7-96.3 142.8-104.7 173.4-128.7 5.8-4.5 9.2-11.5 9.2-18.9v-19c0-26.5-21.5-48-48-48H48C21.5 64 0 85.5 0 112v19c0 7.4 3.4 14.3 9.2 18.9 30.6 23.9 40.7 32.4 173.4 128.7 16.8 12.2 50.2 41.8 73.4 41.4z"/>
696  </symbol>
697</svg>
698
699    <!-- Add html classes for page-specific CSS (before Angular bootstrap) -->
700    
700<script>
701      (function () {
702        var p = location.pathname;
703        // Article/expo pages: defer footer for CLS prevention
704        if (/^\/articles\/[^/]+\/[^/]+/.test(p) || /^\/expo\/[^/]+\/[^/]+\/[^/]+/.test(p)) {
705          document.documentElement.classList.add('defer-footer-top');
706        }
707        // Editor pages: hide footer until Angular takes over (prevents footer flash)
708        if (/^\/edit\//.test(p)) {
709          document.documentElement.classList.add('is-editor-page');
710        }
711        // Личный кабинет: скрываем футер на мобильных до Angular bootstrap
712        if (
713          /^\/messages(\/|$)/.test(p) ||
714          /^\/notifications(\/|$)/.test(p) ||
715          /^\/drafts(\/|$)/.test(p)
716        ) {
717          document.documentElement.classList.add('hide-mobile-footer');
718        }
719      })();
720    </script>
720
721    <!-- Social selection popup -->
722    <social-selection></social-selection>
723
724    <!-- Глобальный индикатор «API недоступен» — виден всем, в т.ч. анонимам.
725         Показывается когда circuit-breaker активен (каскад 5xx/52x).
726         Скрывается автоматически при восстановлении. -->
727    <api-outage-banner></api-outage-banner>
728
729    <!-- Header (hidden when TipTap editor is active - it has its own editor-header) -->
730    <header ng-if="(!$root.statusCode || $root.isStubPage) && !$root.hideSiteHeader" ng-class="{'is-short': $root.isHeaderShort, 'is-expo': $root.isExpoPages, 'is-logo-only': $root.isLogoOnlyPage, 'is-force-compact': $root.isForceCompactHeader }"></header>
731
732    <!-- Feedback modal for editor mode (when site header is hidden, its own instance is gone) -->
733    <send-feedback-message ng-if="$root.hideSiteHeader"></send-feedback-message>
734
735    <!-- Login modal for editor mode (site header's instance is destroyed when hideSiteHeader=true) -->
736    <login-modal ng-if="$root.hideSiteHeader && $root.editorLoginShown" opened="$root.editorLoginShown" event-detail="$root.editorLoginDetail"></login-modal>
737
738    <!-- Route loading indicator -->
739    <route-loading-indicator></route-loading-indicator>
740
741    <!-- Loading footer overlay: visible when .ready class added -->
742    <div class="loading-footer-overlay do-animate" ng-cloak ng-if="$root.isRouteLoading && !$root.statusCode && !$root.isEditorLoading" role="status" aria-live="off" aria-busy="true" aria-label="Загрузка страницы" ng-attr-aria-label="{{ 'Загрузка страницы' | t }}">
743      <!-- Centered punchline - always visible (default value set in siteLoading.js) -->
744      <div class="loading-footer-overlay__punchline" ng-bind-html="$root.loadingPunchline | trustedHtml"></div>
745
746      <!-- Footer image - sync decoding to prevent flicker -->
747      <!-- Responsive: 400w mobile, 640w tablet, 832w retina-mobile/laptop, 1280w desktop.
748           832w закрывает дыру 640→1280: mobile DPR 1.75-2 (720-830 физ.px) и ноутбуки
749           раньше качали 1280w (~102 KiB) вместо ~42 KiB (PSI: −90 KiB на каждой странице). -->
750      <div class="loading-footer-overlay__image" aria-hidden="true">
751        <picture>
752          <source type="image/webp" srcset="/images/footer-mobile.webp 400w, /images/footer-small.webp 640w, /images/footer-medium.webp 832w, /images/footer.webp 1280w" sizes="100vw">
753          <!-- fetchpriority lowered from "high" to "low", decoding from "sync" to "async":
754               This is a decorative overlay image, NOT the LCP element.
755               High priority here competes with the hero article image for bandwidth. -->
756          <img src="/images/footer-small.webp" srcset="/images/footer-mobile.webp 400w, /images/footer-small.webp 640w, /images/footer-medium.webp 832w, /images/footer.webp 1280w" sizes="100vw" loading="eager" fetchpriority="low" decoding="async" width="1280" height="518" alt="" aria-hidden="true">
757        </picture>
758      </div>
759
760      <!-- Статичный футер для overlay — НЕ используем <site-footer>,
761           чтобы не создавать лишний инстанс компонента на каждой навигации.
762           Ссылки дублируют siteFooter.html для корректного отображения на мобилках. -->
763      <!-- Тег <footer>, а не <div>, — для Markdown for Agents: конвертер Cloudflare
764           срезает <header>/<footer>/<head>, но не div с классом. Без тега этот
765           статичный блок уезжал в markdown-версию каждой статьи первым абзацем:
766           «О проекте», «Участие», «Разделы», «Подписка». Замер 06.09.2026 после правки:
767           1321 → 1157 токенов у страницы без тела — оÐ
767ºÐ¾Ð»Ð¾ 160 токенов на футер, остальное
768           там JSON-LD, который конвертер сохраняет намеренно.
769           Гейт: scripts/markdown-noise-semantics.test.js -->
770      <div class="loading-footer-overlay__content">
771        <div id="footer-overlay">
772          <footer class="footer">
773            <!-- Подписи: текст в теге, перевод на ng-bind — не интерполяцией.
774                 Сырую интерполяцию не видит никто, кто не исполняет JS.
775                 Разбор и гейт: scripts/static-footer-text.test.js -->
776            <div class="ds-container footer__inner">
777              <div class="col footer__column footer__column--about">
778                <p class="footer__heading" ng-bind="'О проекте' | t">О проекте</p>
779                <ul class="tw:list-none tw:pl-0">
780                  <li><a href="/manifest" ng-bind="'Манифест' | t">Манифест</a></li>
781                  <li>
782                    <a href="/guide" ng-bind="'Как устроен Дискурс' | t">Как устроен Дискурс</a>
783                  </li>
784                  <li class="tw:max-sm:hidden">
785                    <a href="/principles" ng-bind="'Принципы сообщества' | t">Принципы сообщества</a>
786                  </li>
787                  <li class="tw:max-sm:hidden">
788                    <a href="/dogma" ng-bind="'Догма открытой редакции' | t">Догма открытой редакции</a>
789                  </li>
790                </ul>
791              </div>
792              <div class="col footer__column footer__column--participate">
793                <p class="footer__heading" ng-bind="'Участие' | t">Участие</p>
794                <ul class="tw:list-none tw:pl-0">
795                  <li><a href="/help#idea" ng-bind="'Предложить идею' | t">Предложить идею</a></li>
796                  <li><a href="/drafts" ng-bind="'Создать материал' | t">Создать материал</a></li>
797                  <li><a href="/help" ng-bind="'Поддержать проект' | t">Поддержать проект</a></li>
798                  <li>
799                    <a href="https://forms.gle/GJ3Muqf1UjeCzyDXA" target="_blank" rel="noopener noreferrer" ng-bind="'Присоединиться к редакции' | t">Присоединиться к редакции</a>
800                  </li>
801                </ul>
802              </div>
803              <div class="col footer__column footer__column--sections">
804                <p class="footer__heading" ng-bind="'Разделы' | t">Разделы</p>
805                <ul class="tw:list-none tw:pl-0">
806                  <li><a href="/authors" ng-bind="'Авторы' | t">Авторы</a></li>
807                  <li><a href="/tags" ng-bind="'Темы' | t">Темы</a></li>
808                  <li><a href="/help#specials" ng-bind="'Спецпроекты' | t">Спецпроекты</a></li>
809                </ul>
810              </div>
811              <div class="col footer__column footer__column--subscription">
812                <p class="footer__heading" ng-bind="'Подписка' | t">Подписка</p>
813                <!-- Ключ слово в слово, с &nbsp;: на RU фильтр возвращает его сам. -->
814                <p class="subscription-description" ng-bind="'Чтобы&nbsp;получать&nbsp;рассылку лучших&nbsp;публикаций,
815                  просто&nbsp;укажите&nbsp;почту' | t">
816                  Чтобы&nbsp;получать&nbsp;рассылку лучших&nbsp;публикаций,
817                  просто&nbsp;укажите&nbsp;почту
818                </p>
819                <!-- Форма подписки — в overlay Angular не инициализирован,
820                     интерактивная форма доступна в <site-footer> на загруженной странице -->
821              </div>
822            </div>
823          </footer>
824        </div>
825      </div>
826    </div>
827
828    <!-- Editor loading skeleton: shown during editor lazy-load instead of blank white screen -->
829    <div class="editor-loading-skeleton do-animate" ng-cloak ng-if="$root.isEditorLoading && !$root.statusCode" role="status" aria-l
829ive="off" aria-busy="true" aria-label="Загрузка редактора" ng-attr-aria-label="{{ 'Загрузка редактора' | t }}">
830      <!-- Skeleton header mimicking editor-header (fixed, 56px) -->
831      <div class="editor-loading-skeleton__header">
832        <div class="editor-loading-skeleton__logo">
833          <img src="/images/logo-mini.svg" alt="" width="26" height="28" aria-hidden="true">
834        </div>
835        <div class="editor-loading-skeleton__header-spacer"></div>
836        <div class="editor-loading-skeleton__btn-placeholder"></div>
837        <div class="editor-loading-skeleton__btn-placeholder editor-loading-skeleton__btn-placeholder--wide"></div>
838      </div>
839      <!-- Skeleton body using same layout as real editor:
840           .container > wrapper(margin-top) > tw:sm:w-8/12 tw:md:ml-offset-2
841           Real editor: .discours-form has margin-top:-50px, row has margin-top:150px → net 100px -->
842      <div class="ds-container">
843        <div class="tw:mt-content-top">
844          <div class="editor-loading-skeleton__body-col tw:sm:w-8/12 tw:md:ml-offset-2">
845            <!-- Title row: matches .title-input-row > .title-col/.cover-col with gutter padding -->
846            <div class="title-input-row tw:flex tw:flex-wrap">
847              <div class="title-col tw:sm:w-8/12 tw:lg:w-9/12">
848                <div class="editor-loading-skeleton__title-line"></div>
849                <div class="editor-loading-skeleton__title-line editor-loading-skeleton__title-line--short"></div>
850              </div>
851              <div class="cover-col tw:sm:w-4/12 tw:lg:w-3/12">
852                <div class="editor-loading-skeleton__cover-placeholder"></div>
853              </div>
854            </div>
855            <!-- Editor body: shimmer lines mimicking text content -->
856            <div class="editor-loading-skeleton__editor-block">
857              <div class="editor-loading-skeleton__text-line"></div>
858              <div class="editor-loading-skeleton__text-line editor-loading-skeleton__text-line--90"></div>
859              <div class="editor-loading-skeleton__text-line editor-loading-skeleton__text-line--75"></div>
860              <div class="editor-loading-skeleton__text-gap"></div>
861              <div class="editor-loading-skeleton__text-line"></div>
862              <div class="editor-loading-skeleton__text-line editor-loading-skeleton__text-line--85"></div>
863              <div class="editor-loading-skeleton__text-line editor-loading-skeleton__text-line--60"></div>
864            </div>
865          </div>
866        </div>
867      </div>
868    </div>
869
870    <!-- Theme menu -->
871    <div class="theme-menu-wrapper">
872      <div class="theme-menu-container ds-container theme-menu-skeleton" ng-show="$root.isThemeMenuShowed && ($root.isRouteLoading || $root.statusCode)">
873        <div class="theme-menu-skeleton__placeholder"></div>
874      </div>
875      <div class="theme-menu-container ds-container" ng-show="!$root.isRouteLoading && !$root.statusCode && $root.isThemeMenuShowed">
876        <theme-menu></theme-menu>
877      </div>
878    </div>
879
880    <!-- Main banner (promotional) -->
881    <!-- No lazy-render: the component already defers its own display via $timeout (50-60s).
882         lazy-render adds .lazy-render-reveal animation whose transform: translateY(0) persists
883         (animation-fill-mode: both) and creates a new containing block, breaking
884         position: fixed on .main-banner inside — the banner jumps to the top instead of
885         sticking to the bottom of the viewport. -->
886    <div ng-if="!$root.statusCode && !$root.isStubPage">
887      <main-banner></main-banner>
888    </div>
889
890    <!-- A11y: skip-to-content link — видима только при фокусе с клавиатуры -->
891    <a href="#main-content" class="skip-to-content" ng-bind="'Перейти к содержимому' | t">Перейти к содержимому</a>
892
893    <!-- Main app content -->
894    <main id="main-content" class="main-content" role="main">
895      <!-- ================================================================ -->
896      <!-- Instant Hero: LCP optimization for article/expo pages          -->
897      <!--                                                                -->
898      <!-- HOMEPAGE: Worker injects complete hero card (#ssr-homepage)     -->
899      <!-- into <main> — visible at FCP, removed when Angular renders.    -->
900      <!-- Also has <link rel="preload"> for hero image network benefit.  -->
901      <!--                                                                -->
902      <!-- ARTICLE/EXPO: Uses MutationObserver to inject into Angular's   -->
903      <!-- wrapper when it appears (these pages have different layouts).   -->
904      <!-- ================================================================ -->
905      
905<script>
906        (function () {
907          'use strict';
908
909          var THUMBOR_BASE =
910            ((window.discours && window.discours.ASSETS_DOMAIN) || 'https://assets.discours.io') +
911            '/unsafe';
912          var MEDIA_BASE =
913            (window.discours && window.discours.MEDIA_DOMAIN) || 'https://media.discours.io';
914          var OBSERVER_TIMEOUT = 5000;
915          var path = location.pathname;
916
917          // Анимированный GIF ресайзить нельзя — Thumbor отдаёт на нём 502
918          // (замеры и разбор — в шапке app/scripts/shared/image-presets.js).
919          // Паттерн подставляется на сборке ИЗ ТОГО ЖЕ модуля (webpack
920          // templateParameters), копии правила здесь нет — расходиться нечему.
921          var RE_DIRECT_MEDIA = new RegExp("\\.gif(?:[?#]|$)", 'i');
922          function isDirectMedia(thumborId) {
923            return RE_DIRECT_MEDIA.test(thumborId);
924          }
925
926          // === Helper: URL картинки нужной ширины ===
927          function imageUrl(thumborId, width) {
928            if (isDirectMedia(thumborId)) return MEDIA_BASE + '/' + thumborId;
929            return THUMBOR_BASE + '/' + width + 'x/' + thumborId;
930          }
931
932          // === Helper: generate srcset string ===
933          // У GIF вариантов по ширине не бывает — возвращаем '', тогда браузер берёт src.
934          function makeSrcset(thumborId, widths) {
935            if (isDirectMedia(thumborId)) return '';
936            return widths
937              .map(function (w) {
938                return imageUrl(thumborId, w) + ' ' + w + 'w';
939              })
940              .join(', ');
941          }
942
943          // Homepage: Worker injects #ssr-homepage with complete hero card.
944          // No additional instant-hero injection needed here.
945          if (path === '/') return;
946
947          // ================================================================
948          // ARTICLE / EXPO: MutationObserver approach (needs Angular DOM)
949          // ================================================================
950          // Skip instant-hero injection when SSR content is present —
951          // the Worker already injected a complete article with hero image.
952          if (document.getElementById('ssr-article')) return;
953
954          var pageType = null;
955          if (/^\/articles\/[^/]+\/[^/]+/.test(path)) {
956            pageType = 'article';
957          } else if (/^\/expo\/[^/]+\/[^/]+\/[^/]+/.test(path)) {
958            pageType = 'expo';
959          }
960          if (!pageType) return;
961
962          // Сетка hero и правило её обрезки подставляются на сборке из единого
963          // модуля app/scripts/shared/image-presets.js (webpack templateParameters) —
964          // тот же код исполняют Worker-preload (ArticleHeadHandler), Worker-SSR
965          // и Angular-фильтр imageSrcset. Копии правила здесь НЕТ: совпадение
966          // гарантировано сборкой, иначе браузер не сматчит preload с <img srcset>
967          // и скачает hero-картинку дважды.
968          //
969          // Ниже появляются HERO_WIDTHS, heroWidthsFor() и heroBaseWidth().
970          var HERO_WIDTHS = [320,640,960,1280,1920,2560];
971var HERO_BASE_WIDTH = 960;
972var NATURAL_STEP_MIN_GAIN = 1.1;
973var MIN_SANE_NATURAL_WIDTH = 64;
974function normalizeNaturalWidth(naturalWidth) {
975  if (typeof naturalWidth !== 'number' || !isFinite(naturalWidth)) return 0;
976  var floored = Math.floor(naturalWidth);
977  return floored >= MIN_SANE_NATURAL_WIDTH ? floored : 0;
978}
979function trimWidthsToNatural(widths, naturalWidth) {
980  var natural = normalizeNaturalWidth(naturalWidth);
981  var max = widths[widths.length - 1];
982  if (!natural || natural >= max) return widths.slice();
983
984  var trimmed = widths.filter(function (w) {
985    return w <= natural;
986  });
987  var top = trimmed[trimmed.length - 1];
988  // top нет вовсе — оригинал мельче самой мелкой ступени, тогда он и есть
989  // единственный разумный вариант.
990  if (!top || natural >= top * NATURAL_STEP_MIN_GAIN) trimmed.push(natural);
991  return trimmed;
992}
993function pickBaseWidth(widths, preferred) {
994  var within = widths.filter(function (w) {
995    return w <= preferred;
996  });
997  return within.length ? within[within.length - 1] : widths[0];
998}
999function heroWidthsFor(naturalWidth) {
1000  return trimWidthsToNatural(HERO_WIDTHS, naturalWidth);
1001}
1002function heroBaseWidth(naturalWidth) {
1003  return pickBaseWidth(heroWidthsFor(naturalWidth), HERO_BASE_WIDTH);
1004}
1005          var HERO_SIZES = '(min-width: 768px) 66vw, 100vw';
1006          var configs = {
1007            article: {
1008              dataId: 'preloaded-article',
1009              selector: '.open-post-title-image-wrapper',
1010              imgClass: 'instant-hero-article__img',
1011              imgSizes: HERO_SIZES,
1012              isLoaded: function (wrapper) {
1013                return wrapper.classList.contains('is-image-loaded');
1014              },
1015              getThumbor: function (data) {
1016                return data && data.thumborId;
1017              },
1018            },
1019            expo: {
1020              dataId: 'preloaded-article',
1021              selector: '.expo-item-title-image-wrapper',
1022              imgClass: 'instant-hero-article__img',
1023              imgSizes: HERO_SIZES,
1024              isLoaded: function (wrapper) {
1025                return wrapper.classList.contains('is-image-loaded');
1026              },
1027              getThumbor: function (data) {
1028                return data && data.thumborId;
1029              },
1030            },
1031          };
1032
1033          var config = configs[pageType];
1034
1035          function observeFor(tryInject) {
1036            if (tryInject()) return;
1037            var observer = new MutationObserver(function (mutations, obs) {
1038              if (tryInject()) {
1039                obs.disconnect();
1040              }
1041            });
1042            observer.observe(document.body, { childList: true, subtree: true });
1043            setTimeout(function () {
1044              observer.disconnect();
1045            }, OBSERVER_TIMEOUT);
1046          }
1047
1048          // Continuous aspect-ratio контейнера обложки страницы публикации/экспо
1049          // под реальный ratio картинки в [4:3, 16:9]. Совпадает с ngImageOnload.js.
1050          //
1051          // Scope: inline aspect-ratio ставится ТОЛЬКО для wrapper'ов обложки
1052          // страницы публикации (.open-post-title-image-wrapper) или экспо
1053          // (.expo-item-title-image-wrapper). В фиде и других местах парент img
1054          // имеет свои breakpoint-зависимые правила aspect-ratio — переписывать
1055          // нельзя, layout фида поломается.
1056          function applyOrientation(img, wrapper) {
1057            if (!img || !wrapper) return;
1058            var w = img.naturalWidth;
1059            var h = img.naturalHeight;
1060            if (!w || !h) return;
1061            var ratio = w / h;
1062            if (ratio >= 1) {
1063              wrapper.classList.add('is-landscape');
1064            } else if (ratio >= 0.75) {
1065              wrapper.classList.add('is-portrait');
1066            } else {
1067              wrapper.classList.add('is-very-portrait');
1068            }
1069            var isPostCoverWrapper =
1070              wrapper.classList &&
1071              (wrapper.classList.contains('open-post-title-image-wrapper') ||
1072                wrapper.classList.contains('expo-item-title-image-wrapper'));
1073            if (!isPostCoverWrapper) return;
1074            var clamped = Math.max(4 / 3, Math.min(16 / 9, ratio));
1075            var value = clamped.toFixed(4);
1076            if (!wrapper.style.aspectRatio) {
1077              wrapper.style.aspectRatio = value;
1078            }
1079            if (!wrapper.style.getPropertyValue('--cover-ratio')) {
1080              wrapper.style.setProperty('--cover-ratio', value);
1081            }
1082          }
1083
1084          observeFor(function tryInject() {
1085            var dataEl = document.getElementById(config.dataId);
1086            if (!dataEl) return false;
1087            var wrapper = document.querySelector(config.selector);
1088            if (!wrapper) return false;
1089            if (config.isLoaded(wrapper)) return true;
1090            if (wrapper.querySelector('.' + config.imgClass)) return true;
1091            try {
1092              var data = JSON.parse(dataEl.textContent);
1093              var thumborId = config.getThumbor(data);
1094              if (!thumborId) return true;
1095              // coverImageWidth кладёт Worker в #preloaded-article. Пока его Ð
1095½ÐµÑ‚
1096              // (legacy-материал, ответ без размеров) — сетка остаётся полной,
1097              // то есть ровно прежнее поведение.
1098              var naturalWidth = data && data.coverImageWidth;
1099              var img = document.createElement('img');
1100              img.className = config.imgClass;
1101              img.src = imageUrl(thumborId, heroBaseWidth(naturalWidth));
1102              img.srcset = makeSrcset(thumborId, heroWidthsFor(naturalWidth));
1103              img.sizes = config.imgSizes;
1104              img.alt = '';
1105              img.loading = 'eager';
1106              img.decoding = 'async';
1107              img.fetchPriority = 'high';
1108              img.onload = function () {
1109                applyOrientation(img, wrapper);
1110              };
1111              wrapper.appendChild(img);
1112              // If image is served from cache, onload may have already fired
1113              if (img.complete && img.naturalWidth > 0) {
1114                applyOrientation(img, wrapper);
1115              }
1116              return true;
1117            } catch (e) {
1118              return true;
1119            }
1120          });
1121        })();
1122      </script>
1122
1123
1124      <!-- Route loading bar — shown during lazy controller loading -->
1125      <div class="route-loading-bar" ng-if="$root.lazyRouteLoading" ng-cloak>
1126        <div class="route-loading-bar__progress"></div>
1127      </div>
1128
1129      <ng-view autoscroll="true" ng-show="!$root.isRouteLoading" ng-cloak></ng-view>
1130
1131      <!-- SSR Content Cleanup: Remove pre-rendered content when Angular renders.
1132           For articles (#ssr-article): controller signals readiness via window.__removeSSR()
1133           after ng-include renders the article body. This prevents the flash where SSR is
1134           hidden before Angular content is painted (MutationObserver fires too early because
1135           the route template is inserted before ng-include loads postMainPart.html).
1136           For homepage (#ssr-homepage): MutationObserver detects Angular content insertion. -->
1137      
1137<script>
1138        (function () {
1139          try {
1140            var ssr =
1141              document.getElementById('ssr-article') ||
1142              document.getElementById('ssr-homepage') ||
1143              document.getElementById('ssr-tag') ||
1144              document.getElementById('ssr-author') ||
1145              document.getElementById('ssr-about');
1146            if (!ssr) return;
1147
1148            var removed = false;
1149            var isArticle = ssr.id === 'ssr-article';
1150            var observer;
1151            var detachRetries = 0;
1152            var pinned = false;
1153            var handedOff = false;
1154
1155            // У старых/не-HD роликов YouTube maxresdefault может вернуть не
1156            // ошибку, а служебную картинку 120×90. Angular ловит её директивой
1157            // ng-image-onload; SSR вне ng-view не компилируется, поэтому делает
1158            // тот же переход на гарантированный hqdefault сам. Размер контейнера
1159            // не меняется, то есть fallback не создаёт layout shift.
1160            (function armVideoPosterFallback() {
1161              var poster = ssr.querySelector('.ssr-video-poster-img[data-fallback-src]');
1162              if (!poster) return;
1163              var fallbackSrc = poster.getAttribute('data-fallback-src');
1164              if (!fallbackSrc) return;
1165
1166              function cleanup() {
1167                poster.removeEventListener('load', validatePoster);
1168                poster.removeEventListener('error', useFallback);
1169              }
1170              function useFallback() {
1171                cleanup();
1172                poster.removeAttribute('data-fallback-src');
1173                if (poster.src !== fallbackSrc) poster.src = fallbackSrc;
1174              }
1175              function validatePoster() {
1176                if (!poster.naturalWidth || poster.naturalWidth <= 120) useFallback();
1177                else cleanup();
1178              }
1179
1180              poster.addEventListener('load', validatePoster);
1181              poster.addEventListener('error', useFallback);
1182              if (poster.complete) validatePoster();
1183            })();
1184
1185            // Кадр может не наступить НИКОГДА. Пока вкладка не композитит кадры
1186            // — фоновая вкладка, свёрнутое или полностью перекрытое окно —
1187            // requestAnimationFrame не вызывается, тогда как setTimeout
1188            // продолжает работать (замер 26.08.2026 на проде: свежий rAF не
1189            // сработал за минуты, setTimeout(…, 0) сработал в том же кадре
1190            // кода). Поэтому всё, что ОБЯЗАНО произойти, ждёт кадра, но не
1191            // дольше срока: в видимой вкладке выигрывает кадр (~16 мс) и
1192            // поведение не меняется, в невидимой — таймер.
1193            //
1194            // Без этого статья, открытая во вкладке без кадров, оставалась с
1195            // серверным рендером НАВСЕГДА: `removed` взводится до выбора ветки,
1196            // ветка «обложка уже готова» уходила в doHandoff единственной
1197            // дорогой — через кадр, — и все следующие заходы (сигнал
1198            // контроллера, 3-секундный таймер waitForContentAndRemoveSSR,
1199            // безусловный бэкстоп ниже) гасились первой строкой removeSSR():
1200            // `if (removed) return`. Читатель не видел ни комментариев, ни
1201            // ремарок, ни блока «поддержать».
1202            // Гейт: scripts/ssr-handoff-without-frames.test.js.
1203            function onFrameOrTimeout(fn, ms) {
1204              var done = false;
1205              function run() {
1206                if (done) return;
1207                done = true;
1208                fn();
1209              }
1210              var timer = setTimeout(run, ms);
1211              requestAnimationFrame(function () {
1212                clearTimeout(timer);
1213                run();
1214              });
1215            }
1216
1217            // Узел ng-view, захваченный при разборе документа: фолбэк для гейта
1218            // ниже. Свежий запрос надёжнее, но если он не нашёл узла — лучше
1219            // спросить захваченный, чем закрыть гейт вхолостую.
1220            var capturedNgView = null;
1221
1222            /**
1223             * Готова ли страница к снятию рендера по признаку, который для неё
1224             * главный. ЕДИНСТВЕННОЕ определение критерия: его зовут и ветка
1225             * отпускания пина, и наблюдатель вставок, и поллер.
1226             *
1227             * Ждать здесь имеет смысл только тегу. У статьи и автора готовность
1228             * определяет hero-картинка — её ждёт сам removeSSR(). У главной гейт
1229             * открыт, и поллер снимает рендер на первом же тике, через 100 мс:
1230             * отдельный критерий для неё был бы мёртвым кодом с видом
1231             * работающего. Замер 27.08.2026: на проде воркер #ssr-homepage вообще
1232             * не отдаёт, ветка не исполняется.
1233             */
1234            function contentGateOpen() {
1235              // Стабильный id сохраняется на весь жизненный цикл скрипта; ссылка
1236              // `ssr` после завершённого handoff уже может быть обнулена.
1237              // Страницы «О проекте» (#ssr-about): критической картинки нет, а
1238              // без гейта поллер снял бы серверный текст на первом тике — за
1239              // секунду до того, как ленивый роут дорисует свой. Ждём `.ds-container`
1240              // шаблона about-страницы внутри ng-view (у help-us это не корень, а
1241              // потомок — querySelector ищет вглубь); фолбэк по таймеру общий.
1242              if (ssrId === 'ssr-about') {
1243                var ngAbout = document.querySelector('ng-view, [ng-view]') || capturedNgView;
1244                return !!(ngAbout && ngAbout.querySelector('.ds-container'));
1245              }
1246              if (ssrId !== 'ssr-tag') return true;
1247              // Свежий запрос ng-view: захваченный при парсинге узел может
1248              // устареть, если Angular его пересоздаёт, — тогда гейт ложно вернул
1249              // бы false, и ни наблюдатель, ни поллер не сняли бы рендер при
1250              // готовом фиде. Захваченный остаётся фолбэком.
1251              var ng = document.querySelector('ng-view, [ng-view]') || capturedNgView;
1252              return !!(
1253                ng &&
1254                (ng.querySelector('.articles-container--tag-page .article > .article__inner') ||
1255                  ng.querySelector('.feed-empty'))
1256              );
1257            }
1258
1259            /**
1260             * Подключает наблюдение за реальным Angular-контентом. Наблюдатель
1261             * можно безопасно включить повторно после disconnect(): это нужно
1262             * тегу после успешного retry роута, потому что __pinSSR отключает
1263             * исходную подписку, пока серверная лента прибита поверх ошибки.
1264             */
1265            function observeContentMutations() {
1266              var ng = document.querySelector('ng-view, [ng-view]') || capturedNgView;
1267              var container = ng ? ng.parentNode : null;
1268              if (!ng || !container) return false;
1269              if (ssrId === 'ssr-tag') capturedNgView = ng;
1270
1271              if (!observer) {
1272                observer = new MutationObserver(function (mutations, obs) {
1273                  for (var i = 0; i < mutations.length; i++) {
1274                    var added = mutations[i].addedNodes;
1275                    for (var j = 0; j < added.length; j++) {
1276                      var node = added[j];
1277                      if (node.nodeType === 1 && node.id !== ssrId && node.tagName !== 'SCRIPT') {
1278                        if (contentGateOpen()) {
1279                          obs.disconnect();
1280                          removeSSR();
1281                          return;
1282                        }
1283                        // Tag: контент ещё не готов — продолжаем наблюдать.
1284                        // НЕ вызываем disconnect, ждём следующей мутации.
1285                      }
1286                    }
1287                  }
1288                });
1289              }
1290
1291              try {
1292                observer.observe(container, { childList: true });
1293                // AngularJS вставляет контент ВНУТРИ ng-view (не заменяя элемент).
1294                // Для tag-страниц нужен subtree, чтобы видеть компиляцию <tag-page>.
1295                observer.observe(ng, {
1296                  childList: true,
1297                  subtree: ssrId === 'ssr-tag',
1298                });
1299                return true;
1300              } catch (e) {
1301                return false;
1302              }
1303            }
1304
1305            var reenterCycle = 0;
1306            // Ожидание hero-картинки: наблюдатель и его фолбэк живут снаружи
1307            // removeSSR(), чтобы повторный заход мог погасить прежнее ожидание,
1308            // а не подписаться поверх него.
1309            var heroObs = null;
1310            var heroFallbackTimer = null;
1311            function stopHeroWatch() {
1312              if (heroObs) {
1313                try {
1314                  heroObs.disconnect();
1315                } catch (e) {
1316                  /* ignored */
1317                }
1318                heroObs = null;
1319              }
1320              if (heroFallbackTimer) {
1321                clearTimeout(heroFallbackTimer);
1322                heroFallbackTimer = null;
1323              }
1324            }
1325            /**
1326             * Повторный заход после отпускания «прибитого» рендера.
1327             *
1328             * `removed` взводится в НАЧАЛЕ handoff и назад не отпускается, а
1329             * прибить рендер могли уже ПОСЛЕ сигнала контроллера: читатель ушёл
1330             * по ссылке в те секунды, пока идёт handoff, и новый роут упал. Тогда
1331             * __unpinSSR() возвращал true, но removeSSR() гасился собственной
1332             * первой строкой `if (removed) return`, и страница навсегда
1333             * оставалась серверным рендером под
1334             * `#ssr-*:not(.ssr-hiding) ~ ng-view { display: none }`. Пока handoff
1335             * не состоялся, заход обязан пройти заново; повторные обозреватели
1336             * безвредны — doHandoff защищён `handedOff`.
1337             * Гейт: scripts/ssr-pin-on-route-error.test.js.
1338             *
1339             * @param {boolean} [immediate] — снять НЕ дожидаясь контента. Ставит
1340             *   только смена адреса: рендер там относится к ПРЕЖНЕЙ странице, и
1341             *   ждать ленту новой бессмысленно — пока блок висит без .ssr-hiding,
1342             *   CSS держит ng-view под display:none, и аварийный экран нового
1343             *   роута невидим. Отпускание пина этого не передаёт.
1344             */
1345            function reenterRemoveSSR(immediate) {
1346              // Сбои роута идут пачками: поколение гасит тики, оставшиеся от
1347              // прошлых заходов, чтобы они не сняли рендер нового цикла.
1348              waitThenRemoveSSR(immediate, ++reenterCycle);
1349            }
1350
1351            function waitThenRemoveSSR(immediate, cycle) {
1352              // Цикл устарел — заход был перезапущен новым отпусканием.
1353              if (cycle !== reenterCycle) return;
1354              // Отпускание пина не должно обходить гейт готовности. У тега
1355              // снятие на пустом ng-view обнажает пустоту вместо ленты, которую
1356              // читатель уже видел, — та самая вспышка, против которой и заведено
1357              // ожидание (у остальных гейт открыт, см. contentGateOpen выше).
1358              // После route-error наблюдатель отключён прибитием. Подключаем его
1359              // заново и выходим без поллинга: поздняя карточка сама завершит
1360              // handoff, а на навсегда пустом фиде не останется вечного таймера.
1361              // Повторная проверка после observe закрывает гонку, если карточка
1362              // появилась между первой проверкой гейта и подпиской.
1363              if (!immediate && !handedOff && !contentGateOpen()) {
1364                observeContentMutations();
1365                if (contentGateOpen()) waitThenRemoveSSR(immediate, cycle);
1366                return;
1367              }
1368              if (!handedOff) {
1369                removed = false;
1370                detachRetries = 0;
1371              }
1372              removeSSR();
1373            }
1374
1375            // Аварийный экран роута (route-error-screen.js) рисуется внутрь <ng-view>,
1376            // когда тот есть в DOM; на несобравшемся роуте директива ngView вырезает
1377            // его (transclude: 'element'), и экран дописывается в <main>.
1378            // Если в этот момент снять серверный рендер, читатель получит вместо
1379            // готовой страницы техническую карточку «Медленное соединение…» — а
1380            // Googlebot исполняет JS и в снимке видит ровно её. Так текст ретраев
1381            // попал в сниппет профиля в поиске (замер 26.08.2026). Поэтому при
1382            // ошибке роута серверный рендер ПРИБИВАЕТСЯ: пока #ssr-* без .ssr-hiding,
1383            // CSS держит ng-view под display:none, и аварийный экран не виден ни
1384            // читателю, ни индексатору. Ссылки в SSR — обычные <a href>, навигация
1385            // остаётся рабочей. Исключение — виды 'lock' (403) и 'missing' (404):
1386            // там бэкенд явно сказал, что доступа нет или страницы не существует,
1387            // показывать контент нельзя, и модуль сам зовёт __removeSSR (см. routes.js).
1388            // Возвращает true, если серверный рендер ПРЯМО СЕЙЧАС на экране, —
1389            // только тогда вызывающему есть что прибивать. `removed` для этого не
1390            // годится: он взводится в начале handoff, когда узел ещё виден.
1391            var ssrId = ssr.id;
1392            // Адрес, по которому пришёл документ: скрипт исполняется при разборе
1393            // HTML, поэтому здесь ещё нет никакой SPA-навигации. Сравнение с ним
1394            // защищает от подмены: после перехода на другой роут серверный рендер
1395            // относится к ПРЕЖНЕЙ странице, и оставить его — значит показать чужой
1396            // материал под новым адресом.
1397            var ssrPath = window.location.pathname;
1398            window.__pinSSR = function () {
1399              if (window.location.pathname !== ssrPath) {
1400                // Адрес сменился — рендер описывает ПРЕЖНЮЮ страницу. Мало того
1401                // что прибивать его нельзя: пока он висит без .ssr-hiding, CSS
1402                // держит ng-view под display:none, и аварийный экран нового роута
1403                // оказался бы невидимым — читатель смотрел бы старый материал под
1404                // новым адресом. Поэтому здесь рендер именно СНИМАЕТСЯ — и сразу,
1405                // без гейта готовности: ждать ленту НОВОЙ страницы незачем, пока
1406                // на экране висит рендер ПРЕЖНЕЙ.
1407                pinned = false;
1408                reenterRemoveSSR(true);
1409                return false;
1410              }
1411              pinned = true;
1412              if (observer) {
1413                try {
1414                  observer.disconnect();
1415                } catch (e) {
1416                  /* ignored */
1417                }
1418              }
1419              // Ожидание обложки гасим вместе с наблюдателем вставок: пока рендер
1420              // прибит, doHandoff всё равно выходит по `pinned`, а подписка с
1421              // subtree и слежением за классами продолжала бы звать колбэк на
1422              // каждую смену класса — и ровно в те секунды, когда сеть уже плохая,
1423              // а главному потоку предстоит собрать роут заново. Отпускание пина
1424              // заводит ожидание с нуля (см. reenterRemoveSSR).
1425              stopHeroWatch();
1426              var live = document.getElementById(ssrId);
1427              return !!(live && live.parentNode && !live.classList.contains('ssr-hiding'));
1428            };
1429
1430            // Роут всё-таки загрузился (читатель перешёл по ссылке, ретрай удался).
1431            // Отпускаем рендер и снимаем его обычным путём — иначе ng-view остался бы
1432            // под display:none, и новая страница отрисовалась бы в невидимый контейнер.
1433            window.__unpinSSR = function () {
1434              var pathChanged = window.location.pathname !== ssrPath;
1435              // $routeChangeSuccess приходит и без предшествующего route-error.
1436              // Если за время загрузки адрес сменился, SSR описывает прежнюю
1437              // страницу и обязан уйти немедленно — tag-гейт новой страницы
1438              // заведомо никогда не откроется по старому критерию.
1439              if (!pinned && !pathChanged) return false;
1440              pinned = false;
1441              reenterRemoveSSR(pathChanged);
1442              return true;
1443            };
1444
1445            function removeSSR() {
1446              if (pinned) return;
1447              if (removed) return;
1448              if (!ssr) return;
1449              // AngularJS при загрузке может ПЕРЕСОЗДАТЬ #ssr-tag — захваченная при
1450              // парсинге ссылка `ssr` устаревает (отцепленный orphan), и тогда handoff
1451              // снимал бы .ssr-hiding с orphan'а вхолостую, пока живой #ssr-tag остаётся
1452              // видимым и держит ng-view под display:none. Перезапрашиваем живой узел по
1453              // id (подтверждено замером на проде: .ssr-hiding на getElementById('ssr-tag')
1454              // мгновенно раскрывает ленту, reQueryWouldFix:true).
1455              var _liveSsr = document.getElementById(ssr.id);
1456              if (_liveSsr) ssr = _liveSsr;
1457              // #ssr-tag кратко отцепляется от DOM, пока AngularJS компилирует
1458              // route-шаблон в <ng-view>. Если в этот момент взвести removed=true и
1459              // выйти по !parentNode — handoff блокируется навсегда: и observer, и
1460              // аварийный setTimeout ниже потом заглохнут на `if (removed) return`,
1461              // а готовый фид останется под `#ssr-tag:not(.ssr-hiding) ~ ng-view
1462              // { display:none }`. Поэтому на отцепленном узле НЕ латчим removed,
1463              // а повторяем на следующем кадре (с ограничением — финальной
1464              // страховкой служит 8-сек setTimeout ниже).
1465              if (!ssr.parentNode) {
1466                // Тот же запас по кадрам: без него узел, отцепленный на момент
1467                // вызова, во вкладке без кадров не переспрашивался бы никогда.
1468                if (detachRetries++ < 120) onFrameOrTimeout(removeSSR, 50);
1469                return;
1470              }
1471              removed = true;
1472              if (observer) {
1473                try {
1474                  observer.disconnect();
1475                } catch (e) {
1476                  /* ignored */
1477                }
1478              }
1479              // Новый заход отменяет работу предыдущего. Сбои роута идут пачками, и
1480              // каждый заход заводил СВОЙ наблюдатель на ng-view — с subtree и
1481              // слежением за классами, — а старый оставался подписан до своего
1482              // пятисекундного фолбэка. Замер 27.08.2026: пять сбоев подряд оставляли
1483              // шесть живых наблюдателей и семь таймеров, то есть на каждую смену
1484              // класса в поддереве статьи главный поток звал шесть колбэков вместо
1485              // одного. Гасим здесь, а не в ветке ожидания ниже: заход, у которого
1486              // обложки уже нет, уходит коротким путём в doHandoff и до той ветки
1487              // не доходит — прежнее ожидание пережило бы и его.
1488
1489              stopHeroWatch();
1490
1491              // Ключевая идея: НЕ скрываем SSR (#ssr-article) до тех пор, пока Angular'ная
1492              // hero-картинка не загрузится. Пока SSR виден — ng-view скрыт через CSS
1493              // (#ssr-article:not(.ssr-hiding) ~ ng-view { display: none }).
1494              // Браузер загружает <img> внутри display:none и стреляет load-событиями,
1495              // поэтому ng-image-onload ставит is-loaded/is-image-loaded даже в скрытом ng-view.
1496              // Как только hero готов — скрываем SSR и показываем ng-view одним кадром: ноль мерцания.
1497
1498              var heroWrapperSelector =
1499                '.open-post-title-image-wrapper, .expo-item-title-image-wrapper';
1500              var loadedWrapperSelector =
1501                '.open-post-title-image-wrapper.is-image-loaded, ' +
1502                '.expo-item-title-image-wrapper.is-image-loaded';
1503              var imgSelector =
1504                '.open-post-title-image.is-loaded, ' + '.expo-item-title-image.is-loaded';
1505              // Music has no hero-wrapper: its above-the-fold visual is the square
1506              // cover in Worker scaffold. Match the full scaffold, not a generic
1507              // `.audio-img`: article body is user content and may reuse that class.
1508              var audioCoverSelector =
1509                '.expo-item .audio-wrapper > .audio-container > .audio-img img';
1510              var ssrHasAudioCover = !!ssr.querySelector(audioCoverSelector);
1511
1512              function getNgView() {
1513                return document.querySelector('ng-view, [ng-view]');
1514              }
1515
1516              function hasHeroWrapper() {
1517                var ngView = getNgView();
1518                if (!ngView) return document.querySelector(heroWrapperSelector);
1519                return ngView.querySelector(heroWrapperSelector);
1520              }
1521
1522              function isHeroReady() {
1523                var ngView = getNgView();
1524                if (!ngView) {
1525                  return (
1526                    document.querySelector(loadedWrapperSelector) ||
1527                    document.querySelector(imgSelector)
1528                  );
1529                }
1530                return (
1531                  ngView.querySelector(loadedWrapperSelector) || ngView.querySelector(imgSelector)
1532                );
1533              }
1534
1535              function isAudioCoverReady() {
1536                if (!ssrHasAudioCover) return true;
1537                var ngView = getNgView();
1538                if (!ngView) return false;
1539                var image = ngView.querySelector(audioCoverSelector);
1540                return !!(image && image.complete && image.naturalWidth > 0);
1541              }
1542
1543              function hasCriticalVisual() {
1544                return ssrHasAudioCover || !!hasHeroWrapper();
1545              }
1546
1547              function isCriticalVisualReady() {
1548                return isAudioCoverReady() && (!hasHeroWrapper() || !!isHeroReady());
1549              }
1550
1551              function doHandoff() {
1552                // Прибитый рендер не снимаем даже из уже запущенного handoff:
1553                // removeSSR() латчит removed=true раньше, чем hero-обозреватель
1554                // (или его 5-сек фолбэк) доберётся сюда, поэтому одной проверки
1555                // в removeSSR() недостаточно — окно между ними реально.
1556                if (pinned) return;
1557                // Второй заход штатен и реален: hero-обозреватель снимает рендер
1558                // раньше срока, а 5-секундный фолбэк ниже НЕ отменяется и
1559                // приходит следом. К этому моменту `ssr` уже обнулён, и
1560                // `ssr.classList` роняет колбэк таймера ДО того, как заведётся
1561                // 600-мс уборка, — тогда `ssr-just-swapped` и `has-ssr-content`
1562                // остаются на <html> навсегда: у всех картинок насмерть выключен
1563                // fade-in, а оверлей загрузки роута больше не показывается.
1564                if (handedOff) return;
1565                handedOff = true;
1566                // ssr-just-swapped: подавляет fade-in картинок ПОСЛЕ swap
1567                // + подавляет loading-footer-overlay (см. CSS выше)
1568                document.documentElement.classList.add('ssr-just-swapped');
1569                ssr.classList.add('ssr-hiding');
1570                // has-ssr-content НЕ удаляем сразу — оставляем до 600ms timeout,
1571                // чтобы loading-footer-overlay оставался скрыт пока Angular рендерится.
1572                // ssr-just-swapped — дополнительная страховка (тоже скрывает overlay).
1573                onFrameOrTimeout(function () {
1574                  if (ssr && ssr.parentNode) ssr.remove();
1575                  ssr = null;
1576                  window.__removeSSR = null;
1577                }, 250);
1578                // Убираем ОБА класса через 600ms — Angular к этому моменту уже отрендерился,
1579                // его ng-if убрал overlay из DOM.
1580                setTimeout(function () {
1581                  document.documentElement.classList.remove('ssr-just-swapped');
1582                  document.documentElement.classList.remove('has-ssr-content');
1583                  document.documentElement.classList.remove('has-ssr-tag');
1584                }, 600);
1585              }
1586
1587              // Если критической картинки нет или она уже загружена — переключаем сразу.
1588              // Для Music ждём именно Angular-обложку: `$includeContentLoaded`
1589              // сообщает о шаблоне, но вложенный `ng-if` появляется следующим digest.
1590              if (!hasCriticalVisual() || isCriticalVisualReady()) {
1591                onFrameOrTimeout(doHandoff, 250);
1592                return;
1593              }
1594
1595              // Ждём загрузки hero/аудиообложки через MutationObserver.
1596              // Наблюдаем стабильного родителя SSR и ng-view: AngularJS может
1597              // заменить сам <ng-view> после retry, и подписка на старый узел
1598              // тогда проспала бы готовую картинку до 5-секундного fallback.
1599              // Прежнее ожидание к этому моменту уже погашено — на старте захода.
1600              heroObs = new MutationObserver(function () {
1601                if (isCriticalVisualReady()) {
1602                  stopHeroWatch();
1603                  onFrameOrTimeout(doHandoff, 250);
1604                }
1605              });
1606              heroObs.observe(ssr.parentNode || document.body, {
1607                childList: true,
1608                subtree: true,
1609                attributes: true,
1610                attributeFilter: ['class'],
1611              });
1612
1613              // Fallback: не ждём бесконечно — переключаем через 5 секунд
1614              heroFallbackTimer = setTimeout(function () {
1615                stopHeroWatch();
1616                doHandoff();
1617              }, 5000);
1618            }
1619
1620            // Expose for controller-driven removal (MagazinePostCtrl, ExpoItemCtrl)
1621            window.__removeSSR = removeSSR;
1622
1623            // Homepage/Tag SSR: use MutationObserver (no controller signal available).
1624            // Article/Author SSR: controller calls window.__removeSSR() after content renders.
1625            var isAuthor = ssr.id === 'ssr-author';
1626            if (!isArticle && !isAuthor) {
1627              if (observeContentMutations()) {
1628                // Для tag-страниц ждём РЕАЛЬНЫЙ результат фида, а не первый вставленный
1629                // node, пустой host <article> или голую шапку: либо скомпилированные
1630                // карточки (.article > .article__inner), либо финальное пустое/ошибочное
1631                // состояние (.feed-empty). Skeleton
1632                // (.tag-skeleton — без .article) готовностью НЕ считается, иначе при
1633                // медленном или упавшем фиде живые SSR-карточки снялись бы, обнажив
1634                // skeleton/пустоту. Happy-path (resolve с items) НЕ меняется: items
1635                // добавляются синхронно в $onInit, а handoff срабатывает сразу после
1636                // загрузки шаблона article-компонента — когда карточка уже видима.
1637                // Критерий готовности живёт в одном месте — contentGateOpen выше.
1638                // Прежде он был описан дважды: здесь и в ветке отпускания пина, и
1639                // расхождение между копиями было вопросом времени.
1640                // Таймер-страховка к MutationObserver. На тяжёлом холодном заходе observer
1641                // может ПРОПУСТИТЬ появление контента (карточки отрисовались до observe(),
1642                // либо мутация не дошла), и тогда главный поток ПРОСТАИВАЕТ с готовой-но-
1643                // скрытой лентой, пока случайная поздн
1643яя мутация (загрузка картинки, ~3-5 c)
1644                // не дёрнет наблюдатель. Performance-профиль показал именно это: ~67% времени
1645                // главный поток idle. Простой setInterval проверяет ТОТ ЖЕ гейт независимо от
1646                // мутаций и снимает SSR сразу, как только контент готов. Преждевременно
1647                // сработать не может (условие то же, что у observer). Самоочистка: по removed
1648                // или по лимиту тиков (≈9 с, чуть дольше 8-сек бэкстопа).
1649                var _pollTicks = 0;
1650                var _pollId = setInterval(function () {
1651                  if (removed || ++_pollTicks > 90) {
1652                    clearInterval(_pollId);
1653                    return;
1654                  }
1655                  if (contentGateOpen()) {
1656                    clearInterval(_pollId);
1657                    removeSSR();
1658                  }
1659                }, 100);
1660              }
1661            }
1662
1663            // Safety timeout: remove SSR even if controller/observer doesn't fire.
1664            // Article: 5с (ждём hero-image), остальные (Homepage/Tag/Author): 8с
1665            // (тяжёлый контент; на медленных устройствах Angular route грузится долго).
1666            // Тег — исключение: пустые host-элементы карточек ещё не дают читателю
1667            // контента, поэтому его SSR остаётся до карточки или явного feed-empty.
1668            // На slow-2g/2g увеличиваем timeout — Angular грузится дольше, SSR должен оставаться.
1669            var conn = navigator.connection || navigator.mozConnection;
1670            var isSlow = conn && (conn.effectiveType === 'slow-2g' || conn.effectiveType === '2g');
1671            var ssrTimeout = isArticle ? (isSlow ? 10000 : 5000) : isSlow ? 15000 : 8000;
1672            setTimeout(function () {
1673              if (ssrId === 'ssr-tag' && !contentGateOpen()) return;
1674              // AngularJS temporarily replaces <ng-view> while compiling a cold
1675              // route. Removing an article/expo SSR block in that window exposes
1676              // a blank page until the controller can signal readiness. Keep the
1677              // useful server render; a ready controller still calls __removeSSR,
1678              // and the existing no-frame fallback remains active after that call.
1679              if (isArticle) {
1680                var liveNgView = document.querySelector('ng-view, [ng-view]');
1681                var articleRouteReady =
1682                  liveNgView &&
1683                  liveNgView.querySelector(
1684                    '.open-post, .expo-item, ' +
1685                      '.open-post-title-image-wrapper, .expo-item-title-image-wrapper, ' +
1686                      '.auth-required-screen',
1687                  );
1688                if (!articleRouteReady) return;
1689              }
1690              removeSSR();
1691            }, ssrTimeout);
1692          } catch (e) {
1693            // Safety net: SSR cleanup не должен крашить Angular boot
1694            try {
1695              if (ssr && ssr.parentNode) ssr.remove();
1696            } catch (_) {
1697              /* ignore */
1698            }
1699          }
1700        })();
1701      </script>
1701
1702
1703      <!-- SSR H1 Cleanup: снимает #ssr-h1 воркера, когда Angular отрисовал свой
1704           заголовок. Почему отдельно от очистки выше — в SsrH1Handler (rewriters.js).
1705           Тег заголовка тут написан словом намеренно: разметка внутри комментария
1706           подделывается под настоящую и сбивает и грепы, и внешние сканеры. -->
1707      
1707<script>
1708        (function () {
1709          try {
1710            var ssrH1 = document.getElementById('ssr-h1');
1711            if (!ssrH1) return;
1712
1713            // Наблюдаем за РОДИТЕЛЕМ ng-view, а не за ним самим: AngularJS
1714            // пересоздаёт <ng-view> при компиляции, и подписка на захваченный
1715            // узел перестаёт получать мутации (проверено в браузере 05.08.2026 —
1716            // #ssr-h1 оставался вторым h1 при готовом Angular-заголовке).
1717            // Родитель <main> статичен, замена ng-view видна как мутация в нём.
1718            var host = ssrH1.parentNode;
1719            if (!host) return;
1720
1721            var obs;
1722            var timer;
1723
1724            function stop() {
1725              if (obs) {
1726                try {
1727                  obs.disconnect();
1728                } catch (e) {
1729                  /* ignored */
1730                }
1731                obs = null;
1732              }
1733              if (timer) {
1734                clearTimeout(timer);
1735                timer = null;
1736              }
1737            }
1738
1739            function dropIfAngularHasH1() {
1740              // Узел ng-view запрашиваем каждый раз заново — по той же причине.
1741              var ng = document.querySelector('ng-view, [ng-view]');
1742              if (!ng || !ng.querySelector('h1')) return false;
1743              stop();
1744              if (ssrH1 && ssrH1.parentNode) ssrH1.remove();
1745              ssrH1 = null;
1746              return true;
1747            }
1748
1749            if (dropIfAngularHasH1()) return;
1750
1751            obs = new MutationObserver(function () {
1752              dropIfAngularHasH1();
1753            });
1754            obs.observe(host, { childList: true, subtree: true });
1755
1756            // 60 с, а не 15: на нагруженной машине Angular отрисовал заголовок
1757            // через 17 с, и короткий таймаут оставил бы два h1 навсегда.
1758            // Наблюдатель дёшев (замер: 5 вызовов на 105 пачек мутаций).
1759            timer = setTimeout(stop, 60000);
1760          } catch (e) {
1761            /* очистка h1 не должна мешать загрузке Angular */
1762          }
1763        })();
1764      </script>
1764
1765    </main>
1766
1767    <!-- Правовые ссылки без JS: проверка бренда OAuth в Google требует видимую
1768         ссылку на политику конфиденциальности с главной, а подвал <site-footer>
1769         рисует Angular — без JS на главной видна только ссылка «Перейти к
1770         содержимому» (замер 28.09.2026). Статическая копия в overlay загрузки
1771         не годится: overlay скрыт до старта Angular, и ссылка была бы только
1772         в исходнике. С JS блок не отображается вовсе. -->
1773    <noscript>
1774      <p class="ds-container">
1775        <a href="/terms-of-use" class="copyright-link">Правила сайта</a>&nbsp;·
1776        <a href="/privacy-policy" class="copyright-link">Политика конфиденциальности</a>
1777      </p>
1778    </noscript>
1779
1780    <!-- Footer: hidden during route loading and in editor mode (hideSiteHeader) to prevent CLS -->
1781    <div class="footer-wrapper" ng-if="!$root.statusCode || $root.isStubPage" ng-show="!$root.isRouteLoading && !$root.hideSiteHeader" site-loading>
1782      <site-footer></site-footer>
1783    </div>
1784
1785    <!-- Music player (global audio player) -->
1786    <div ng-if="!$root.statusCode || $root.isStubPage">
1787      <music-player></music-player>
1788    </div>
1789
1790    <!-- ================================================================== -->
1791    <!-- VENDOR SCRIPTS: Split into small bundles to reduce long tasks (TBT) -->
1792    <!-- Built by: npm run prebuild → scripts/build-vendor-libs.js          -->
1793    <!-- ================================================================== -->
1794
1795    <!-- ================================================================== -->
1796    <!-- ALL vendor scripts use `defer` to unblock first paint (FCP).       -->
1797    <!-- Defer guarantees: (1) parallel download during parsing,            -->
1798    <!-- (2) execution in document order, (3) before DOMContentLoaded.      -->
1799    <!-- This lets the browser paint HTML+CSS without waiting for ~3.5s     -->
1800    <!-- of JS evaluation, dramatically improving FCP (5.7s → ~2s target).  -->
1801    <!-- ================================================================== -->
1802
1803    <!-- JQUERY: Must execute first — Angular and other libs depend on it   -->
1804    
1804<script defer="defer" src="/scripts/vendor-jquery-7d17a96c.js"></script>
1804
1805
1806    <!-- ANGULAR CORE: angular.min.js only                                 -->
1807    
1807<script defer="defer" src="/scripts/vendor-angular-core-dd9e4fc5.js"></script>
1807
1808
1809    <!-- ANGULAR MODULES: route + sanitize + touch                         -->
1810    
1810<script defer="defer" src="/scripts/vendor-angular-modules-53712019.js"></script>
1810
1811
1812    <!-- LAZYSIZES: Image lazy loading (standalone, no dependencies).
1813         Changed from defer to async — removes it from the DOMContentLoaded
1814         blocking chain, allowing Angular to bootstrap ~50ms faster.
1815         Native loading="lazy" handles most cases; lazysizes is supplementary. -->
1816    
1816<script async src="/scripts/vendor-lazysizes-1ab9fa33.js"></script>
1816
1817
1818    <!-- UI PLUGINS: Slick carousel + Bootstrap jQuery plugins.
1819         Must stay defer (depends on jQuery being available globally).
1820         async would risk execution before jQuery, causing errors. -->
1821    
1821<script defer="defer" src="/scripts/vendor-ui-deferred-fb1641de.js"></script>
1821
1822
1823    <!-- ================================================================== -->
1824    <!-- DEFERRED VENDOR SCRIPTS: Not needed for initial render             -->
1825    <!-- Loaded with defer to avoid blocking first paint                    -->
1826    <!-- ================================================================== -->
1827
1828    <!-- Bootbox удалён — заменён на dialog-lite.js (встроенный в Webpack-бандл).
1829         Используется в: ratingButtons (диалог отзыва), captionEditorService (подпись). -->
1830
1831    <!-- He.js removed: replaced with native htmlDecode utility in Webpack bundle (saves ~99KB) -->
1832
1833    <!-- ================================================================== -->
1834    <!-- APPLICATION: Webpack bundles (includes config.js for proxy URL replacement) -->
1835    <!-- All use `defer` (set by HtmlWebpackPlugin scriptLoading: 'defer')  -->
1836    <!-- ================================================================== -->
1837    <!-- Note: config.js is now bundled in application-*.js to allow proxy to replace API URLs -->
1838
1839    <!-- Webpack bundles (auto-injected by HtmlWebpackPlugin) -->
1840    
1841    
1841<script defer="defer" src="/scripts/runtime-0b964bee.js"></script>
1841
1842    
1843    
1843<script defer="defer" src="/scripts/vendor-4feffd26.js"></script>
1843
1844    
1845    
1845<script defer="defer" src="/scripts/application-0d4abaf6.js"></script>
1845
1846    
1847
1848    <!-- ================================================================== -->
1849    <!-- POST-APP SCRIPTS: Execute after Webpack bundles (need window.* functions) -->
1850    <!-- ================================================================== -->
1851
1852    <!-- Asteroid (DDP client for real-time WebSocket notifications) -->
1853    <!-- MUST be after Webpack bundles: initializeAsteroid определена в application bundle.
1854         С defer-скриптами выполнение идёт по порядку документа — этот скрипт запустится после app bundle.
1855         CSP: onload-обработчик заменён на скрипт ниже для совместимости с nonce-based CSP. -->
1856    
1856<script defer="defer" src="/scripts/vendor-asteroid-a82aa62d.js" id="asteroid-script"></script>
1856
1857    
1857<script>
1858      (function () {
1859        var s = document.getElementById('asteroid-script');
1860        if (!s) return;
1861        s.addEventListener('load', function () {
1862          if (window.initializeAsteroid) window.initializeAsteroid();
1863        });
1864      })();
1865    </script>
1865
1866
1867    <!-- Optional utilities loaded after main app (async — not needed for Angular bootstrap,
1868         removed from defer chain so they don't block DOMContentLoaded) -->
1869    
1869<script async src="/scripts/utils/scheduleTasks.js"></script>
1869
1870
1871    <!-- Editor scripts loader: dynamically loads CodeMirror, Typograf -->
1872    
1872<script async src="/scripts/utils/loadEditorScripts.js"></script>
1872
1873
1874    <!-- Manual Angular bootstrap
1875         All vendor and application scripts use `defer` and execute in order before
1876         DOMContentLoaded. Bootstrap uses setTimeout(0) to yield to the browser between
1877         script execution and Angular compilation, breaking the long task chain and
1878         improving TBT. Lazysizes is the only async script (no Angular dependency). -->
1879    
1879<script>
1880      (function () {
1881        var maxAttempts = 50; // 5 seconds max (50 * 100ms)
1882        var attempt = 0;
1883
1884        function tryBootstrap() {
1885          attempt++;
1886
1887          // Check if Angular is available
1888          if (typeof angular === 'undefined' || !angular.module) {
1889            if (attempt < maxAttempts) {
1890              setTimeout(tryBootstrap, 100);
1891            } else {
1892              console.error('[Bootstrap] Angular not available after', maxAttempts * 100, 'ms');
1893            }
1894            return;
1895          }
1896
1897          var rootEl = document.getElementById('discours-app');
1898          if (!rootEl) {
1899            if (attempt < maxAttempts) {
1900              setTimeout(tryBootstrap, 100);
1901            } else {
1902              console.error('[Bootstrap] Element #discours-app not found');
1903            }
1904            return;
1905          }
1906          // Avoid double bootstrap (e.g. hot reload or duplicate script run)
1907          try {
1908            if (angular.element(rootEl).data('$injector')) {
1909              return;
1910            }
1911          } catch (e) {}
1912          // Check if discoursApp module is available
1913          try {
1914            angular.module('discoursApp');
1915            angular.bootstrap(rootEl, ['discoursApp']);
1916          } catch (e) {
1917            if (attempt === 1) {
1918              console.error('[Bootstrap] First attempt failed:', e && e.message ? e.message : e);
1919            }
1920            if (attempt < maxAttempts) {
1921              setTimeout(tryBootstrap, 100);
1922            } else {
1923              console.error('[Bootstrap] Failed after', attempt, 'attempts:', e);
1924            }
1925          }
1926        }
1927
1928        // Yield to main thread before Angular bootstrap to break the long task chain.
1929        // scheduler.yield() (when available) yields with higher priority than setTimeout(0),
1930        // ensuring the bootstrap task runs sooner while still allowing paint + input processing.
1931        function yieldThenBootstrap() {
1932          if (typeof scheduler !== 'undefined' && typeof scheduler.yield === 'function') {
1933            scheduler.yield().then(tryBootstrap);
1934          } else {
1935            setTimeout(tryBootstrap, 0);
1936          }
1937        }
1938        if (document.readyState === 'loading') {
1939          document.addEventListener('DOMContentLoaded', yieldThenBootstrap);
1940        } else {
1941          yieldThenBootstrap();
1942        }
1943      })();
1944    </script>
1944
1945
1946    <!-- Service Worker registration: caches fonts, CSS, JS, and Thumbor images + API responses
1947         for dramatically faster repeat visits and offline resilience.
1948         Registered after page load to avoid competing with initial resource loading.
1949         Periodic update check ensures users get new SW within 1 hour after deploy. -->
1950    
1950<script>
1951      if ('serviceWorker' in navigator) {
1952        var hostname = window.location.hostname;
1953        var isLocalhost =
1954          hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '[::1]';
1955
1956        window.addEventListener('load', function () {
1957          if (isLocalhost) {
1958            Promise.all([
1959              navigator.serviceWorker.getRegistrations().then(function (registrations) {
1960                return Promise.all(
1961                  registrations.map(function (registration) {
1962                    return registration.unregister();
1963                  }),
1964                );
1965              }),
1966              typeof caches !== 'undefined'
1967                ? caches.keys().then(function (keys) {
1968                    return Promise.all(
1969                      keys.map(function (key) {
1970                        return caches.delete(key);
1971                      }),
1972                    );
1973                  })
1974                : Promise.resolve(),
1975            ])
1976              .catch(function () {
1977                /* SW cleanup error — не критично */
1978              })
1979              .finally(function () {
1980                try {
1981                  var reloadKey = 'localhost-sw-reset';
1982                  if (navigator.serviceWorker.controller && !sessionStorage.getItem(reloadKey)) {
1983                    sessionStorage.setItem(reloadKey, '1');
1984                    window.location.reload();
1985                  }
1986                } catch (e) {
1987                  /* sessionStorage недоступен (приватный режим) — пропускаем reload */
1988                }
1989              });
1990            return;
1991          }
1992
1993          // Флаг: перезагрузка уже запланирована (предотвращает двойной reload)
1994          var swReloading = false;
1995          // Запоминаем начальный контроллер ДО регистрации. Если null — это первая активация,
1996          // баннер «Обновить» не нужен (пользователь уже видит свежий HTML).
1997          var hadController = !!navigator.serviceWorker.controller;
1998          // Флаг для race condition: controllerchange может сработать до загрузки sw-register.js.
1999          // sw-register.js проверяет этот флаг сразу после инициализации и показывает баннер.
2000          window.__swUpdatePending = false;
2001
2002          // Listener ДОЛЖЕН быть inline — если вынести в async-скрипт, controllerchange
2003          // может сработать раньше, чем скрипт загрузится (new SW skipWaiting при первом визите).
2004          navigator.serviceWorker.addEventListener('controllerchange', function () {
2005            if (swReloading) return;
2006            if (!hadController) return;
2007            // showSwUpdateBanner определена в sw-register.js (загружается ниже)
2008            if (typeof window.__showSwUpdateBanner === 'function') {
2009              window.__showSwUpdateBanner(function () {
2010                swReloading = true;
2011              });
2012            } else {
2013              // sw-register.js ещё не загрузился — баннер покажем при его load
2014              window.__swUpdatePending = true;
2015            }
2016          });
2017
2018          // Регистрация SW и баннер обновления — вынесены в отдельный модуль.
2019          var s = document.createElement('script');
2020          s.src = '/scripts/sw-register.js';
2021          s.async = true;
2022          document.body.appendChild(s);
2023        });
2024      }
2025    </script>
2025
2026
2027    <!-- Google Analytics (deferred with event buffering) -->
2028    
2028<script>
2029      // Buffer dataLayer events before gtag.js loads to avoid missing early page events
2030      window.dataLayer = window.dataLayer || [];
2031      function gtag() {
2032        window.dataLayer.push(arguments);
2033      }
2034      window.gtag = gtag;
2035
2036      // Pre-configure GA before script loads (events are buffered in dataLayer)
2037      gtag('js', new Date());
2038      gtag('config', 'G-LQ4B87H8C2', {
2039        transport_type: 'beacon',
2040        anonymize_ip: true,
2041      });
2042
2043      // Load gtag.js after first user interaction to avoid TBT impact on INP.
2044      // GTM contributes ~172ms of main-thread work (two long tasks: 94ms + 78ms).
2045      // Deferring until interaction pushes this work to when the user is actively
2046      // engaged (expecting slight delay), not during passive page viewing.
2047      // The dataLayer buffer ensures no events are lost during this deferral.
2048      // Safety: 10s timeout guarantees GA loads even for passive visitors.
2049      (function () {
2050        var loaded = false;
2051        function loadGA() {
2052          if (loaded) return;
2053          loaded = true;
2054          removeInteractionListeners();
2055          var s = document.createElement('script');
2056          s.async = true;
2057          s.src = 'https://www.googletagmanager.com/gtag/js?id=G-LQ4B87H8C2';
2058          document.head.appendChild(s);
2059        }
2060
2061        function scheduleGA() {
2062          if ('requestIdleCallback' in window) {
2063            requestIdleCallback(loadGA, { timeout: 5000 });
2064          } else {
2065            setTimeout(loadGA, 4000);
2066          }
2067        }
2068
2069        // Listen for first user interaction (click, scroll, touch, keydown)
2070        var interactionEvents = ['click', 'scroll', 'touchstart', 'keydown'];
2071        function onFirstInteraction() {
2072          removeInteractionListeners();
2073          scheduleGA();
2074        }
2075        function removeInteractionListeners() {
2076          interactionEvents.forEach(function (evt) {
2077            window.removeEventListener(evt, onFirstInteraction);
2078          });
2079        }
2080
2081        function init() {
2082          // Register interaction listeners
2083          interactionEvents.forEach(function (evt) {
2084            window.addEventListener(evt, onFirstInteraction, { once: true, passive: true });
2085          });
2086          // Safety: load GA after 10s even without interaction (passive visitors / bots)
2087          setTimeout(scheduleGA, 10000);
2088        }
2089
2090        // Start listening after page load (don't add listeners during initial parse)
2091        if (document.readyState === 'complete') {
2092          init();
2093        } else {
2094          window.addEventListener('load', init);
2095        }
2096      })();
2097    </script>
2097
2098
2099    <!-- Кнопка «Наверх» — появляется при прокрутке > 2 экранов -->
2100    <style>#back-to-top{align-items:center;justify-content:center;padding:0;line-height:0;transition:opacity .25s ease,transform .25s ease,background-color .2s ease,color .2s ease,border-color .2s ease,box-shadow .2s ease}#back-to-top svg{display:block;width:18px;height:18px}#back-to-top:hover{background:rgba(20,20,20,.92)!important;color:#fff!important;border-color:rgba(20,20,20,.92)!important;box-shadow:0 4px 14px rgba(0,0,0,.18)!important}@media (hover:hover) and (pointer:fine) and (min-width:768px){#back-to-top::before{content:'Наверх';position:absolute;right:calc(100% + 10px);top:50%;transform:translateY(-50%) translateX(4px);padding:6px 10px;background:rgba(20,20,20,.92);color:#fff;font-size:13px;line-height:1;font-family:inherit;white-space:nowrap;border-radius:6px;box-shadow:0 2px 8px rgba(0,0,0,.15);opacity:0;pointer-events:none;transition:opacity .18s ease,transform .18s ease}#back-to-top:hover::before{opacity:1;transform:translateY(-50%) translateX(0)}}</style>
2101    <button id="back-to-top" type="button" aria-label="Наверх" ng-attr-aria-label="{{ 'Наверх' | t }}" style="display:none;position:fixed;bottom:24px;right:24px;z-index:100;width:44px;height:44px;border-radius:50%;border:1px solid rgba(0,0,0,.15);background:rgba(255,255,255,.92);color:#333;cursor:pointer;box-shadow:0 2px 8px rgba(0,0,0,.1);opacity:0;transform:translateY(8px);backdrop-filter:blur(4px)">
2102      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
2103        <path d="M12 19V5M5 12l7-7 7 7"/>
2104      </svg>
2105    </button>
2106    
2106<script>
2107      (function () {
2108        var btn = document.getElementById('back-to-top');
2109        if (!btn) return;
2110        var visible = false;
2111        var threshold = window.innerHeight * 2;
2112        var lastScrollY = window.scrollY;
2113        var isMobile = window.innerWidth < 768;
2114
2115        /* Мобильные отступы */
2116        if (isMobile) {
2117          btn.style.bottom = '15px';
2118          btn.style.right = '15px';
2119        }
2120
2121        function showBtn() {
2122          if (visible) return;
2123          visible = true;
2124          btn.style.display = 'flex';
2125          requestAnimationFrame(function () {
2126            btn.style.opacity = '1';
2127            btn.style.transform = 'translateY(0)';
2128          });
2129        }
2130
2131        function hideBtn() {
2132          if (!visible) return;
2133          visible = false;
2134          btn.style.opacity = '0';
2135          btn.style.transform = 'translateY(8px)';
2136          setTimeout(function () {
2137            btn.style.display = 'none';
2138          }, 300);
2139        }
2140
2141        window.addEventListener(
2142          'scroll',
2143          function () {
2144            var scrollY = window.scrollY;
2145            var deepEnough = scrollY > threshold;
2146            var scrollingUp = scrollY < lastScrollY;
2147            lastScrollY = scrollY;
2148
2149            if (!deepEnough) {
2150              hideBtn();
2151              return;
2152            }
2153
2154            /* На десктопе — всегда видна, на мобильных — только при скролле вверх */
2155            if (isMobile) {
2156              if (scrollingUp) {
2157                showBtn();
2158              } else {
2159                hideBtn();
2160              }
2161            } else {
2162              showBtn();
2163            }
2164          },
2165          { passive: true },
2166        );
2167        btn.addEventListener('click', function () {
2168          window.scrollTo({ top: 0, behavior: 'smooth' });
2169        });
2170      })();
2171    </script>
2171
2172  </body>
2173</html>

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.