/* ══════ 00-fonts.css ══════ */
/* ============================================================
   00-FONTS — Inter, свой хостинг
   Файлы лежат в src/assets/fonts/ и копируются в dist как есть.
   Google Fonts намеренно не используется: внешний домен блокирует
   первую отрисовку и передаёт IP посетителя за пределы РФ (152-ФЗ).
   ============================================================ */

/* Один вариативный файл на всю ось 100..900 — CSS свободно пользуется
   промежуточными весами (420, 620, 650, 720), а не только ступенями.
   Со статикой 400/500/600/700 браузер округлял их или синтезировал жир. */

/* cyrillic — основной для сайта, грузится первым (preload в <head>) */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url('/assets/fonts/inter-cyrillic.woff2') format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}

/* latin — цифры, телефоны, латиница в адресах */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url('/assets/fonts/inter-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url('/assets/fonts/inter-cyrillic-ext.woff2') format('woff2');
  unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}

@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url('/assets/fonts/inter-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
    U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* Подменный шрифт с метриками Inter.
   Без него в первые ~100 мс страница рисуется системным шрифтом другой
   ширины и высоты строки, а после загрузки Inter текст «прыгает» (CLS).
   Переопределения приводят локальный гротеск к пропорциям Inter, так что
   подмена проходит почти незаметно. */
@font-face {
  font-family: 'Inter Fallback';
  font-style: normal;
  font-weight: 100 900;
  src: local('Segoe UI'), local('Roboto'), local('Helvetica Neue'), local('Arial');
  ascent-override: 90%;
  descent-override: 22.43%;
  line-gap-override: 0%;
  size-adjust: 107.12%;
}

/* ══════ 01-tokens.css ══════ */
/* ============================================================
   01-TOKENS — единственный источник правды по дизайну
   Комплекс-А · Design System v4 «Стояк»

   Правило: в остальных файлах не должно быть сырых цветов.
   Любой цвет, граница и тень берутся отсюда: правки цвета делаются
   в одном месте, а не поиском по файлам.
   ============================================================ */

:root {
  /* «only light», а не просто «light»: мобильные браузеры при системной
     тёмной теме умеют насильно инвертировать чужие страницы (Chrome
     Android — флаг Force Dark, Samsung Internet и MIUI делают это без
     спроса). У сайта своей тёмной темы нет, и такая инверсия красит его
     в чёрное вместе с текстом. Ключевое слово only запрещает подмену. */
  color-scheme: only light;

  /* ── Чернила (вместо серого — тёплый сине-графитовый) ──
     Шкала пересчитана под WCAG AA: каждый оттенок, которым набирают
     текст, даёт не меньше 4.5:1 на белом и на --paper-2.
     Проверено: 500 → 7.9:1, 400 → 6.4:1, 300 → 5.4:1.
     Прежние #93A6C0 (2.5:1) и #6B82A3 (3.9:1) проверку не проходили. */
  --ink-900: #08152E;
  --ink-800: #12294A;
  --ink-700: #1E3A5F;
  --ink-500: #3B5279;
  --ink-400: #456085;
  --ink-300: #4F6C93;
  /* Единственный декоративный оттенок: разделители и «ручка» модалки.
     Текстом не набирать — контраста не хватит. */
  --ink-200: #C3D0E0;

  /* ── Корпоративный синий ── */
  --brand-900: #062F5C;
  --brand-800: #07437F;
  --brand-700: #07529C;
  --brand-600: #0A66C2;   /* базовый корпоративный */
  --brand-500: #1478DB;
  --brand-400: #3B9BFF;   /* свечение магистрали */

  /* Синий, которым набирают ТЕКСТ и рисуют иконки.
     Отделён от заливок: текст и иконки должны читаться на бумаге,
     а градиент кнопки — оставаться тёмным под белой подписью.
     Раньше и то и другое было --brand-600. */
  --accent:        var(--brand-600);
  --accent-strong: var(--brand-700);
  --accent-soft:   var(--brand-500);

  /* ── Подложки ── */
  --paper:     #FFFFFF;
  --paper-2:   #F4F7FB;   /* тёплая бумага */
  --tint:      #EAF2FB;   /* светло-синяя зона */
  --tint-2:    #DCEAF9;

  /* ── Границы и поверхности состояний ──
     До этого по файлам было рассыпано шесть разных rgba(8,21,46,.0x);
     разница между .07 и .09 на глаз не читалась. */
  --line:         rgba(8, 21, 46, .08);   /* хайрлайн: строки, футер, карточки */
  --line-2:       rgba(8, 21, 46, .14);   /* заметная граница */
  --line-brand:   rgba(10, 102, 194, .14);
  --line-brand-2: rgba(10, 102, 194, .22);
  --surface-hover: var(--tint);
  --glow:         rgba(59, 155, 255, .16); /* ореол узлов магистрали */
  --skeleton-sweep: rgba(255, 255, 255, .85);

  /* ── Сигнальные (статус офиса: открыт / перерыв / закрыт) ── */
  --alert-600: #D92D20;
  --ok-600:    #17B26A;
  --ok-700:    #067647;
  --warn-500:  #F5A524;
  --warn-700:  #B54708;

  /* Жёлтый звёзд — один на весь сайт. Раньше рейтинги на картах были
     #F5A524, а звёзды в отзывах #FACC15: одна сущность, два цвета. */
  --star: #F5A524;

  /* ── Типографика ──
     Inter подключён вариативным файлом (00-fonts.css), поэтому
     промежуточные веса (420, 620, 650, 720) — настоящие начертания,
     а не округление до ближайшей ступени. */
  --font: 'Inter', 'Inter Fallback', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;

  --fs-h1:      clamp(28px, 7.4vw, 52px);
  --fs-h2:      clamp(23px, 5.6vw, 36px);
  --fs-h3:      clamp(18px, 4.4vw, 22px);
  --fs-lead:    clamp(16px, 4.2vw, 20px);
  --fs-body:    16px;
  --fs-sm:      14px;
  --fs-xs:      12px;
  --fs-micro:   11px;

  --lh-tight: 1.06;
  --lh-body:  1.62;
  --lh-loose: 1.75;

  --tr-display: -0.045em;
  --tr-head:    -0.03em;
  --tr-body:    -0.011em;
  --tr-caps:     0.12em;

  /* ── Вертикальный ритм (воздух) ── */
  --sp-1:  4px;
  --sp-2:  8px;
  --sp-3:  12px;
  --sp-4:  16px;
  --sp-5:  24px;
  --sp-6:  32px;
  --sp-7:  44px;
  --sp-8:  60px;
  --sp-9:  80px;
  --sp-10: 112px;

  /* Плотность вёрстки: множитель МЕЖБЛОЧНЫХ расстояний. Отдельная ручка
     от --zoom: тот меняет размер всего, этот — только воздух между блоками,
     не трогая внутренности карточек. 1 — исходный ритм, .75 — на четверть
     плотнее. Применяется к --block-gap, секционному отступу (03-layout.css)
     и верхнему полю вступительных блоков (04-modules.css). */
  /* 1.2 — мобильное значение: даёт 38px между блоками и 67px в паузе перед
     разделом. Было 1.6 (51 и 90px) — на телефоне это читалось как провалы
     между блоками. На планшете и десктопе возвращается к .75 (ниже по файлу). */
  --air: 1.2;

  --block-gap: calc(var(--sp-6) * var(--air));   /* дистанция между блоками */
  /* 20px, а не 28: вместе с колонкой стояка поля съедали 96px из 375, и на
     содержимое оставалось меньше двух третей экрана. На планшете и десктопе
     поля возвращаются к прежним (ниже по файлу). */
  --page-pad:  20px;
  /* Левое поле шире правого: стояк уехал вправо и съедает правый край,
     из-за чего вся масса текста читалась прижатой влево. Освободившиеся
     от сужения колонки 10px отданы левому полю — ширина контента та же,
     но блок встаёт по центру. На планшете и десктопе поля симметричны. */
  --page-pad-l: 30px;

  /* ── Магистраль (фишка) ── */
  --rail-col:   30px;   /* ширина колонки под стояк */
  /* 3px, а не 2: на телефоне двухпиксельная линия слишком тонкая,
     и движение заливки по ней глазом не читается. На планшете
     и десктопе возвращается к 2px (ниже по файлу). */
  --rail-w:     3px;
  /* --node задаёт ОСЬ стояка (линия проходит по --node/2) и остаётся
     прежним. --node-box — размер рисунка узла: он больше оси, потому
     что у вентиля есть маховик, а у тройника отвод, и им нужно место.
     Узел центрируется на оси, поэтому геометрия линии не меняется. */
  --node:       11px;
  --node-box:   22px;
  /* Сдвиг стояка в САЙДБАРЕ вправо от колонки меню. Стояк на самой
     странице стоит вплотную к левому краю блока — так и задумано. */
  --srail-x:    0px;

  /* ── Масштаб всего макета ──
     В проекте нет ни одного rem: все размеры заданы в px, поэтому
     «уменьшить сайт» через font-size на html невозможно — его никто
     не потребляет. Один zoom на body (02-base.css) отдаляет макет
     целиком и сохраняет пропорции; масштаб меняется здесь и только здесь.
     1 — исходный вид, .9 — на 10% мельче.

     Оговорка: величины, привязанные к ФИЗИЧЕСКОМУ экрану, zoom сжимает
     тоже — а они не должны сжиматься. Это высоты от вьюпорта (dvh),
     env(safe-area-*) и тап-таргеты; все они делятся на --zoom обратно.
     Ищите «/ var(--zoom)» — других мест нет. */
  /* .79 на мобилке: весь макет мельче на 7%. Десктоп остаётся на .85
     (возврат ниже по файлу) — там уменьшать не просили. */
  --zoom: .79;

  /* ── Геометрия ── */
  --r-sm:  10px;
  --r-md:  16px;
  --r-lg:  22px;
  --r-xl:  28px;
  --r-pill: 999px;

  /* ── Тени: оптические, синие ── */
  --sh-1: 0 1px 2px rgba(8, 21, 46, .05);
  --sh-3: 0 10px 30px rgba(10, 102, 194, .10);
  --sh-modal: 0 -20px 60px rgba(6, 24, 48, .3);

  /* ── Минимальный размер тап-таргета (требование Яндекс.Вебмастера) ── */
  --tap: calc(48px / var(--zoom));

  /* ── Движение ── */
  --e-out:    cubic-bezier(.22, 1, .36, 1);
  --e-spring: cubic-bezier(.34, 1.4, .64, 1);
  /* Длительности отдельно от готовых значений transition: --t-slow
     содержит ещё и функцию плавности, поэтому как задержка не годится. */
  --d-fast:   140ms;
  --d:        260ms;
  --d-slow:   420ms;

  --t-fast:   var(--d-fast) var(--e-out);
  --t:        var(--d) var(--e-out);
  --t-slow:   var(--d-slow) var(--e-out);

  /* Высота липкой шапки. Была магическим числом в двух местах —
     .appbar и scroll-margin-top у якорей; при рассинхроне прокрутка
     останавливалась не там. Поднята на 10%: 62 → 68. */
  --appbar-h: 80px;

  /* ── Слои ── */
  --z-header:  60;
  --z-dock:    80;
  --z-modal:   120;
}

/* Тёмной темы у сайта нет намеренно: color-scheme: light выше не даёт
   браузеру самому перекрашивать поля и элементы управления. */

/* Планшет */
@media (min-width: 768px) {
  :root {
    --page-pad: 48px;
    --rail-col: 56px;
    --rail-w: 2px;
    /* Мобильные значения выше касаются только телефона */
    --page-pad-l: var(--page-pad);
    --air: .75;
    --zoom: .85;
    --appbar-h: 68px;
    --block-gap: calc(var(--sp-6) * var(--air));
  }
}

/* Десктоп */
@media (min-width: 1100px) {
  :root {
    /* Почти полный размер. Десктоп наследовал .85 от планшета и всё
       выглядело мелко; ровная единица оказалась чуть крупной — полтора
       процента возвращают запас по высоте, глазу неразличимо. */
    --zoom: .985;
    --page-pad: 68px;
    --rail-col: 64px;
    --block-gap: calc(60px * var(--air));
    --node: 13px;
    --node-box: 26px;
    /* Сайдбарный стояк отодвинут от края колонки меню */
    --srail-x: 14px;
  }
}

/* ══════ 02-base.css ══════ */
/* ============================================================
   02-BASE — сброс, типографика, примитивы
   ============================================================ */

*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }

/* Фон под всей страницей, а не только под body: область перепрокрутки
   и полоса под safe-area на айфоне брали голый белый, из-за чего низ
   в Safari выглядел провалом. Совпадает с theme-color в head.html. */
html { background: var(--paper-2); }

html {
  font-family: var(--font);
  font-size: 16px;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-size-adjust: 100%;
  /* scroll-behavior: smooth здесь намеренно НЕТ.
     Глобальный плавный скролл ломает переход по якорю: пока идёт
     анимация, браузер уже применил scroll-margin-top к промежуточной
     позиции, и на медленных Android раздел останавливается под шапкой.
     Плавность включает JS точечно (NavSpy), где она уместна. */
}

body {
  background: var(--paper);
  color: var(--ink-700);
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  letter-spacing: var(--tr-body);
  /* 100dvh под zoom даёт 90% экрана, а не весь: dvh считается от реального
     вьюпорта, и только потом всё уменьшается. Делим обратно. */
  min-height: calc(100dvh / var(--zoom));

  /* Масштаб всего макета — ручка в 01-tokens.css. На body, а не на .shell:
     шапка, док и модалки — прямые дети body и должны масштабироваться тоже. */
  zoom: var(--zoom);
}

/* Блокировка прокрутки под модалкой/меню. overflow-x: hidden на body
   намеренно не ставим: он маскирует горизонтальное переполнение,
   которое Яндекс.Вебмастер всё равно найдёт. */
body.is-locked { overflow: hidden; }

img, svg, video { display: block; max-width: 100%; }
svg { flex-shrink: 0; }
ul, ol { list-style: none; }
a { color: inherit; text-decoration: none; }
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; text-align: left; }

::selection { background: var(--brand-600); color: #fff; }
/* На синих поверхностях синее выделение сливалось с фоном */
.zone ::selection, .dock ::selection { background: #fff; color: var(--brand-800); }

/* border-radius здесь НЕ задаём: контур сам повторяет скругление
   элемента. Раньше жёсткие 10px превращали фокус на круглой кнопке
   в прямоугольник. */
:focus-visible {
  outline: 2px solid var(--accent-soft);
  outline-offset: 3px;
}

/* ── Заголовки ─────────────────────────────────────────── */
h1, h2, h3, h4 {
  color: var(--ink-900);
  font-weight: 700;
  text-wrap: balance;
}

h1 {
  font-size: var(--fs-h1);
  line-height: var(--lh-tight);
  letter-spacing: var(--tr-display);
  font-weight: 800;
}

h2 {
  font-size: var(--fs-h2);
  line-height: 1.18;
  letter-spacing: var(--tr-head);
  font-weight: 700;
}

h3, .t-h3 {
  font-size: var(--fs-h3);
  line-height: 1.3;
  letter-spacing: var(--tr-head);
  font-weight: 650;
}

.t-lead {
  font-size: var(--fs-lead);
  line-height: 1.5;
  letter-spacing: -0.018em;
  color: var(--ink-500);
  font-weight: 420;
  text-wrap: pretty;
}

p { text-wrap: pretty; }

/* ── Надзаголовок / капслок ────────────────────────────── */
.t-eyebrow {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  font-size: var(--fs-micro);
  font-weight: 650;
  letter-spacing: var(--tr-caps);
  text-transform: uppercase;
  color: var(--accent);
}

.t-eyebrow::before {
  content: '';
  width: 18px;
  height: 2px;
  border-radius: 2px;
  background: currentColor;
  opacity: .5;
}

.t-num   { font-variant-numeric: tabular-nums; font-feature-settings: 'tnum' 1; }
.nowrap  { white-space: nowrap; }

/* Мелкая капслок-подпись: контакты, метаданные модалок */
.label {
  display: block;
  font-size: var(--fs-micro);
  font-weight: 650;
  letter-spacing: var(--tr-caps);
  text-transform: uppercase;
  color: var(--ink-300);
}

/* ── Утилиты ───────────────────────────────────────────── */
.sr-only {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* Ссылка «к содержимому» — первое, что услышит скринридер */
.skip-link {
  position: absolute;
  left: var(--sp-4);
  top: -100px;
  z-index: 200;
  padding: 12px 20px;
  border-radius: var(--r-pill);
  background: var(--brand-600);
  color: #fff;
  font-weight: 650;
  transition: top var(--t-fast);
}
.skip-link:focus { top: var(--sp-4); }

/* ── Кнопки ────────────────────────────────────────────── */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  min-height: 52px;
  padding: 0 22px;
  border-radius: var(--r-pill);
  font-size: var(--fs-sm);
  font-weight: 650;
  letter-spacing: -0.01em;
  white-space: nowrap;
  transition: transform var(--t-fast), box-shadow var(--t), background var(--t);
}
.btn:active { transform: scale(.975); }

.btn--primary {
  position: relative;
  overflow: hidden;
  background: linear-gradient(180deg, var(--brand-500), var(--brand-700));
  color: #fff;
  box-shadow: var(--sh-3);
}
.btn--primary:hover { box-shadow: 0 14px 34px rgba(10,102,194,.28); }

/* Блик: по кнопке один раз проходит светлая полоса.
   Только у главной кнопки — на светлой .btn--ghost белая полоса не
   читается, а на каждой кнопке подряд превратилась бы в мигание.
   Полоса лежит за подписью и не перехватывает клик. */
.btn--primary::after {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  left: -60%;
  width: 45%;
  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, .35), transparent);
  transform: skewX(-18deg);
  pointer-events: none;
}
.btn--primary:hover::after { animation: btn-shine 700ms var(--e-out); }
@keyframes btn-shine { to { left: 120%; } }

/* Глобальное правило prefers-reduced-motion гасит анимацию, но полоса
   осталась бы стоять в кадре — здесь убираем её совсем */
@media (prefers-reduced-motion: reduce) {
  .btn--primary::after { display: none; }
}

.btn--ghost {
  background: var(--tint);
  color: var(--accent-strong);
  box-shadow: inset 0 0 0 1px var(--line-brand);
}
.btn--ghost:hover { background: var(--tint-2); }

/* backdrop-filter поддерживают не все WebView; там полупрозрачный фон
   без размытия давал нечитаемую подпись, поэтому база — плотная */
.btn--onDark {
  background: rgba(255,255,255,.22);
  color: #fff;
  box-shadow: inset 0 0 0 1px rgba(255,255,255,.24);
}
.btn--onDark:hover { background: rgba(255,255,255,.3); }

@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .btn--onDark {
    background: rgba(255,255,255,.12);
    -webkit-backdrop-filter: blur(8px);
    backdrop-filter: blur(8px);
  }
  .btn--onDark:hover { background: rgba(255,255,255,.2); }
}

.btn--wide { width: 100%; }
.btn[disabled] { opacity: .35; pointer-events: none; }

/* ── Строчная ссылка с иконкой ─────────────────────────
   Второстепенное действие рядом с кнопкой. Кнопкой не делаем:
   когда «главных» действий три подряд, главного нет ни одного.
   Высота всё равно 44px — палец должен попадать. */
.tlink {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  min-height: calc(44px / var(--zoom));
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--ink-500);
  transition: color var(--t-fast);
}
.tlink svg { color: var(--accent); transition: transform var(--t-fast); }

/* .tlink рассчитан на самостоятельную строку-ссылку: жёсткие 14px и 44px
   высоты под палец. Внутри лида это подводит дважды — ссылка выходит мельче
   окружающего текста (лид идёт на 16–20px), и цвет у неё тот же --ink-500,
   что у самого абзаца, то есть ссылкой она не выглядит вовсе. Внутри текста
   ведём себя как .prose a: наследуем кегль, красимся акцентом, подчёркиваемся. */
.t-lead .tlink {
  display: inline;
  min-height: 0;
  font-size: inherit;
  color: var(--accent-strong);
  text-decoration: underline;
  text-underline-offset: 2px;
}
.tlink:hover { color: var(--accent-strong); }
.tlink:hover svg { transform: translateX(2px); }

/* ── Плитка с иконкой: карточки услуг и строки контактов ─ */
.icon-tile {
  display: grid;
  place-items: center;
  flex-shrink: 0;
  width: 44px;
  height: 44px;
  border-radius: 13px;
  background: var(--tint);
  color: var(--accent);
  box-shadow: inset 0 0 0 1px var(--line-brand);
}

/* Знак MAX лежит растром, но его альфа-канал УЖЕ является формой знака:
   все непрозрачные пиксели одного цвета (белые). Поэтому берём картинку
   маской и красим currentColor — цвет совпадает с соседними иконками точно.
   Прежний способ, цепочка filter из brightness/invert/hue-rotate, попадал
   лишь примерно, отсюда и была заметна разница.
   Маску нельзя навесить на сам <img>: его собственные пиксели рисуются
   поверх заливки — поэтому в разметке пустой span. */
.row__icon--max::before {
  content: '';
  width: 18px;
  height: 18px;
  background: currentColor;
  -webkit-mask: url("/assets/images/max-icon.png") no-repeat center / contain;
          mask: url("/assets/images/max-icon.png") no-repeat center / contain;
}


/* ── Живой статус офиса ────────────────────────────────── */
.hours {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: var(--fs-xs);
  color: var(--ink-400);
}
.hours::before {
  content: '';
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--ink-200);
  flex-shrink: 0;
}
.hours[data-hours-state="open"]  { color: var(--ok-700); }
.hours[data-hours-state="open"]::before  { background: var(--ok-600); }
.hours[data-hours-state="break"] { color: var(--warn-700); }
.hours[data-hours-state="break"]::before { background: var(--warn-500); }
.hours[data-hours-state="closed"]::before { background: var(--ink-300); }

.hours--lg { font-size: var(--fs-sm); margin-bottom: var(--sp-3); }

/* ── Скелетон: заглушка на время загрузки ──────────────── */
.skeleton {
  position: relative;
  overflow: hidden;
  border-radius: var(--r-md);
  background: var(--paper-2);
}
.skeleton::after {
  content: '';
  position: absolute;
  inset: 0;
  transform: translateX(-100%);
  background: linear-gradient(90deg, transparent, var(--skeleton-sweep), transparent);
  animation: skeleton-sweep 1.4s var(--e-out) infinite;
}
@keyframes skeleton-sweep { to { transform: translateX(100%); } }

@media (prefers-reduced-motion: reduce) {
  .skeleton::after { animation: none; }
}

/* ── Оценка на картах ──────────────────────────────────── */
.ratings { display: flex; flex-wrap: wrap; gap: var(--sp-3); }
.rating {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: var(--fs-sm);
  color: var(--ink-500);
}
.rating svg { width: 14px; height: 14px; color: var(--star); }
.rating b { color: var(--ink-900); font-weight: 700; }

/* ── Доступность: уважаем reduced-motion ───────────────── */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ══════ 03-layout.css ══════ */
/* ============================================================
   03-LAYOUT — оболочка, шапка, вертикальное меню, МАГИСТРАЛЬ
   ============================================================ */

/* ─────────────────────────────────────────────
   SHELL
   ───────────────────────────────────────────── */
.shell {
  width: 100%;
  /* Контент фиксированной ширины (1700px) — блоки и карточки не
     растягиваются; по центру, пустые поля симметрично по бокам. */
  max-width: 1700px;
  margin-inline: auto;
}

.page {
  padding-inline: var(--page-pad-l) var(--page-pad);
  padding-bottom: calc(var(--sp-7) + 80px); /* место под мобильный док */
}

@media (min-width: 1100px) {
  /* Было --sp-10 (112px): под копирайтом зияла большая пустота. Футер
     несёт свой отступ, поэтому странице снизу достаточно немного. */
  .page { padding-bottom: var(--sp-4); }
}

/* ─────────────────────────────────────────────
   APPBAR — компактная шапка (мобиль/планшет)
   ───────────────────────────────────────────── */
.appbar {
  position: sticky;
  top: 0;
  z-index: var(--z-header);
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  height: var(--appbar-h);
  padding-inline: var(--page-pad);
  /* База — непрозрачная. В WebView без backdrop-filter полупрозрачная
     шапка пропускала контент насквозь и текст наезжал на текст. */
  background: var(--paper);
  border-bottom: 1px solid var(--line);
}

@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .appbar {
    background: color-mix(in srgb, var(--paper) 82%, transparent);
    -webkit-backdrop-filter: saturate(180%) blur(18px);
    backdrop-filter: saturate(180%) blur(18px);
  }
}

/* Логотип — один набор правил и для шапки, и для сайдбара */
.brand {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
}

.brand__mark {
  border-radius: 9px;
  object-fit: contain;
  background: var(--paper-2);
  box-shadow: inset 0 0 0 1px var(--line);
}

.brand__name {
  font-size: 16px;
  font-weight: 750;
  letter-spacing: -0.03em;
  color: var(--ink-900);
  line-height: 1.1;
}

.brand__city {
  display: block;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: var(--tr-caps);
  text-transform: uppercase;
  color: var(--ink-300);
}

/* min-height — требование Яндекс.Вебмастера к размеру тап-таргета */
.appbar__brand { min-height: var(--tap); }
.appbar__mark { width: 43px; height: 43px; }
/* Название и знак в шапке крупнее общей .brand-базы: на мобилке это
   единственный текст в шапке, и он не должен читаться мельче навигации. */
.appbar .brand__name { font-size: 18px; }
.appbar .brand__city { font-size: 11px; }

/* Пилюли разделов в шапке — активную ведёт скроллспай.
   Визуально это тот же язык, что у .doc. */
.appbar__nav {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: var(--sp-3);
}

.navchip {
  position: relative;
  display: inline-flex;
  align-items: center;
  min-height: calc(44px / var(--zoom));              /* тап-таргет, требование Яндекс.Вебмастера */
  padding: 0 2px;
  font-size: 14px;
  font-weight: 600;
  white-space: nowrap;
  color: var(--ink-500);
  transition: color var(--t-fast);
}

/* Подчёркивание рисуем псевдоэлементом, а не border-bottom: так активный
   раздел не смещает базовую линию текста относительно соседей */
.navchip::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 11px;
  height: 2px;
  border-radius: 2px;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--t-fast);
}

.navchip:hover { color: var(--accent-strong); }
.navchip[aria-current="page"] { color: var(--accent-strong); font-weight: 700; }
.navchip[aria-current="page"]::after { transform: scaleX(1); }

/* Короткая метка живёт только на узких экранах */
.navchip__short { display: none; }

/* Название компании и три метки целиком на узкий экран не помещаются —
   сокращаем метки, знак и название остаются */
@media (max-width: 400px) {
  .appbar { gap: var(--sp-2); padding-inline: 14px; }
  .appbar .brand { flex: 0 1 auto; overflow: hidden; }
  .appbar .brand__name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .appbar__nav { gap: 10px; }
  .navchip { font-size: 14.5px; }
  .navchip__full { display: none; }
  .navchip__short { display: inline; }
}

/* Ниже ~344px даже короткие метки рядом с названием не помещаются —
   переносим навигацию на вторую строку, чтобы ничего не обрезалось */
@media (max-width: 344px) {
  .appbar {
    flex-wrap: wrap;
    height: auto;
    padding-block: 8px 0;
  }
  .appbar__brand { min-height: 40px; }
  .appbar__nav {
    flex-basis: 100%;
    margin-left: 0;
    justify-content: space-between;
  }
  .navchip { min-height: 40px; }
}

@media (min-width: 1100px) {
  .appbar { display: none; }
}

/* Вертикальный список — используется только сайдбаром (десктоп).
   Мобильное меню-шторка удалено, его тёмные стили вместе с ним. */
.vnav { display: flex; flex-direction: column; }

.vnav__item {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: var(--sp-3);
  width: 100%;
}

/* ─────────────────────────────────────────────
   SIDEBAR — вертикальное меню (десктоп)
   ───────────────────────────────────────────── */
.layout { display: block; }

.sidebar { display: none; }

@media (min-width: 1100px) {
  /* Сайдбар крупнее остальной страницы на десятую часть. Множитель один
     на всё: и ширина колонки, и содержимое внутри. Через zoom, а не через
     правку десятка размеров шрифтов и логотипа по отдельности — этим же
     приёмом в проекте задана вся плотность вёрстки (--zoom в 01-tokens). */
  :root { --sb-zoom: 1.1; }

  .layout {
    display: grid;
    grid-template-columns: calc(300px * var(--sb-zoom)) minmax(0, 1fr);
    gap: var(--sp-8);
    align-items: start;
    padding-inline: var(--page-pad);
  }

  .page { padding-inline: 0; }

  /* Колонка занимала 795 px из 910 — почти весь экран, и на ноутбуках
     1280×720 не помещалась вовсе. Плотность подтянута везде, кроме
     знака: он остаётся крупным. */
  .sidebar {
    display: flex;
    flex-direction: column;
    gap: var(--sp-5);
    position: sticky;
    top: var(--sp-6);
    padding-block: var(--sp-6);
    /* Внутри сайдбара пиксели свои, уменьшенные на --sb-zoom: 330px колонки
       для него самого — это 300. Поэтому padding-inline / margin-inline /
       width: calc(100% + 72px) ниже остаются как были и продолжают работать. */
    zoom: var(--sb-zoom);
    /* На ноутбуках 1280×720 низ колонки иначе уезжает за экран. Делим и на
       свой zoom тоже — иначе колонка считала бы себе на десятую часть
       больше высоты, чем есть на экране. */
    max-height: calc(100dvh / (var(--zoom) * var(--sb-zoom)) - var(--sp-6) * 2);
    overflow-y: auto;
    scrollbar-width: thin;
    scrollbar-color: var(--ink-200) transparent;
    /* overflow-y: auto превращает overflow-x в auto и срезает всё левее
       padding-box. Ромбы и их свечение выходят за колонку, поэтому окно
       обрезки расширяем паддингом с обеих сторон, а отрицательный margin
       возвращает колонку на прежнее место. Справа запаса не было вовсе,
       и тень кнопки «Личный кабинет» срезало по краю контента. */
    /* 36px — по размытию тени кнопки «Личный кабинет»: 30px в покое
       и 34px при наведении. Меньше — и ореол срезает по краю колонки. */
    padding-inline: 36px;
    margin-inline: -36px;
    width: calc(100% + 72px);
  }

  /* Знак стоит у самого края колонки, а меню отодвинуто вправо под стояк.
     Величина выставлена ползунком в dist/tune-lab.html — на глаз такое
     не обсудить. Сначала 4px, потом попросили сдвинуть ещё на столько же. */
  .sidebar__brand { gap: 16px; padding-left: 8px; }
  .sidebar__mark {
    width: 78px; height: 78px; border-radius: 20px;
    transition: transform var(--t) var(--e-spring), box-shadow var(--t);
  }
  .sidebar__brand:hover .sidebar__mark {
    transform: translateY(-2px);
    box-shadow: inset 0 0 0 1px var(--line), 0 6px 16px rgba(10, 102, 194, .16);
  }
  .sidebar__brand .brand__name {
    font-size: 26px; font-weight: 780; letter-spacing: -0.035em; line-height: 1.05;
  }
  .sidebar__brand .brand__city { font-size: 14px; margin-top: 3px; }

  /* ── Магистраль сайдбара ──────────────────────────────
     Разделы и контакты сидят на одном стояке. Он отодвинут от края
     колонки на --srail-x и заполняется синхронно со стояком страницы:
     уровень заливки ведёт тот же модуль Spine и кладёт его в --srail-p.
     Отступ текста растёт вместе со сдвигом, поэтому расстояние от оси
     до подписей остаётся прежним. */
  .sidebar__body {
    position: relative;
    padding-left: calc(26px + var(--srail-x));
    display: flex;
    flex-direction: column;
    gap: var(--sp-5);
  }

  /* Труба и заливка — одна геометрия на два слоя.
     Линия начинается выше первого узла и уходит немного ниже последнего,
     в промежуток перед кнопками: короткий стояк выглядел обрубком.
     bottom должен совпадать с SRAIL_BOTTOM в spine.js — было -22px, но
     после починки зазора «меню → телефон» .sidebar__body стал короче
     на ту же величину, и та же труба повисала в пустоте на 19 физических
     px ниже последней строки текста, почти утыкаясь в разделитель перед
     кнопкой. */
  .sidebar__body::before,
  .sidebar__body::after {
    content: '';
    position: absolute;
    left: calc(var(--srail-x) + var(--node) / 2);
    transform: translateX(-50%);
    top: -10px;
    bottom: -8px;
    width: var(--rail-w);
    border-radius: var(--rail-w);
  }

  /* Непройденная часть */
  .sidebar__body::before {
    background: linear-gradient(180deg,
      transparent,
      var(--line-brand-2) 30px,
      var(--line-brand) 60%,
      transparent);
  }

  /* Пройденная часть — та же механика, что у .block__spine::after */
  .sidebar__body::after {
    background: linear-gradient(180deg, transparent, var(--accent) 30px, var(--accent-soft));
    box-shadow: 0 0 10px var(--glow);
    transform: translateX(-50%) scaleY(var(--srail-p, 0));
    transform-origin: top;
    will-change: transform;
  }

  @media (prefers-reduced-motion: reduce) {
    .sidebar__body::after { box-shadow: none; }
  }

  /* Пункт получает собственную поверхность — строка кликабельна целиком */
  /* 44px вместо 54: пять пунктов занимали 270 px — больше трети
     колонки. min-height держит тап-таргет. */
  .sidebar .vnav__item {
    grid-template-columns: 1fr auto;
    min-height: calc(44px / var(--zoom));
    padding: 10px 12px;
    margin-inline: -12px;
    border: 0;
    border-radius: var(--r-sm);
    position: relative;
    transition: background var(--t-fast);
  }
  .sidebar .vnav__item:hover { background: var(--surface-hover); }
  .sidebar .vnav__item:focus-visible { outline-offset: 2px; }

  /* Узел пункта меню — те же фигуры, что у блоков страницы.
     14px в left гасит margin-inline:-12px пункта и 26px паддинг тела,
     чтобы узел сел ровно на ось трубы. */
  .sidebar .vnav__node {
    position: absolute;
    left: calc(var(--node) / 2 - 14px - var(--node-box) / 2);
    top: 50%;
    width: var(--node-box); height: var(--node-box);
    margin-top: calc(var(--node-box) / -2);
  }

  .sidebar .vnav__label {
    font-size: 18px;
    font-weight: 620;
    line-height: 1.25;
    letter-spacing: -0.02em;
    color: var(--ink-500);
    transition: color var(--t-fast);
  }
  .sidebar .vnav__item:hover .vnav__label { color: var(--ink-900); }
  .sidebar .vnav__item:hover .vnav__node::before { inset: 10%; }

  /* Узел, который уровень заливки уже прошёл */
  .sidebar .vnav__item.is-lit .vnav__node { --node-fill: var(--accent); }
  .sidebar .vnav__item.is-lit .vnav__node::before { inset: 8%; }
  .sidebar .vnav__item.is-lit .vnav__label { color: var(--ink-900); }

  /* Текущий раздел — залитый узел, как .block__node--filled */
  .sidebar .vnav__item[aria-current="page"] {
    padding-left: 12px;
    background: var(--surface-hover);
  }
  .sidebar .vnav__item[aria-current="page"] .vnav__label { color: var(--accent-strong); font-weight: 720; }
  .sidebar .vnav__item[aria-current="page"] .vnav__node { --node-fill: var(--accent); }
  .sidebar .vnav__item[aria-current="page"] .vnav__node::before { inset: 4%; }

  /* Отвод от стояка к подписи — тот же приём, что у блоков страницы,
     но показываем его только у текущего пункта: у всех сразу колонка
     превращалась бы в гребёнку. */
  .sidebar .vnav__item[aria-current="page"],
  .sidebar .vnav__item.is-lit {
    background-image: linear-gradient(90deg, var(--line-brand-2), transparent);
    background-repeat: no-repeat;
    background-position: calc(var(--node) / 2 - 14px) center;
    background-size: 22px var(--rail-w);
  }

  /* Контакты — последний узел на стояке, под разделами */
  .sidebar__contacts {
    /* Обе величины — переменные не для красоты: узел ниже центрируется по
       строке телефона. Когда padding-top правили ради зазора «меню →
       телефон», а константу узла (20px) не трогали, ромб уехал выше строки
       на 7.5px. Теперь изменение зазора двигает узел вместе с телефоном. */
    --contacts-pad: 15px;
    --phone-line: 25px;   /* 20px кегль × 1.25 line-height */
    position: relative;
    display: flex;
    flex-direction: column;
    gap: 2px;
    /* Эталон — зазор МЕЖДУ ПУНКТАМИ МЕНЮ (25px физических), а не между
       логотипом и меню: пункты меню стоят вплотную (.vnav без gap), весь
       их видимый интервал — это по 10px верхнего/нижнего padding у
       каждого .vnav__item. Телефон живёт в соседнем flex-контейнере
       (.sidebar__body), у которого свой gap --sp-5 (24px) между .vnav
       и .sidebar__contacts — именно этот ЛИШНИЙ групповой отступ (которого
       между обычными пунктами нет вообще) и раздувал интервал до 47px.
       Гасим его отрицательным margin-top и добавляем ровно тот же
       padding-top, что у самого пункта меню (10px) — тогда переход
       к телефону визуально неотличим от перехода между двумя пунктами. */
    margin-top: calc(-1 * var(--sp-5));
    padding-top: var(--contacts-pad);
  }
  /* Телефон — тоже станция магистрали. Вентиль: аварийную линию
     «открывают», когда нужно.
     Центр узла = центр строки телефона: padding контактов + половина
     высоты строки. Раньше здесь стояло 20px — до появления padding-top
     это совпадало, после него ромб оказался на 7.5px выше строки. */
  .sidebar__node {
    position: absolute;
    left: calc(var(--node) / 2 - 26px - var(--node-box) / 2);
    top: calc(var(--contacts-pad) + var(--phone-line) / 2 - var(--node-box) / 2);
    width: var(--node-box); height: var(--node-box);
  }
  /* Заливка проходит мимо этого узла до самого низа трубы, но сам он не
     зажигается: телефон — не место в документе, отмечать им прочитанное
     неверно. Горящий глиф означает «этот раздел пройден», а разделом
     телефон не является (см. collectRail в spine.js). */
  .sidebar__phone {
    font-size: 20px;
    font-weight: 700;
    letter-spacing: -0.025em;
    /* Раньше не задан вовсе → браузерное «normal», у которого half-leading
       не совпадает с .vnav__label (18px/1.25). Из-за этого расхождения
       фиксированный padding-top у .sidebar__contacts ниже подгонялся «на
       глаз» под один замер и не попадал в цель на других разрешениях.
       Тот же коэффициент, что у пункта меню, — оба текста теперь стоят
       на одной типографической сетке. */
    line-height: 1.25;
    color: var(--accent);
    transition: color var(--t-fast);
  }
  .sidebar__phone:hover { color: var(--accent-strong); }
  /* 14px, а не --fs-xs: под зумом двенадцать пикселей давали на экране
     десять с небольшим — расписание читалось с трудом. */
  .sidebar__status { margin-top: 3px; font-size: 14px; font-weight: 600; }
  .sidebar__hours {
    margin-top: 4px;
    font-size: 14px;
    color: var(--ink-400);
    line-height: 1.55;
  }
  .sidebar__hours b { color: var(--ink-700); font-weight: 650; }

  /* Действия — отбиты хайрлайном, выровнены по колонке текста */
  .sidebar__actions {
    display: flex;
    flex-direction: column;
    gap: var(--sp-1);
    padding-left: calc(26px + var(--srail-x));
    padding-top: var(--sp-4);
    border-top: 1px solid var(--line-2);
  }
  .sidebar__actions .btn { font-size: 16px; }

  /* Ссылка центрируется по той же оси, что и содержимое кнопки: обе строки
     стоят посередине синей плашки, а не жмутся к её левому краю. */
  .sidebar__links { display: flex; flex-direction: column; align-items: center; }
  /* Оптическая подгонка. По боксам значок и текст уже отцентрованы (.tlink
     держит align-items: center), расхождение меньше пикселя. Но глаз меряет
     не бокс, а видимую массу: у строчной кириллицы она лежит между базовой
     линией и высотой строчных, то есть выше середины строки, — и значок,
     отцентрованный по всей высоте, кажется опущенным.

     transform, а не margin: раскладку не трогает, высота ссылки и её
     тап-таргет остаются прежними. Правило скоплено на сайдбар — тот же
     .tlink стоит в лидах и в пагинации, где кегль другой и сдвиг не нужен.

     Величина подобрана глазом, а не формулой. Без сдвига значок свисает
     на восемь пикселей ниже базовой линии (у речевого облачка внизу хвост)
     и читается опущенным. Ровное совмещение с серединой заглавных — это
     четыре пикселя — наоборот задирает его над строкой: значок вдвое выше
     текста, и при точном центрировании он выпирает сверху. Два — середина
     между этими крайностями. */
  .sidebar__links .tlink svg { transform: translateY(-2px); }
}

/* Якоря разделов — прокрутка должна останавливаться под липкой шапкой */
[data-spy] { scroll-margin-top: calc(var(--appbar-h) + var(--sp-4)); }

@media (min-width: 1100px) {
  /* На десктопе липкой шапки нет, и упирать заголовок раздела в самый верх
     незачем: клик по пункту меню должен уводить чуть глубже, к содержимому,
     а не ставить экран ровно на вступление. Отрицательный отступ и значит
     «прокрутить дальше»: верх раздела уходит выше кромки на эту величину.

     Больше -80px брать нельзя, не тронув скроллспай: он считает раздел
     текущим, пока верх в пределах 80px от кромки (nav-spy.js, headerHeight
     на десктопе даёт 0). Уедем дальше — подсветка перескочит на следующий
     раздел сразу после клика. */
  [data-spy] { scroll-margin-top: -40px; }
}

/* ─────────────────────────────────────────────
   МАГИСТРАЛЬ «СТОЯК» — фирменный элемент
   Каждый блок подключён к вертикальной линии узлом.
   Стояк «заполняется» по мере прокрутки: пройденная часть — сплошная
   синяя, непройденная — бледная. Прогресс считает модуль Spine и кладёт
   его в --rail-p (0…1) на каждом блоке.
   ───────────────────────────────────────────── */
.block {
  display: grid;
  grid-template-columns: var(--rail-col) minmax(0, 1fr);
  position: relative;
}

.block + .block { margin-top: var(--block-gap); }

/* Начало смыслового раздела («Услуги», «Контакты»).
   Расстояние между соседними блоками и расстояние между разделами
   было одинаковым — разделы слипались в одну ленту. Здесь пауза
   заметно больше, а по её середине проходит хайрлайн: глаз видит,
   что предыдущая тема закончилась. */
/* Пауза вынесена в переменную: её же половину берёт хайрлайн ниже, и без
   общего значения они разъезжались. На узком экране 92px съедали почти
   треть экрана — там пауза меньше, но всё равно втрое больше межблочной. */
.block + .block--sectionStart {
  --section-gap: calc(var(--block-gap) + var(--sp-5) * var(--air));
  margin-top: var(--section-gap);
  position: relative;
}
@media (min-width: 1100px) {
  .block + .block--sectionStart { --section-gap: calc(var(--block-gap) + var(--sp-6) * var(--air)); }
}
.block + .block--sectionStart::before {
  content: '';
  position: absolute;
  top: calc(-1 * var(--section-gap) / 2);
  left: 0;
  right: 0;
  height: 1px;
  background: linear-gradient(90deg, var(--line-2), var(--line) 40%, transparent);
}

/* Колонка магистрали.
   Отвод к заголовку рисуем фоном самой колонки, а не третьим
   псевдоэлементом: у .block__spine оба уже заняты трубой и заливкой.
   Горизонтальная линия идёт от оси стояка вправо, на высоте центра
   узла, и растворяется, не доходя до текста, — связь узла с блоком
   становится видимой, но остаётся тихой. */
.block__spine {
  position: relative;
  grid-column: 1;
  background-image: linear-gradient(90deg, var(--line-brand-2), transparent);
  background-repeat: no-repeat;
  background-position: calc(var(--node) / 2) calc(9px + var(--node) / 2 - var(--rail-w) / 2);
  background-size: calc(100% - var(--node) / 2 - var(--sp-2)) var(--rail-w);
}

/* У блока без магистрали колонки нет — отвод рисовать не на чем */
.block--bleed .block__spine { background-image: none; }

/* Сама линия — уходит в промежуток до следующего блока */
.block__spine::before,
.block__spine::after {
  content: '';
  position: absolute;
  left: calc(var(--node) / 2);
  transform: translateX(-50%);
  top: 0;
  bottom: calc(-1 * var(--block-gap));
  width: var(--rail-w);
  border-radius: var(--rail-w);
}

/* Непройденная часть — бледная «труба» */
.block__spine::before {
  background: linear-gradient(180deg,
    var(--line-brand-2),
    var(--line-brand) 60%,
    var(--line-brand));
}

/* Пройденная часть — заливка сверху вниз.
   scaleY по --rail-p: одно свойство, анимируется на композиторе,
   пересчёт при скролле не трогает раскладку. */
.block__spine::after {
  background: linear-gradient(180deg, var(--accent), var(--accent-soft));
  box-shadow: 0 0 10px var(--glow);
  transform: translateX(-50%) scaleY(var(--rail-p, 0));
  transform-origin: top;
  /* без JS (--rail-p не задан) заливки просто нет — стояк выглядит
     ровно так же, как до появления модуля */
  will-change: transform;
}

/* Начало магистрали — линия проявляется плавно */
.block--railStart .block__spine::before {
  background: linear-gradient(180deg,
    transparent,
    var(--line-brand-2) 24px,
    var(--line-brand));
}

/* Конец магистрали. Бледная труба растворяется — ей продолжаться некуда.
   Заливка так не делает: растворённый хвост читался как «не долилось»,
   хотя --rail-p давно единица. Дочитанный блок кончается ровным торцом,
   скруглённым по --rail-w, — заливка догнала конец трубы и встала. */
.block--railEnd .block__spine::before,
.block--railEnd .block__spine::after {
  bottom: 0;
}
.block--railEnd .block__spine::before {
  background: linear-gradient(180deg, var(--line-brand), transparent);
}

/* ── УЗЛЫ ──────────────────────────────────────────────
   Форма узла говорит, что за блок к нему подключён: манометр —
   показатели, тройник — ветвление, гребёнка — список, заглушка —
   конец магистрали. Раньше везде стоял один ромб.

   Рисунок узла шире оси (--node-box против --node), поэтому маховик
   вентиля и отвод тройника выходят за линию — как у настоящей арматуры.
   Центр при этом остаётся ровно на оси, геометрия стояка не съехала. */
.node {
  display: grid;
  place-items: center;
  color: var(--accent);
  transition: color var(--t), transform var(--t) var(--e-spring);
}

/* Ореол — полупрозрачный, труба сквозь него видна: узел сидит НА
   стояке, а не закрывает его заглушкой */
.node::before {
  content: '';
  position: absolute;
  inset: 22%;
  border-radius: 50%;
  background: var(--glow);
  transition: inset var(--t), background var(--t);
}

.node svg { position: relative; overflow: visible; }
.node .n-body,
.node .n-line {
  stroke: currentColor;
  stroke-width: 2.2;
  stroke-linecap: round;
  stroke-linejoin: round;
  transition: fill var(--t);
}
/* Тело детали закрывает трубу — это и делает узел «врезкой» */
.node .n-body { fill: var(--node-fill, var(--paper)); }
.node .n-line { fill: none; }

/* Глиф, у которого тело и силуэт — одно и то же. У ромба трубу перекрывает
   отдельная деталь (.n-body), а у телефонной трубки такой детали нет:
   работу тела делает её собственный замкнутый контур. Пока узел не горит,
   он залит бумагой и точно так же прячет за собой стояк; на горящем
   заливка становится акцентной.

   По умолчанию здесь стояло none, и труба просматривалась сквозь трубку
   насквозь — особенно заметно с тех пор, как заливка пошла ниже последнего
   узла и по глифу поехала яркая линия.

   Газете этот класс не подходит, хотя труба сквозь неё тоже просвечивала.
   В n-fillable склеены два разных поведения: «перекрыть трубу» и «залиться
   акцентом на пройденном узле». Газете нужно только первое — залитый синим
   лист читается как сплошное пятно, а не как газета, и раздел он показывает
   хуже, чем подсветка строки. Для неё заведён n-mask ниже. */
.node .n-fillable { fill: var(--node-fill, var(--paper)); }

/* То же перекрытие трубы, но без заливки акцентом: --node-fill здесь не
   читается вовсе, лист остаётся бумажным и когда стояк до него доехал.

   Отдельный класс, а не модификатор к n-fillable, потому что это не оттенок
   одного поведения, а второе самостоятельное. Пробовали одним классом —
   получили синий лист на «Новостях», и разделять пришлось всё равно. */
.node .n-mask { fill: var(--paper); }

/* Одной заливки контура мало — нужна и полоска под глифом.
   У трубки контур вогнутый: ось стояка проходит по
   середине глифа, а силуэт закрывает её лишь на пятой части высоты — труба
   идёт сквозь выемку. Ромбу для этого служит тело детали, здесь — полоска
   шириной ровно с трубу. На бумаге её не видно, в отличие от круглой
   подложки n-plate, которую отсюда и убирали.

   z-index: -1 кладёт полоску под сам глиф, но внутри узла — то есть над
   трубой, раз узел ниже получил свой контекст наложения. */
.node:has(.n-fillable)::after,
.node:has(.n-mask)::after {
  content: '';
  position: absolute;
  left: 50%;
  top: 10%;
  bottom: 10%;
  width: calc(var(--rail-w) + 2px);
  transform: translateX(-50%);
  background: var(--paper);
  z-index: -1;
}

/* Облегчённый штрих. Общие 2.2 подобраны под крупные ромбы; у газеты тело
   всего 11×9 юнитов, и на этом весе её строки сливались в сплошную плиту.
   1.9 — ровно тот вес, которым нарисованы отдельно стоящие иконки набора. */
.node .n-thin { stroke-width: 1.9; }

/* Сплошная начинка: ядро, половинка. Цветом узла на светлом теле, а на
   залитом (пройденном) — цветом бумаги, иначе деталь пропадала бы в заливке. */
.node .n-solid {
  fill: var(--node-solid, currentColor);
  stroke: none;
}
.node.is-lit .n-solid,
.node--filled .n-solid { --node-solid: var(--paper); }

/* Штрихи поверх собственной заливки глифа. У ромба такой задачи нет: там
   начинка лежит на теле детали и красится через --node-solid. У газеты
   штрихи — сам рисунок (строки текста, сгиб), и на горящем узле они
   оказывались тёмными на акценте: лист превращался в мазок. Выворачиваем
   их в бумагу тем же приёмом. */
.node.is-lit:has(.n-fillable) .n-line,
.node--filled:has(.n-fillable) .n-line { stroke: var(--paper); }
/* n-mask сюда не попадает намеренно: лист газеты не заливается акцентом,
   и выворачивать его штрихи в бумагу не надо — они просто исчезнут. */

/* Пройденный узел заливается, ореол расходится */
.node.is-lit,
.node--filled {
  --node-fill: var(--accent);
 
}
.node.is-lit::before,
.node--filled::before { inset: 8%; }
.node.is-lit { transform: scale(1.06); }

@media (prefers-reduced-motion: reduce) {
  .node.is-lit { transform: none; }
}

/* Узел обязан краситься НАД трубой. Труба и заливка — псевдоэлементы
   родителя, а ::after у него последний: без z-index он ложится поверх узла и
   кладёт акцентную линию прямо на глиф. Все три узла уже позиционированы,
   поэтому хватает одного свойства. */
.block__node,
.vnav__node,
.sidebar__node { z-index: 1; }

/* Узел на стояке страницы: центр там же, где был у прежнего ромба */
.block__node {
  position: absolute;
  top: calc(9px + var(--node) / 2 - var(--node-box) / 2);
  left: calc(var(--node) / 2 - var(--node-box) / 2);
  width: var(--node-box);
  height: var(--node-box);
}

/* Номер блока — повёрнут вдоль магистрали */
.block__no {
  position: absolute;
  top: 42px;
  left: calc(var(--node) / 2);
  transform: translateX(-50%) rotate(180deg);
  writing-mode: vertical-rl;
  font-size: var(--fs-micro);
  font-weight: 700;
  letter-spacing: var(--tr-caps);
  text-transform: uppercase;
  color: var(--ink-300);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  user-select: none;
  transition: color var(--t);
}

/* Подпись пройденного блока подхватывает акцент — глаз видит,
   где он сейчас находится, не отрываясь от текста */
.block:has(.block__node.is-lit) .block__no { color: var(--accent); }

/* Тело блока */
.block__body {
  grid-column: 2;
  min-width: 0;
  padding-left: var(--sp-1);
}

/* Блок без магистрали (полноширинная зона) */
.block--bleed { grid-template-columns: 1fr; }
.block--bleed .block__body { grid-column: 1; padding-left: 0; }

/* ── МАГИСТРАЛЬ СПРАВА (мобиль) ──────────────────────────
   На узком экране стояк уезжает к правому краю: колонки меняются
   местами, а линия, заливка прогресса, узел и вертикальный номер
   отражаются относительно правого края своей колонки. Ось по-прежнему
   на --node/2, только считается от right, поэтому геометрия узлов
   и «уровень воды» модуля Spine не меняются — он кладёт число
   в --rail-p, а его применяет тот же scaleY. */
@media (max-width: 767px) {
  .block { grid-template-columns: minmax(0, 1fr) var(--rail-col); }
  /* grid-row обязателен обоим. Авторазмещение идёт по колонкам слева направо
     и назад не возвращается: стояк с grid-column: 2 занимал первую строку,
     тело с grid-column: 1 не помещалось за ним и уезжало во вторую. Строка
     стояка оставалась пустой, его высота — нулевой, и модуль Spine выходил
     на проверке len <= 0, ни разу не посчитав заливку. */
  .block__spine { grid-column: 2; grid-row: 1; }
  .block__body  { grid-column: 1; grid-row: 1; padding-left: 0; padding-right: var(--sp-1); }

  .block--bleed { grid-template-columns: 1fr; }
  .block--bleed .block__body { grid-column: 1; padding-right: 0; }

  /* горизонтальный отвод к телу блока — направление и точка отсчёта */
  .block__spine {
    background-image: linear-gradient(270deg, var(--line-brand-2), transparent);
    background-position: right calc(var(--node) / 2) top calc(9px + var(--node) / 2 - var(--rail-w) / 2);
  }

  /* труба и заливка */
  .block__spine::before,
  .block__spine::after {
    left: auto;
    right: calc(var(--node) / 2);
    transform: translateX(50%);
  }
  /* scaleY обязан остаться — иначе прогресс перестанет рисоваться */
  .block__spine::after { transform: translateX(50%) scaleY(var(--rail-p, 0)); }

  .block__node { left: auto; right: calc(var(--node) / 2 - var(--node-box) / 2); }

  .block__no {
    left: auto;
    right: calc(var(--node) / 2);
    transform: translateX(50%) rotate(180deg);
  }
}

/* Заголовок блока */
.block__head { margin-bottom: var(--sp-5); }
.block__head > * + * { margin-top: var(--sp-3); }

@media (prefers-reduced-motion: reduce) {
  .block__spine::after { box-shadow: none; }
  .block__node.is-lit { transform: rotate(45deg); }
}

/* ─────────────────────────────────────────────
   DOCK — постоянная аварийная кнопка (мобиль)
   ───────────────────────────────────────────── */
.dock {
  position: fixed;
  /* Левое поле шире правого по всей странице (--page-pad-l) — плашка
     выравнена по тем же краям, что и текст, а не по симметричным
     полям старой раскладки. */
  left: var(--page-pad-l);
  /* Отступы по бокам одинаковые. Раньше правый был поджат на 40px, чтобы
     space-between не вываливал ~80px пустоты между подписью и кнопкой; но
     платой была плашка, вдвинутая справа на 60px против 30px слева — она
     стояла на 15px левее центра экрана, и перекос было видно глазом.
     Зазор внутри плашки — меньшая беда, чем кривая плашка. */
  right: var(--page-pad-l);
  bottom: calc(22px + env(safe-area-inset-bottom) / var(--zoom));
  z-index: var(--z-dock);
  display: flex;
  align-items: center;
  justify-content: space-between;
  /* Раздутый минимум зазора (44px) искусственно расталкивал текст и кнопку
     по краям плашки, сжимая подпись, — пара читалась как «далеко друг от
     друга». Небольшой зазор оставляет их у своих краёв (space-between), но
     ближе друг к другу. Верх clamp держит зазор от разрастания на крупных
     телефонах. */
  gap: clamp(12px, 4vw, 20px);
  /* Раньше 58px литералом: --tap внутри (48px, делённые на --zoom, чтобы
     физический тап-таргет не сжимался) растёт при уменьшении --zoom,
     а этот литерал — нет. При --zoom .79 кнопка становится физически
     выше самой плашки, та растягивается по кнопке и обхватывает её
     впритык — снаружи не остаётся синей рамки, будто кнопка одна
     занимает всю плашку. Тот же делитель держит зазор постоянным
     при любом --zoom. */
  min-height: calc(58px / var(--zoom));
  /* Справа 16, а не 8: белая кнопка стояла почти впритык к краю плашки —
     6.3px на экране. Двигается только содержимое, сама плашка и её
     выравнивание по полям страницы остаются прежними. */
  padding: 0 16px 0 18px;
  border-radius: var(--r-pill);
  /* Тот же синий, что у .zone в 04-modules.css («Комплекс-А в цифрах»):
     блик сверху справа плюс brand-500 → brand-700. Раньше здесь стояли
     две самые тёмные ступени палитры (700 → 800), и плашка выглядела
     чужой рядом с акцентным блоком главной. Если меняешь цвет зоны —
     меняй и здесь, они должны читаться как одна поверхность. */
  background:
    radial-gradient(120% 90% at 90% 0%, rgba(140, 200, 255, .38), transparent 58%),
    linear-gradient(158deg, var(--brand-500), var(--brand-700) 78%);
  /* Свечение прижато к плашке: со сдвигом 12 и размытием 34 оно уходило на
     46px ниже дока и упиралось в нижнюю панель Safari — на айфоне ореол
     выглядел обрезанным. Теперь он лежит вокруг плашки, а не под ней. */
  box-shadow: 0 4px 18px rgba(10, 102, 194, .34), 0 0 24px rgba(10, 102, 194, .16), inset 0 0 0 1px rgba(255, 255, 255, .14);
  color: #fff;
  /* по умолчанию убран за нижний край: показывает его модуль Dock */
  transform: translateY(140%);
  opacity: 0;
  pointer-events: none;
  transition: transform var(--t), opacity var(--t);
}

.dock.is-shown { transform: none; opacity: 1; pointer-events: auto; }


/* flex:1 распирал подпись на всю свободную ширину и отжимал кнопку к
   правому краю: «Личный кабинет» и «Войти» оказывались по разным концам
   плашки и не читались как одна мысль. Теперь подпись занимает только
   свою ширину, а пара стоит группой по центру (justify-content у .dock). */
.dock__txt { flex: 0 1 auto; min-width: 0; line-height: 1.15; }
/* Плашка шириной 444px, а подпись с кнопкой занимали в ней всего 322 —
   группа висела посередине с пустыми полями по краям, и это читалось как
   «кнопка стоит далеко». Дело было не в зазоре: содержимое просто мелкое
   для такой широкой полосы. Кегли ниже подобраны так, чтобы контент
   заполнял плашку. Подстроить можно ползунками в dist/tune-lab.html. */
.dock__title {
  display: block;
  font-size: 16px;
  font-weight: 650;
  letter-spacing: -0.015em;
}
/* Подзаголовок скрыт всегда, а не по медиазапросу: порог по ширине
   промахивается мимо крупных айфонов (393 попадает, 430 — нет), и
   плашка на них становится двухстрочной. Заголовок и кнопка несут
   всё нужное, поэтому высота дока не должна зависеть от устройства. */
.dock__sub {
  display: block;
  font-size: 12px;
  color: rgba(255, 255, 255, .86);
  letter-spacing: 0.02em;
}

/* Высота была = --tap (48px) — та же плашка ещё и растянута доком на
   58px/--zoom по вертикали, и в сумме белая пилюля выглядела грузной
   рядом с короткой подписью «Войти». 40px визуально легче; настоящий
   тап-таргет не теряется — его держит невидимый ::after ниже, как у
   .metric__note/.req__copy. */
.dock__cta {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  height: 44px;
  padding: 0 22px;
  border-radius: var(--r-pill);
  background: linear-gradient(180deg, #fff, #F3F6FB);
  color: var(--brand-700);
  font-size: 15px;
  font-weight: 700;
  letter-spacing: -0.01em;
  box-shadow: 0 6px 16px rgba(6, 24, 48, .24), inset 0 0 0 1px rgba(10, 102, 194, .08);
  transition: transform var(--t-fast), background var(--t-fast), box-shadow var(--t-fast);
}
.dock__cta::after {
  content: '';
  position: absolute;
  top: 50%; left: 50%;
  width: calc(48px / var(--zoom));
  height: calc(48px / var(--zoom));
  transform: translate(-50%, -50%);
}
.dock__cta:hover {
  background: linear-gradient(180deg, #fff, var(--tint));
  box-shadow: 0 8px 20px rgba(6, 24, 48, .28), inset 0 0 0 1px rgba(10, 102, 194, .14);
}
.dock__cta:active { transform: scale(.96); }

@media (prefers-reduced-motion: reduce) {
  .dock { transition: none; }
}

@media (min-width: 1100px) {
  .dock { display: none; }
}

/* ══════ modules/01-hero.css ══════ */
/* ─────────────────────────────────────────────
   MODULE: hero
   ───────────────────────────────────────────── */
/* Пока сверху стоит липкая шапка (до 1100px), она сама отделяет первый экран —
   прежние 44px поверх её 62px давали полсотни пикселей пустоты до надзаголовка.
   Крупный отступ нужен только там, где шапки нет и её сменяет сайдбар. */
.hero { padding-top: var(--sp-3); }

.hero__title { margin-top: calc(var(--sp-4) * var(--air)); }
.hero__title em {
  font-style: normal;
  color: var(--accent);
  position: relative;
  white-space: nowrap;
  /* Без своего контекста наложения подложка ниже (z-index: -1) всплывает
     до корневого и рисуется РАНЬШЕ фона body — то есть под ним. Пока у html
     не было фона, фон body уходил на канву и своего бокса не рисовал, так что
     подложку было видно; как только у html появился фон, body начал рисовать
     белый прямоугольник поверх неё. Тот же приём стоит у .zone ниже. */
  isolation: isolate;
}
.hero__title em::after {
  content: '';
  position: absolute;
  left: 0; right: 0; bottom: .08em;
  height: .34em;
  /* Не затухает в прозрачность: на телефоне хвост в ноль читался как
     отсутствие подложки вовсе. */
  background: linear-gradient(90deg, rgba(10, 102, 194, .38), rgba(10, 102, 194, .10));
  border-radius: 3px;
  z-index: -1;
}

.hero__lead { margin-top: calc(var(--sp-4) * var(--air)); max-width: 46ch; }

.hero__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3);
  margin-top: var(--sp-6);
}

/* В синей зоне на узком экране две кнопки обязаны встать в один ряд:
   делим строку пополам и снимаем запрет на сжатие, который flex
   ставит по умолчанию (min-width: auto).

   Половина строки — это около 140px, и длинная надпись в неё одной строкой
   не влезает: .btn по умолчанию запрещает перенос (white-space: nowrap),
   и текст вылезал за скруглённый край. Внутри этих двух кнопок перенос
   разрешён, строки поджаты — надпись складывается в две и остаётся внутри.
   Обе кнопки при этом равной высоты: их тянет флекс. */
@media (max-width: 767px) {
  .zone .hero__actions { flex-wrap: nowrap; gap: var(--sp-2); }
  .zone .hero__actions .btn {
    flex: 1 1 0;
    min-width: 0;
    padding: var(--sp-2) 12px;
    white-space: normal;
    line-height: 1.25;
    text-align: center;
  }
}

@media (min-width: 768px) {
  .hero__actions .btn { min-width: 190px; }
}

/* 1100px — граница, на которой шапка уступает место сайдбару */
@media (min-width: 1100px) {
  .hero { padding-top: calc(var(--sp-8) * var(--air)); }
  /* На мобилке 46ch — нормальная длина строки. На десктопе потолка нет
     вовсе: абзац идёт во всю колонку, до её правого края. При 62ch справа
     простаивало под 300px, и первый экран выглядел незаполненным. */
  .hero__lead { max-width: none; }
}

/* ══════ modules/02-zone.css ══════ */
/* ─────────────────────────────────────────────
   MODULE: zone — СИНЯЯ ПОДЛОЖКА (акцентная зона)
   ───────────────────────────────────────────── */
/* Позиция указателя. Значения по умолчанию — центр блока: без JS, до
   первого движения мыши и на телефоне блок обязан выглядеть законченным,
   а не ждать события, которого не будет. Кладёт их модуль Zone. */
.zone {
  --mx: 50%;
  --my: 0%;

  position: relative;
  border-radius: var(--r-xl);
  /* 24px по бокам, а не --sp-4: шестнадцать под zoom .79 давали на экране
     двенадцать с половиной, и текст с цифрами стоял вплотную к краю
     плашки. Медиазапрос ≥768px ниже перебивает это своими 44px. */
  padding: 22px 24px 18px;
  overflow: hidden;
  color: #fff;
  /* Третий слой — прожектор за курсором. Шесть процентов белого: свет
     должен угадываться, а не спорить с цифрами, ради которых блок и стоит. */
  background:
    radial-gradient(420px circle at var(--mx) var(--my), rgba(255, 255, 255, .06), transparent 70%),
    radial-gradient(120% 90% at 90% 0%, rgba(140, 200, 255, .38), transparent 58%),
    linear-gradient(158deg, var(--brand-500), var(--brand-700) 78%);
  /* Живой градиент: подложка медленно перетекает сама, без участия мыши —
     значит работает и на телефоне. Прожектор из анимации исключён (его
     размер задан в px), иначе он «дышал» бы вместе с фоном. */
  background-size: auto, 170% 170%, 170% 170%;
  box-shadow: 0 20px 50px rgba(10, 102, 194, .22);
  isolation: isolate;
  animation: zone-breathe 16s ease-in-out infinite;
}

@keyframes zone-breathe {
  0%, 100% { background-position: 0 0, 100% 0%, 0% 0%; }
  50%      { background-position: 0 0, 60% 30%, 40% 25%; }
}

/* Технический паттерн — сетка стояков. Окно, в котором она видна, идёт за
   курсором: под мышью проступает «чертёж», в покое — прежний угол. */
.zone::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  background-image:
    linear-gradient(rgba(255,255,255,.09) 1px, transparent 1px),
    linear-gradient(90deg, rgba(255,255,255,.09) 1px, transparent 1px);
  background-size: 46px 46px;
  mask-image: radial-gradient(130% 110% at var(--mx) var(--my), #000 20%, transparent 72%);
  -webkit-mask-image: radial-gradient(130% 110% at var(--mx) var(--my), #000 20%, transparent 72%);
}

/* Зерно поверх заливки. Большие однотонные площади выглядят пластиковыми;
   тонкий шум возвращает им фактуру — приём из печати. overlay, а не простое
   наложение: иначе шум высветляет тёмный край градиента. */
.zone::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  opacity: .15;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='0.5'/%3E%3C/svg%3E");
}

/* Просили покоя — отдаём покой: ни переливания, ни слежения за курсором.
   Зерно остаётся: оно статично и движением не является. */
@media (prefers-reduced-motion: reduce) {
  .zone { animation: none; }
  .zone::before {
    mask-image: radial-gradient(120% 100% at 0% 0%, #000 30%, transparent 78%);
    -webkit-mask-image: radial-gradient(120% 100% at 0% 0%, #000 30%, transparent 78%);
  }
}

.zone__eyebrow {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  font-size: var(--fs-micro);
  font-weight: 650;
  letter-spacing: var(--tr-caps);
  text-transform: uppercase;
  color: rgba(255, 255, 255, .82);
}
.zone__eyebrow::before {
  content: '';
  width: 18px; height: 2px;
  background: currentColor; opacity: .7; border-radius: 2px;
}

.zone__title { color: #fff; margin-top: var(--sp-2); font-size: clamp(20px, 5.4vw, 36px); }
.zone__lead  { color: rgba(255,255,255,.88); margin-top: var(--sp-2); max-width: 52ch; font-size: 14.5px; }

@media (min-width: 768px) {
  .zone { padding: var(--sp-8) var(--sp-7) var(--sp-7); }
  .zone__title { margin-top: var(--sp-3); font-size: var(--fs-h2); }
  .zone__lead  { margin-top: var(--sp-3); font-size: var(--fs-body); }
}

/* Строки лида равной длины вместо «длинная — короткая»: без балансировки
   абзац в две строки ронял хвост в треть ширины, и правая половина плашки
   пустовала. Потолок 52ch при этом не трогаем — с балансировкой разбивка
   получается та же, что и без него, значит расширять колонку незачем. */
@media (min-width: 1100px) {
  .zone__lead { text-wrap: balance; }
}

/* ══════ modules/03-metrics.css ══════ */
/* ─────────────────────────────────────────────
   MODULE: metrics — крупные показатели внутри зоны
   ───────────────────────────────────────────── */
.metrics {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  margin-top: 18px;
  border-top: 1px solid rgba(255,255,255,.22);
}

/* subgrid, как у карточек услуг: «Домов под управлением» переносится на две
   строки, соседние подписи — на одну, и без общих дорожек числа стояли ровно,
   а подписи под ними разъезжались. */
.metric {
  display: grid;
  grid-template-rows: subgrid;
  grid-row: span 2;
  row-gap: 0;
  align-content: start;
  padding: 14px 10px 12px 0;
  border-bottom: 1px solid rgba(255,255,255,.22);
}
@supports not (grid-template-rows: subgrid) {
  .metric { display: block; }
}
.metric:nth-child(odd)  { padding-right: var(--sp-4); border-right: 1px solid rgba(255,255,255,.22); }
.metric:nth-child(even) { padding-left: var(--sp-5); }
.metric:nth-last-child(-n+2) { border-bottom: 0; padding-bottom: 0; }

.metric__val {
  /* flex, а не block: плюс и сноска встают по центру цифр, а не по базовой
     линии. vertical-align в разных браузерах считает середину по-своему —
     здесь центровка задана раскладкой и ведёт себя одинаково везде. */
  display: flex;
  align-items: center;
  flex-wrap: nowrap;
  font-size: clamp(23px, 6.2vw, 44px);
  font-weight: 800;
  letter-spacing: -0.05em;
  line-height: 1;
  color: #fff;
  font-variant-numeric: tabular-nums;
}
/* Плюс и единица — два соседних элемента строки, а не стопка. Обоих
   центрирует align-items: center родителя, поэтому плюс стоит одинаково
   и у «20+», и у «75+», а «Лет» приходится ровно на середину цифр.
   Раньше единица лежала ВНУТРИ sup, стопкой под плюсом: родитель центрировал
   стопку целиком, и её приходилось сдвигать вниз ради плюса — вместе с ним
   на 11px под центр цифр уезжало и «Лет». */
/* Строка, а не столбец: «+» и «Лет» стопкой друг над другом читались
   как две отдельные подписи, а не одна короткая приставка к «20». */
.metric__stack {
  display: flex;
  flex-direction: row;
  align-items: baseline;
  margin-left: 3px;
  gap: 3px;
}

.metric__val sup {
  line-height: 1;
  font-size: .4em;
  font-weight: 700;
  color: rgba(255, 255, 255, .86);
  letter-spacing: 0;
}

.metric__unit {
  font-size: .33em;
  font-weight: 650;
  white-space: nowrap;
  color: rgba(255, 255, 255, .86);
  letter-spacing: 0;
}

.metric__label {
  display: block;
  margin-top: 5px;
  font-size: var(--fs-micro);
  line-height: 1.4;
  color: rgba(255,255,255,.88);
  letter-spacing: 0.01em;
}

@media (min-width: 768px) {
  .metrics { grid-template-columns: repeat(4, minmax(0, 1fr)); margin-top: var(--sp-6); }
  .metric__val { font-size: clamp(30px, 8.6vw, 44px); }
  .metric__label { margin-top: 10px; font-size: var(--fs-xs); }
  /* в один ряд отступ слева нужен всем, кроме первой: иначе текст
     начинается вплотную к разделительной линии предыдущей колонки */
  .metric {
    border-bottom: 0;
    padding: var(--sp-5) var(--sp-5) 0;
    border-right: 1px solid rgba(255,255,255,.22);
  }
  .metric:first-child { padding-left: 0; }
  .metric:last-child  { padding-right: 0; border-right: 0; }
}

/* Сноска-пояснение.
   Кружок 16px: на 20px он спорил по весу с самим «№1» и читался как часть
   показателя. Разборчивость добираем не размером, а плотным белым фоном —
   именно полупрозрачность делала значок мутным, а не мелкий кегль.
   Область нажатия расширена псевдоэлементом до 44px по высоте, но по ширине
   ограничена 28px — квадрат 44×44 у соседних колонок перекрывался, и палец
   попадал не в ту сноску. */
.metric__note {
  position: relative;
  display: inline-grid;
  place-items: center;
  width: 16px; height: 16px;
  /* Строчная «i» асимметрична по вертикали (точка + штрих) — центрирование
     блока через place-items оставляло глиф на ~1px выше геометрического
     центра круга. padding-top сдвигает контент вниз без изменения размера
     самого круга. */
  padding-top: 1px;
  margin-left: 5px;
  border-radius: 50%;
  font-size: 10px;
  font-weight: 650;
  line-height: 1;
  color: var(--brand-800);
  background: rgba(255, 255, 255, .92);
  vertical-align: middle;
  cursor: pointer;
  touch-action: manipulation;
  transition: background var(--t-fast), transform var(--t-fast);
}
/* Масштаб на наведении был 1.12 — рядом с крупной цифрой показателя
   заметно «дёргался» и тянул взгляд на себя. 1.06 достаточно, чтобы
   почувствовать отклик, не соревнуясь с самим числом. */
.metric__note:hover { transform: scale(1.06); }
.metric__note::after {
  content: '';
  position: absolute;
  top: 50%; left: 50%;
  width: 28px; height: 44px;
  transform: translate(-50%, -50%);
}

/* На телефоне цифра показателя садится до 23px, а кружок оставался
   шестнадцатью — рядом с «№1» он переставал быть сноской и начинал
   читаться частью самого показателя. Уменьшаем видимый круг; тап-зона
   в ::after остаётся прежней, 28×44, и трогать её нельзя. */
@media (max-width: 767px) {
  .metric__note {
    width: 14px; height: 14px;
    margin-left: 4px;
    font-size: 9px;
  }

  /* Доли в em подобраны под десктопные 44px цифры. На мобильных 23px те же
     .4 и .33 дают плюсу 9.3px, а единице 7.7px — под zoom .79 это 7.3 и 6.1
     физического пикселя, то есть мельче подписи метрики (--fs-micro, 11px).
     Приставка к самой крупной цифре блока не может быть мельче подписи под
     ней. Здесь единица встаёт вровень с подписью, плюс — чуть крупнее. */
  .metric__val sup { font-size: .56em; }
  .metric__unit    { font-size: .47em; }
}

/* ══════ modules/04-tenets.css ══════ */
/* ─────────────────────────────────────────────
   MODULE: tenets — принципы «Как мы работаем»
   Намеренно без карточной обвязки: рамка, подложка и плитка с иконкой
   остались приметой блока услуг. Здесь работает только номер.
   ───────────────────────────────────────────── */
.tenets {
  display: grid;
  gap: var(--sp-5) var(--sp-6);
  counter-reset: tenet;
}

.tenet {
  display: grid;
  grid-template-columns: 34px minmax(0, 1fr);
  gap: var(--sp-4);
  align-items: start;
}

/* Номер сидит под короткой чертой — тот же приём, что у надзаголовков */
.tenet__no {
  padding-top: var(--sp-2);
  border-top: 2px solid var(--line-brand-2);
  font-size: var(--fs-sm);
  font-weight: 750;
  line-height: 1;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
  transition: border-color var(--t);
}
.tenet:hover .tenet__no { border-top-color: var(--accent); }

.tenet__title {
  font-size: 17px;
  font-weight: 680;
  letter-spacing: -0.022em;
  line-height: 1.28;
}

.tenet__text {
  margin-top: var(--sp-2);
  font-size: var(--fs-sm);
  line-height: var(--lh-body);
  color: var(--ink-500);
  max-width: 46ch;
}

@media (min-width: 768px) {
  .tenets { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--sp-6) var(--sp-8); }
  .tenet__title { font-size: 18px; }
}

/* Кнопка есть только у тезиса про приложение (поле cta в principles.json).
   Отступ сверху, чтобы она не липла к тексту, и ширина по содержимому:
   .btn сам по себе inline-flex, но в grid-колонке растянулся бы на всю ширину. */
.tenet__cta {
  margin-top: var(--sp-3);
  align-self: start;
}

/* ══════ modules/05-services.css ══════ */
/* ─────────────────────────────────────────────
   MODULE: services — карточки услуг
   ───────────────────────────────────────────── */
.services {
  display: grid;
  gap: var(--sp-3);
}

/* Карточка встаёт на дорожки родителя (subgrid): иконка, заголовок, текст и
   список выравниваются по всему ряду карточек. Без этого список был прижат
   к низу через margin-top:auto, и пунктирная линия над галочками вставала на
   разной высоте — тем выше, чем длиннее список у соседа.
   row-gap: 0 гасит зазор ТОЛЬКО между дорожками самой карточки; расстояние
   между рядами карточек по-прежнему держит gap у .services. */
.svc {
  position: relative;
  display: grid;
  grid-template-rows: subgrid;
  grid-row: span 4;
  row-gap: 0;
  align-content: start;
  padding: var(--sp-4);
  border-radius: var(--r-lg);
  background: var(--paper);
  box-shadow: inset 0 0 0 1px var(--line-2), var(--sh-1);
  overflow: hidden;
  transition: box-shadow var(--t), transform var(--t), background var(--t);
}

.svc::after {
  content: '';
  position: absolute;
  left: 0; top: 0; bottom: 0;
  width: 3px;
  background: var(--accent);
  transform: scaleY(0);
  transform-origin: top;
  transition: transform var(--t) var(--e-out);
}

.svc:hover {
  background: var(--paper);
  box-shadow: inset 0 0 0 1px var(--line-brand-2), var(--sh-3);
  transform: translateY(-3px);
}
.svc:hover::after { transform: scaleY(1); }

/* На тач-устройствах :hover не наступает никогда — без этого
   акцентная полоса и подсветка карточки не показывались вообще */
@media (hover: none) {
  .svc:active {
    box-shadow: inset 0 0 0 1px var(--line-brand-2), var(--sh-3);
    transform: translateY(-1px);
  }
  .svc:active::after { transform: scaleY(1); }
}

/* Призрачный номер */
.svc__ghost {
  position: absolute;
  top: -2px;
  right: 8px;
  font-size: 52px;
  font-weight: 800;
  letter-spacing: -0.06em;
  line-height: 1;
  color: var(--line-brand);
  font-variant-numeric: tabular-nums;
  user-select: none;
  pointer-events: none;
}

/* геометрия — в .icon-tile (02-base.css), здесь только отличие по размеру */
.svc__icon { width: 38px; height: 38px; }

.svc__title { margin-top: var(--sp-3); font-size: clamp(16.5px, 4.4vw, 22px); }

.svc__text {
  margin-top: 6px;
  font-size: 13.5px;
  line-height: var(--lh-body);
  color: var(--ink-500);
  max-width: 44ch;
}

.svc__list {
  display: flex;
  flex-direction: column;
  gap: 7px;
  align-self: start;
  margin-top: var(--sp-4);
  padding-top: var(--sp-3);
  border-top: 1px dashed var(--line-2);
}

/* Запасной вариант для браузеров без subgrid: возвращаем прежнюю раскладку —
   линия снова пляшет по высоте, но карточка хотя бы не рассыпается. */
@supports not (grid-template-rows: subgrid) {
  .svc { display: flex; flex-direction: column; }
  .svc__list { margin-top: auto; }
}

.svc__list li {
  display: grid;
  grid-template-columns: 18px 1fr;
  gap: 10px;
  align-items: start;
  font-size: var(--fs-sm);
  line-height: 1.5;
  color: var(--ink-500);
}
.svc__list li::before {
  content: '';
  width: 16px; height: 16px;
  margin-top: 3px;
  border-radius: 4px;
  background: var(--tint);
  box-shadow: inset 0 0 0 1px var(--line-brand);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none'%3E%3Cpath d='M4.4 8.2l2.3 2.3 4.9-5' stroke='%230A66C2' stroke-width='1.9' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: center;
}

@media (min-width: 768px) {
  .services { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--sp-5); }
  .svc { padding: var(--sp-6); }
  .svc__ghost { top: -6px; right: 10px; font-size: 74px; }
  .svc__icon { width: 46px; height: 46px; }
  .svc__title { margin-top: var(--sp-4); font-size: var(--fs-h3); }
  .svc__text { margin-top: var(--sp-2); font-size: var(--fs-sm); }
  .svc__list { gap: 9px; margin-top: var(--sp-5); padding-top: var(--sp-4); }
}

/* ══════ modules/06-reviews.css ══════ */
/* ─────────────────────────────────────────────
   MODULE: testimonials — слайдер отзывов
   ───────────────────────────────────────────── */
.reviews-wrapper {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--sp-3);
}

.reviews {
  display: flex;
  gap: var(--sp-4);
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  /* Не 0: при нулевом значении привязка scroll-snap-align: start считает
     началом padding box и игнорирует padding-left — первая карточка
     защёлкивалась вплотную к краю экрана и выглядела срезанной. */
  scroll-padding-left: var(--page-pad-l);
  padding-block: 24px 40px;
  margin-block: -24px -24px;
  margin-inline: calc(-1 * var(--page-pad-l)) calc(-1 * var(--page-pad));
  padding-inline: var(--page-pad-l) var(--page-pad);
  scrollbar-width: none;
  flex: 1;
}

/* Стояк на мобилке стоит справа, и лента, выходя за поле, проезжала
   под его линией. Влево она по-прежнему уходит к краю экрана —
   это и есть задуманный вылет, — а справа останавливается у стояка. */
@media (max-width: 767px) {
  .reviews { margin-right: 0; padding-right: var(--sp-3); }
}
.reviews::-webkit-scrollbar { display: none; }

.reviews.is-dragging { cursor: grabbing; scroll-snap-type: none; }

@media (min-width: 1100px) {
  /* горизонтальные поля обнуляются, но тень крайних карточек
     должна куда-то ложиться — оставляем запас и гасим его margin */
  .reviews { margin-inline: -30px; padding-inline: 30px; }
}

.review {
  flex: 0 0 min(84vw, 380px);
  scroll-snap-align: start;
  display: flex;
  flex-direction: column;
  padding: var(--sp-5);
  border-radius: var(--r-lg);
  background: var(--paper-2);
  box-shadow: inset 0 0 0 1px var(--line);
  cursor: pointer;
  /* карточка целиком открывает отзыв в модалке — выделять в ней текст
     незачем, а при перетаскивании ленты он подсвечивался синим */
  user-select: none;
  -webkit-user-select: none;
  transition: background var(--t), box-shadow var(--t), transform var(--t);
}
/* карточки проявляются каскадом, а не все разом */
.js [data-reveal] .review {
  opacity: 0;
  transform: translateY(10px);
  transition: opacity 420ms var(--e-out), transform 420ms var(--e-out);
  transition-delay: calc(var(--i, 1) * 70ms);
}
.js [data-reveal].is-in .review { opacity: 1; transform: none; }

.review:hover {
  background: var(--paper);
  box-shadow: inset 0 0 0 1px var(--line-brand-2), var(--sh-3);
  transform: translateY(-3px);
}

/* Карточка целиком — кнопка, но по виду это не читалось: у строк контактов
   и документов шеврон есть, у отзыва не было. Один знак на все три места. */
.review__chev {
  margin-left: auto;
  align-self: center;
  color: var(--ink-300);
  transition: color var(--t-fast), transform var(--t-fast);
}
.review:hover .review__chev { color: var(--accent); transform: translateX(3px); }

.review__quote {
  position: relative;
  font-size: 15px;
  line-height: 1.6;
  color: var(--ink-700);
  flex: 1;
  display: -webkit-box;
  /* Четыре строки — ровно та высота карточки, что была до перехода на
     полный текст (замерено: 247px на десктопе в обоих случаях). Пять строк
     поднимали ленту на строку выше прежнего.

     Обрыв на четвёртой не беда: полного текста хватает, чтобы заполнить
     блок до низа, и срез приходится на нижнюю кромку, а край затухает.
     Дырой это выглядело раньше, когда карточка показывала укороченное поле
     quote: текст кончался на второй строке, а места оставалось на четыре. */
  -webkit-line-clamp: 4;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* Обрыв текста на полуслове читался как баг вёрстки. Модуль Reviews
   ставит .is-clamped только тем цитатам, которые действительно не
   поместились, — короткие отзывы остаются без затемнения. */
.review__quote.is-clamped::after {
  content: '';
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 2.4em;
  background: linear-gradient(180deg, transparent, var(--paper-2) 82%);
  pointer-events: none;
  transition: background var(--t);
}
.review:hover .review__quote.is-clamped::after {
  background: linear-gradient(180deg, transparent, var(--paper) 82%);
}

.review__quote::before {
  content: '«';
  color: var(--accent);
  font-size: 26px;
  font-weight: 700;
  line-height: 0;
  vertical-align: -2px;
  margin-right: 3px;
}

.review__foot {
  display: flex;
  align-items: center;
  gap: 11px;
  margin-top: var(--sp-5);
  padding-top: var(--sp-4);
  border-top: 1px solid var(--line);
}

.review__ava {
  display: grid;
  place-items: center;
  width: 38px; height: 38px;
  border-radius: 11px;
  background: var(--brand-600);
  color: #fff;
  font-size: 13px;
  font-weight: 700;
  letter-spacing: -0.02em;
  flex-shrink: 0;
}

/* акцентная аватарка для отзывов с пометкой в данных */
.review__ava--pink {
  background: linear-gradient(135deg, #FFA4CE 0%, #FFD1E8 100%);
  color: #8B1F4D;
  font-size: 15px;
}

.review__ava-img {
  width: 38px; height: 38px;
  border-radius: 11px;
  object-fit: cover;
  display: block;
}

.review__stars {
  display: flex;
  gap: 2px;
  color: var(--star);
}

/* Звёзды и дата одной группой. Распорка (space-between) отбрасывала дату
   к правому краю карточки, и две части одной мысли — «пять звёзд, тогда-то» —
   читались как два несвязанных элемента по углам. */
.review__topline {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  margin-bottom: var(--sp-2);
}
.review__date {
  font-size: 12.5px;
  color: var(--ink-400);
  white-space: nowrap;
}

/* Та же строка в окне отзыва несёт ещё и адрес. Перенос разрешён: на узком
   экране «звёзды + адрес + дата» в одну строку не помещаются, и без wrap
   дата выдавливала бы адрес за край. */
.review__topline--modal {
  flex-wrap: wrap;
  gap: var(--sp-2) var(--sp-3);
  margin-bottom: var(--sp-4);
  /* По базовой линии, а не по центру. Кегли в строке разные — адрес идёт
     капителью в 11px, дата в 12.5px, — и при центровке по боксу их строки
     садились на разной высоте: заметно, будто съехали. */
  align-items: baseline;
}
/* У иконок базовой линии нет, звёзды выравниваем по центру строки */
.review__topline--modal .review__stars { align-self: center; }
/* .label блочный по умолчанию (02-base.css) — здесь он часть строки */
.review__topline--modal .modal__meta {
  display: inline;
  margin: 0;
}
/* Дата к правому краю: она служебная, и прижимать её к адресу незачем */
.review__topline--modal .review__date { margin-left: auto; }

.review__who { min-width: 0; line-height: 1.25; }
.review__name { display: block; font-size: var(--fs-sm); font-weight: 650; color: var(--ink-900); }
.review__addr { display: block; font-size: var(--fs-xs); color: var(--ink-400); }

/* Управление лентой: стрелки, точки, счётчик */
.reviews-nav {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  margin-top: var(--sp-2);
}
.reviews-nav[hidden] { display: none; }

.reviews-arrow {
  display: grid;
  place-items: center;
  width: 42px;
  height: 42px;
  border-radius: 50%;
  color: var(--accent);
  background: var(--paper);
  box-shadow: inset 0 0 0 1px var(--line-2);
  transition: background var(--t-fast), box-shadow var(--t-fast), color var(--t-fast);
}
.reviews-arrow:hover { background: var(--surface-hover); box-shadow: inset 0 0 0 1px var(--line-brand-2); }
/* Крайние положения ленты: кнопка гаснет, но остаётся на месте —
   иначе панель дёргалась бы при каждом достижении края */
.reviews-arrow[disabled] { opacity: .3; pointer-events: none; }

.reviews-dots {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-left: var(--sp-2);
}
.reviews-dot {
  width: 7px;
  height: 7px;
  border-radius: var(--r-pill);
  background: var(--ink-200);
  transition: width var(--t) var(--e-out), background var(--t);
}
/* Точка текущей карточки вытягивается в полоску */
.reviews-dot.is-on { width: 22px; background: var(--accent); }

.reviews-count {
  margin-left: auto;
  font-size: var(--fs-xs);
  color: var(--ink-300);
}

/* ══════ modules/07-rows.css ══════ */
/* ─────────────────────────────────────────────
   MODULE: contacts — строки контактов
   ───────────────────────────────────────────── */
.rows {
  border-radius: var(--r-lg);
  overflow: hidden;
  box-shadow: inset 0 0 0 1px var(--line-2);
  background: var(--paper);
  margin-top: var(--sp-4);
}

/* ВАЖНО: width: 100% обязателен.
   Часть строк — это <button> (диспетчерская, офис), а у кнопки ширина
   по умолчанию shrink-to-fit даже при display: grid. Без явной ширины
   кнопки сжимались по содержимому, колонка 1fr схлопывалась в max-content,
   текст переносился раньше времени, и карточка не доходила до правого края,
   тогда как соседние <a> тянулись на всю ширину. */
/* Мобильная геометрия — отдельная, а не унаследованная от десктопа.
   С прежними 24px отступов, плиткой 44 и колонкой под шеврон тексту на
   экране 375 оставалось 131px: телефон рвался посреди номера, почта — посреди
   домена, строки вырастали до 286px. Шеврон убран из потока (не колонка, а
   абсолют у правого края) — визуально он там же, но ширину больше не держит. */
.row {
  position: relative;
  display: grid;
  grid-template-columns: 38px minmax(0, 1fr);
  align-items: start;
  gap: var(--sp-3);
  width: 100%;
  min-height: var(--tap);
  padding: 14px 14px;
  text-align: left;
  transition: background var(--t-fast);
}
.row + .row { box-shadow: inset 0 1px 0 var(--line); }
.row:hover { background: var(--surface-hover); }

/* Аварийный телефон — главный сценарий: выделяем кеглем, а не фоном
   (постоянная подложка читалась как «строка всегда подсвечена») */
.row--accent .row__value { font-size: 19px; color: var(--accent-strong); }

/* геометрия плитки — в .icon-tile (02-base.css), здесь только размер под узкий экран */
/* Значок по центру строки, а не у её верха. Сама .row держит align-items: start —
   это нужно тексту, он должен начинаться сверху. Но плитка высотой 38px в строке
   на 102px оставалась прижатой к верху, тогда как шеврон справа центрируется
   отдельно (position: absolute, ниже): в одной строке значок вверху, стрелка
   посередине. Поэтому центрируем только сам значок, не трогая .row. */
.row__icon { width: 38px; height: 38px; border-radius: 11px; align-self: center; }

/* справа оставлен коридор под шеврон, который лежит абсолютом */
.row__body { min-width: 0; padding-right: 22px; }
.row__label {
  display: block;
  font-size: var(--fs-micro);
  font-weight: 650;
  letter-spacing: var(--tr-caps);
  text-transform: uppercase;
  color: var(--ink-300);
}
.row__value {
  display: block;
  margin-top: 3px;
  font-size: 17px;
  font-weight: 650;
  letter-spacing: -0.025em;
  color: var(--ink-900);
  word-wrap: break-word;
  overflow-wrap: break-word;
}

/* колонкой, а не строкой: в строке офиса статус, режим работы и подсказка
   слипались в кашу, где маркер статуса висел отдельно от своего текста */
.row__hint {
  display: flex;
  flex-direction: row;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0;
  margin-top: 4px;
  font-size: var(--fs-xs);
  color: var(--ink-400);
}

/* Только у почты: «Письма, запросы, документы и коммерческие предложения»
   на 12px шире доступной ширины и переносится на вторую строку.
   Не трогаем .row__hint глобально — там же строка офиса, где .hours
   держит свой font-size 12px (02-base.css) независимо от родителя;
   при общем сужении подсказки те же 12px .hours и урезанный текст рядом
   вставали на разную высоту при baseline-выравнивании. */
.row__hint--tight { font-size: 10px; }
@media (min-width: 768px) {
  .row__hint--tight { font-size: var(--fs-xs); }
}

/* «нажмите, чтобы…» — язык мыши. Там, где наведения не бывает, подсказка
   не нужна: строка и так нажимается пальцем, а места занимает до двух строк. */
@media (hover: none) {
  .row__cue { display: none; }
}
.row__chev {
  position: absolute;
  right: 12px;
  top: 50%;
  transform: translateY(-50%);
  /* был --ink-200: шеврон почти не читался, и строка не выглядела нажимаемой */
  color: var(--ink-300);
  transition: transform var(--t-fast), color var(--t-fast);
}
.row__chev svg { width: 18px; height: 18px; }
/* сдвиг по X дописан к центрированию по Y — иначе шеврон подпрыгивал при наведении */
.row:hover .row__chev { color: var(--accent); transform: translate(3px, -50%); }

/* геометрия узкого экрана нужна только там, где экран узкий: с 768px
   возвращаем прежние отступы, плитку и положение шеврона — десктоп не меняется */
@media (min-width: 768px) {
  .row { gap: var(--sp-4); padding: var(--sp-4) var(--sp-5); grid-template-columns: 44px minmax(0, 1fr); }
  .row__icon { width: 44px; height: 44px; border-radius: 13px; }
  .row__body { padding-right: 34px; }
  .row__chev { right: 24px; }
}

/* ── Режим офиса: расписание отдельно от статуса ────────────
   hours.js затирает весь текст элемента [data-hours] своим статусом. Пока расписание
   лежало внутри него, в выходной от строки оставалось одно «Сегодня выходной»: часы и
   перерыв пропадали. Теперь расписание — свой элемент, его никто не трогает. */
.hours__plan {
  font-size: var(--fs-xs);
  color: var(--ink-400);
}
/* Разделителя между расписанием и статусом нет намеренно: точку уже рисует .hours::before,
   и она же отделяет. Пробовали ставить « · » — при пустом статусе он повисал в воздухе. */
.hours__plan + .hours { margin-left: 6px; }
/* до выполнения JS статуса нет — не показываем одинокую точку состояния */
.hours:empty { display: none; }

/* ══════ modules/08-docs.css ══════ */
/* ─────────────────────────────────────────────
   MODULE: docs — служебные ссылки
   ───────────────────────────────────────────── */
/* Список, а не россыпь пилюль. Подписи сильно разной длины (161 / 224 / 221 /
   88px при 299px экрана) давали рваный правый край, и блок читался скомканным.
   Оформление повторяет строки контактов (.rows / .row) — этот паттерн в
   проекте уже отлажен; заодно строка получает тап-таргет во весь --tap
   вместо прежних 40px. */
.docs {
  border-radius: var(--r-lg);
  overflow: hidden;
  box-shadow: inset 0 0 0 1px var(--line-2);
  background: var(--paper);
}

.doc {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--sp-3);
  width: 100%;
  min-height: var(--tap);
  padding: var(--sp-3) var(--sp-4);
  font-size: var(--fs-sm);
  font-weight: 550;
  color: var(--ink-500);
  text-align: left;
  transition: background var(--t-fast), color var(--t-fast);
}
.doc + .doc { box-shadow: inset 0 1px 0 var(--line); }
.doc:hover { background: var(--surface-hover); color: var(--accent-strong); }

/* На тач-устройствах :hover не наступает, и между нажатием и появлением
   окна не происходило ничего — палец не получал подтверждения.
   Тот же приём уже стоит у .svc, здесь он распространён на остальные
   кнопки, открывающие окна. */
@media (hover: none) {
  .row:active,
  .doc:active { background: var(--surface-hover); }
  .review:active {
    background: var(--paper);
    box-shadow: inset 0 0 0 1px var(--line-brand-2), var(--sh-3);
    transform: translateY(-1px);
  }
  .metric__note:active { background: rgba(255, 255, 255, .38); }
  .row:active .row__chev,
  .doc:active .doc__chev,
  .review:active .review__chev { color: var(--accent); }
}

.doc__chev {
  display: grid;
  place-items: center;
  color: var(--ink-300);
  transition: transform var(--t-fast), color var(--t-fast);
}
.doc:hover .doc__chev { color: var(--accent); transform: translateX(3px); }

/* ══════ modules/09-requisites.css ══════ */
/* ─────────────────────────────────────────────
   MODULE: requisites — таблица реквизитов в модалке
   ───────────────────────────────────────────── */
/* Панель «взять всё разом» над таблицей */
.req-all {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--sp-3);
  margin-bottom: var(--sp-4);
  padding: var(--sp-3) var(--sp-4);
  border-radius: var(--r-md);
  background: var(--tint);
}
.req-all__t { font-size: var(--fs-sm); color: var(--ink-700); }
/* Кнопка ниже обычной: она вторична по отношению к построчным копиям */
.req-all__b { min-height: 44px; padding-inline: 18px; }

.req { display: grid; gap: 6px; }

/* Карточка вместо строки списка: у номера появляется кнопка копирования,
   и ей нужна своя область, а не край общей строки. */
.req__row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: 2px var(--sp-3);
  padding: 11px var(--sp-3);
  border-radius: var(--r-sm);
  background: var(--paper-2);
}
.req__key { grid-column: 1; }
.req__val { grid-column: 1; }
.req__copy { grid-column: 2; grid-row: 1 / 3; }
.req__key {
  font-size: var(--fs-micro);
  font-weight: 650;
  letter-spacing: var(--tr-caps);
  text-transform: uppercase;
  color: var(--ink-300);
}
/* Реквизиты — юридические строки (ОКВЭД, адрес, счёт), перенос ломает их
   на середине формулировки и читать неудобно. white-space:nowrap не даёт
   переноситься никогда; у самых длинных значений на узких экранах строка
   не помещается целиком — тогда просто свайп внутри строки, а не обрыв
   текста на новую строку. */
.req__val {
  font-size: var(--fs-sm);
  color: var(--ink-900);
  white-space: nowrap;
  overflow-x: auto;
  -ms-overflow-style: none;
  scrollbar-width: none;
}
.req__val::-webkit-scrollbar { display: none; }
/* Моноширинные цифры: номер разбит по четыре, и группы должны
   вставать столбиком, а не гулять по ширине глифов. */
.req__val--num { font-variant-numeric: tabular-nums; letter-spacing: .02em; }

.req__copy {
  position: relative;
  display: grid;
  place-items: center;
  width: 40px; height: 40px;
  border-radius: 11px;
  background: var(--paper);
  box-shadow: inset 0 0 0 1px var(--line);
  color: var(--ink-400);
  transition: color var(--t-fast), background var(--t-fast);
}
.req__copy:hover { color: var(--accent-strong); }

/* Область нажатия расширена невидимым псевдоэлементом до 44px физических:
   под зумом сорокапиксельная кнопка даёт на экране тридцать два, и пальцем
   в неё попадаешь через раз. Тот же приём стоит у сноски .metric__note. */
.req__copy::after {
  content: "";
  position: absolute;
  top: 50%; left: 50%;
  width: calc(44px / var(--zoom));
  height: calc(44px / var(--zoom));
  transform: translate(-50%, -50%);
}
.req__copy.is-copied { background: var(--ok-600); color: #fff; box-shadow: none; }

/* Иконки лежат обе, показывается одна — модуль Copy только метит классом */
.req__copy-i { display: grid; }
.req__copy-i--ok { display: none; }
.req__copy.is-copied .req__copy-i { display: none; }
.req__copy.is-copied .req__copy-i--ok { display: grid; }

/* На широком экране ключ и значение встают в строку, а кнопка уходит к
   правому краю. Раскладка задаётся здесь целиком — и колонки, и место
   каждого элемента: базовые правила выше расставлены под мобильную
   раскладку, где ключ и значение стоят друг под другом в одной колонке.
   Пока медиазапрос менял только grid-template-columns, эти назначения
   продолжали действовать: значение оставалось в колонке ключа и жалось
   в 220px, а кнопка занимала широкую колонку и висела посреди строки
   вместо правого края. */
@media (min-width: 768px) {
  .req__row {
    grid-template-columns: 220px minmax(0, 1fr) auto;
    gap: var(--sp-4);
    align-items: center;
  }
  .req__key { grid-column: 1; grid-row: 1; }
  .req__val { grid-column: 2; grid-row: 1; }
  /* строку тоже сбрасываем: на мобильной кнопка растянута на две (1 / 3) */
  .req__copy { grid-column: 3; grid-row: 1; }
}

/* ══════ modules/10-modal.css ══════ */
/* ─────────────────────────────────────────────
   MODULE: modal — единая модальная система
   Мобиль: bottom-sheet · Десктоп: центр
   ───────────────────────────────────────────── */
.modal {
  position: fixed;
  inset: 0;
  z-index: var(--z-modal);
  display: flex;
  align-items: flex-end;
  justify-content: center;
  visibility: hidden;
  pointer-events: none;
}
.modal.is-open { visibility: visible; pointer-events: auto; }

/* Кадры закрытия. Раньше .is-open снимался, и окно исчезало мгновенно:
   анимация выезда шторки вниз, прописанная у .modal__sheet, не
   проигрывалась ни разу — её видели только при открытии.
   Класс держит окно видимым, пока шторка уезжает; снимает его модуль
   Modal по transitionend (со страховкой по таймеру).
   Переходом visibility это не решается: браузер обрывает его на
   первом же кадре, как только элемент получает inert. */
.modal.is-closing { visibility: visible; pointer-events: none; }

.modal__scrim {
  position: absolute;
  inset: 0;
  background: rgba(6, 24, 48, .5);
  opacity: 0;
  transition: opacity var(--t);
}
.modal.is-open .modal__scrim { opacity: 1; }

@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .modal__scrim {
    background: rgba(6, 24, 48, .42);
    -webkit-backdrop-filter: blur(5px);
    backdrop-filter: blur(5px);
  }
}

.modal__sheet {
  position: relative;
  width: 100%;
  max-height: calc(88dvh / var(--zoom));
  /* Колонка, а не один прокручиваемый блок: шапка с заголовком и крестиком
     остаётся на месте, уезжает только содержимое. У длинных окон —
     реквизиты, режим работы — крестик всегда под рукой. */
  display: flex;
  flex-direction: column;
  overflow: hidden;
  border-radius: var(--r-xl) var(--r-xl) 0 0;
  background: var(--paper);
  box-shadow: var(--sh-modal);
  transform: translateY(100%);
  transition: transform var(--t-slow);
}
.modal.is-open .modal__sheet { transform: none; }

/* Шапка окна: иконка темы, заголовок, крестик. При прокрутке остаётся
   на месте — у длинных окон (реквизиты, режим работы) крестик всегда
   под рукой, а раньше до него приходилось доскроллить обратно вверх. */
.modal__hd {
  flex: none;
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-5) var(--sp-5) var(--sp-4);
  border-bottom: 1px solid var(--line);
}

/* Иконка темы: окно опознаётся до чтения заголовка */
.modal__ico { width: 40px; height: 40px; }

/* Аватар автора отзыва в шапке модалки. Когда фото есть — прячем запасную
   плитку с инициалами; когда фото нет (img[hidden]) — показываем инициалы. */
.modal__ava {
  width: 40px;
  height: 40px;
  border-radius: 11px;
  object-fit: cover;
  flex: none;
}
.modal__ava:not([hidden]) ~ .modal__ava-fb { display: none; }

/* Плитка со знаком компании вместо SVG-иконки. Логотип лежит растром, и
   у него собственные поля внутри картинки — на общем бледном фоне плитки
   он выглядел бы маркой на конверте. Поэтому фон и внутренняя рамка
   снимаются, знак занимает плитку целиком. */
.modal__ico--logo {
  background: none;
  box-shadow: none;
}
.modal__ico--logo img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  /* Знак везде скруглён примерно на четверть своего размера (78→20, 32→8.2,
     20→5). Здесь радиуса не было вовсе, и в шапке окна знак торчал
     единственным квадратом на сайте. Плитка 44px → те же ≈0.25. */
  border-radius: 11px;
}

/* Прокручивается только содержимое. safe-area учитывается здесь, потому
   что именно этот блок доходит до нижнего края экрана. */
.modal__scroll {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: var(--sp-5) var(--sp-5) calc(var(--sp-6) + env(safe-area-inset-bottom) / var(--zoom));
}

.modal__close {
  flex: none;
  display: grid;
  place-items: center;
  width: var(--tap); height: var(--tap);
  border-radius: 13px;
  background: var(--paper-2);
  /* Контур: на белом листе плашка почти сливалась с фоном
     и крестик приходилось искать глазами. */
  box-shadow: inset 0 0 0 1px var(--line);
  color: var(--ink-700);
  transition: background var(--t-fast), color var(--t-fast);
}
.modal__close:hover { background: var(--surface-hover); color: var(--accent-strong); }

/* не <h2>: заголовок модалки не должен попадать в структуру заголовков страницы */
/* Отступ справа под крестик больше не нужен: крестик стоит в одном ряду
   с заголовком, а не поверх него абсолютом. */
.modal__title { flex: 1; min-width: 0; color: var(--ink-900); }
.modal__meta { margin-bottom: var(--sp-4); }
.modal__body { font-size: var(--fs-body); line-height: var(--lh-loose); color: var(--ink-500); }
.modal__frame { width: 100%; height: calc(62dvh / var(--zoom)); border: 0; border-radius: var(--r-md); overflow: hidden; }

/* На телефоне в альбомной ориентации 62dvh — это ~180px: карта
   превращалась в полоску. Там отдаём ей почти весь экран. */
@media (max-height: 520px) {
  .modal__frame { height: calc(78dvh / var(--zoom)); }
  .modal__sheet { max-height: calc(94dvh / var(--zoom)); }
}

/* Скелетон карты: держим место, пока грузится виджет Яндекса */
.modal__mapbox { position: relative; }
.modal__mapbox .skeleton {
  position: absolute;
  inset: 0;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--t);
}
.modal.is-loading .modal__mapbox .skeleton { opacity: 1; }
.modal__link {
  display: inline-block;
  margin-top: var(--sp-4);
  color: var(--accent);
  font-weight: 600;
}
.modal__link[hidden] { display: none; }
.modal__cta { margin-top: var(--sp-5); }

@media (min-width: 768px) {
  .modal { align-items: center; padding: var(--sp-5); }
  .modal__sheet {
    max-width: 560px;
    max-height: calc(84dvh / var(--zoom));
    border-radius: var(--r-xl);
    transform: translateY(14px) scale(.97);
    opacity: 0;
    transition: transform var(--t), opacity var(--t);
  }
  .modal.is-open .modal__sheet { transform: none; opacity: 1; }
  /* Отступы живут в прокручиваемом теле, а не в листе */
  .modal__hd { padding: var(--sp-6) var(--sp-6) var(--sp-4); }
  .modal__scroll { padding: var(--sp-5) var(--sp-6) var(--sp-6); }
  .modal--wide .modal__sheet { max-width: 860px; }
  .modal__frame { height: calc(60dvh / var(--zoom)); }
}

/* ══════ modules/11-footer.css ══════ */
/* ─────────────────────────────────────────────
   MODULE: footer
   ───────────────────────────────────────────── */
.footer {
  margin-top: var(--block-gap);
  /* Своей подложки нет намеренно: фон низа держит html (02-base.css),
     а серая плашка во всю ширину давала лишний стык с белым контентом.
     safe-area учитывали только док и модалка — теперь и футер. */
  padding: var(--sp-6) 0 calc(var(--sp-5) + env(safe-area-inset-bottom) / var(--zoom));
  border-top: 1px solid var(--line);
  font-size: var(--fs-xs);
  color: var(--ink-400);
}
.footer__row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3) var(--sp-5);
  align-items: center;
  justify-content: space-between;
}

/* ══════ modules/12-news.css ══════ */
/* ─────────────────────────────────────────────
   MODULE: news — лента новостей и страница статьи
   ───────────────────────────────────────────── */
.news {
  display: grid;
  gap: var(--sp-4);
}
/* Сетка вместо столбца. Тело страницы на широком экране около 1500px, и
   лента в одну колонку растягивала карточку во всю эту ширину: заголовок
   в четверть строки, а обложка — горизонтальная полоска. Три колонки дают
   карточке те же ~470px, на которые она и рассчитана. */
/* Порог третьей колонки высокий не по прихоти: тело блока ужато сайдбаром
   и стояком, и на 1280px в нём всего 776px — три колонки дали бы карточку
   в 221px, где заголовок ломается на каждом слове. Тысяча восемьсот — это
   1186px тела и карточка в 357px, с которой уже можно жить. */
@media (min-width: 720px) {
  .news { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (min-width: 1800px) {
  .news { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--sp-5); }
}

/* Карточка целиком — ссылка. Тот же приём, что у .review: кликать по
   заголовку, когда вся плашка ведёт в одно место, незачем. */
.news__link {
  /* Колонка, а не блок: соседи по ряду разной длины, и без растяжки
     тела карточки заканчивались бы на разной высоте. */
  display: flex;
  flex-direction: column;
  height: 100%;
  border-radius: var(--r-lg);
  overflow: hidden;
  background: var(--paper-2);
  box-shadow: inset 0 0 0 1px var(--line-2);
  transition: background var(--t-fast), box-shadow var(--t-fast), transform var(--t-fast);
}
.news__body { flex: 1; display: grid; gap: var(--sp-2); align-content: start; padding: var(--sp-5); }

/* Обложка новости. Пропорция фиксирована, а не высота: снимки приходят
   разных форматов — от квадратов до вертикальных, — и общая рамка держит
   ленту ровной. Раньше здесь стояли жёсткие 190px, и на широком экране
   вертикальный кадр 1497×1920 сминался в полоску во всю ширину страницы.
   3:2 в колонке ~470px даёт ~313px высоты — кадр, а не обрезок.

   Фон — скелетон: пока картинка идёт с чужого CDN, на её месте мерцает
   плашка, а не пустота. Анимацию снимает is-loaded (img-load.js), иначе
   она крутилась бы под уже загруженным снимком. */
.news__cover {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 3 / 2;
  object-fit: cover;
  background-color: var(--tint);
  opacity: 0;
  transition: opacity var(--t-fast);
}
.news__cover.is-loaded,
.news__cover.is-failed { opacity: 1; }
/* Проявление держится на скрипте, поэтому без него картинка обязана быть
   видна сразу: .js ставит app.js, и правило действует только там. */
html:not(.js) .news__cover { opacity: 1; }

@media (prefers-reduced-motion: no-preference) {
  .news__cover {
    background-image: linear-gradient(
      90deg,
      transparent 0%,
      var(--paper) 45%,
      var(--paper) 55%,
      transparent 100%
    );
    background-size: 220% 100%;
    animation: ka-shimmer 1.4s linear infinite;
  }
  .news__cover.is-loaded,
  .news__cover.is-failed { background-image: none; animation: none; }

  @keyframes ka-shimmer {
    from { background-position: 120% 0; }
    to   { background-position: -120% 0; }
  }
}

/* Строка над заголовком: категория и дата */
.news__meta {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  flex-wrap: wrap;
}

.news__tag {
  font-size: 10.5px;
  font-weight: 800;
  letter-spacing: .05em;
  text-transform: uppercase;
  padding: 4px 10px;
  border-radius: var(--r-pill);
  white-space: nowrap;
}
/* Цвета категорий. Пары фон/текст держат контраст не ниже 4.5:1 —
   надпись мелкая и заглавными, ей нужен запас. */
.news__tag--news  { background: #E0F2FE; color: #075985; }
.news__tag--alert { background: #FEE9E9; color: #991B1B; }
.news__tag--event { background: #E7F8EE; color: #166534; }
.news__tag--bills { background: #FFF2E2; color: #9A3412; }
.news__link:hover {
  background: var(--paper);
  box-shadow: inset 0 0 0 1px var(--line-brand-2), var(--sh-3);
  transform: translateY(-3px);
}
.news__link:active { transform: translateY(-1px); }

.news__date {
  font-size: var(--fs-micro);
  font-weight: 650;
  letter-spacing: var(--tr-caps);
  text-transform: uppercase;
  color: var(--ink-300);
  font-variant-numeric: tabular-nums;
}

/* Обрез по строкам: в ряду сетки карточки стоят плечом к плечу, и лид
   на шесть строк рядом с лидом на две выглядит сбоем, а не разнообразием.
   Полный текст ждёт на странице новости — карточка только зазывает. */
.news__title {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  font-size: 19px;
  font-weight: 700;
  letter-spacing: -0.022em;
  line-height: 1.3;
  color: var(--ink-700);
}

.news__lead {
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
  font-size: var(--fs-sm);
  line-height: var(--lh-body);
  color: var(--ink-500);
  /* Мера строки нужна только там, где лента ещё в одну колонку:
     в сетке ширину задаёт сама колонка. */
  max-width: 62ch;
}

.news__more {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  margin-top: var(--sp-1);
  font-size: var(--fs-sm);
  font-weight: 650;
  color: var(--accent);
}
.news__chev { display: inline-flex; transition: transform var(--t-fast); }
.news__link:hover .news__chev { transform: translateX(3px); }

/* Пустая лента: механизм наполняет парсер канала MAX, и до первой новости
   раздел обязан выглядеть осознанно, а не сломанной вёрсткой. */
.news__empty {
  padding: var(--sp-6);
  border-radius: var(--r-lg);
  background: var(--paper-2);
  box-shadow: inset 0 0 0 1px var(--line-2);
  font-size: var(--fs-sm);
  line-height: var(--lh-body);
  color: var(--ink-500);
}
.news__empty .tlink { display: inline; min-height: 0; }

/* ── Страница одной новости ────────────────────────── */
.crumbs {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  margin-bottom: var(--sp-4);
  font-size: var(--fs-micro);
  color: var(--ink-300);
}
.crumbs__link { color: var(--ink-500); transition: color var(--t-fast); }
.crumbs__link:hover { color: var(--accent-strong); }
.crumbs__sep { color: var(--ink-200); }

/* Тело статьи приходит готовой разметкой из data/news.json, поэтому стили
   вешаются на потомков контейнера, а не на классы: расставить классы
   в тексте, который пишет парсер, невозможно. */
.prose {
  margin-top: var(--sp-5);
  max-width: 68ch;
  /* Мера строки уже колонки, поэтому текст центрируем — иначе он стоял бы
     левее заголовка и футера, с которыми делит страницу. На узком экране
     колонка сама уже 68ch, и авто-поля ничего не двигают. */
  margin-inline: auto;
  font-size: var(--fs-body);
  line-height: 1.7;
  color: var(--ink-700);
}
.prose > * + * { margin-top: var(--sp-4); }
.prose h2 {
  margin-top: var(--sp-6);
  font-size: 22px;
  font-weight: 700;
  letter-spacing: -0.022em;
}
.prose h3 { margin-top: var(--sp-5); font-size: 18px; font-weight: 680; }
.prose ul, .prose ol { padding-left: 1.3em; }
.prose li + li { margin-top: var(--sp-2); }
.prose ul > li { list-style: disc; }
.prose ol > li { list-style: decimal; }
.prose a { color: var(--accent-strong); text-decoration: underline; text-underline-offset: 2px; }
.prose img { border-radius: var(--r-md); }

/* ── Галерея снимков в статье ──────────────────────── */
/* Ширину галереи задаёт форма снимка, а не колонка страницы. При вписывании
   кадра целиком (contain) вертикальное фото занимало 430px из 936 — больше
   половины ленты оставалось пустой плашкой. Теперь размер ведёт пропорция:
   --gal-ar приходит из данных и равен самому ШИРОКОМУ кадру галереи
   (build.js, Math.max по w/h). Именно широкий задаёт потолок высоты: при
   такой пропорции ни у одного снимка не остаётся полей сверху и снизу,
   а у более узких запас уходит по бокам — там он и уместен.

   По центру колонки — как и текст статьи под ней. Заголовок, крошки и
   «Другие новости» идут во всю ширину, и их центр совпадает с центром
   футера; фото и текст уже, поэтому прижатые влево они выглядели
   съехавшими относительно всего остального. */
.gallery {
  position: relative;
  margin-top: var(--sp-5);
  --gal-h: min(62vh, 460px);
  width: min(100%, calc(var(--gal-h) * var(--gal-ar, 1.33)));
  margin-inline: auto;
}

/* Лента со снапом: пальцем, колесом и перетаскиванием мышью. Полосу
   прокрутки прячем — о том, что лента листается, говорят точки и то,
   что следующий снимок выглядывает справа. */
.gallery__track {
  display: flex;
  gap: var(--sp-2);
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
  -ms-overflow-style: none;
  border-radius: var(--r-lg);
  background: var(--tint);
  /* Высоту держит дорожка, а не снимок: кадры приходят и вертикальные,
     и горизонтальные, и при высоте по картинке лента прыгала бы при
     каждом листании. Одинаковая рамка на всех слайдах — лента стоит.

     Но высота выводится из ШИРИНЫ, а не задаётся числом. Было height:
     var(--gal-h) — на десктопе это совпадало с шириной по пропорции и
     полей не давало, а на телефоне ширина упиралась в 100% колонки, тогда
     как высота оставалась прежними 62vh: у горизонтального кадра сверху
     и снизу висели широкие полосы подложки. aspect-ratio держит ту же
     пропорцию, что и ширина, а --gal-h остаётся потолком — на широком
     экране всё считается ровно как раньше. */
  align-items: center;
  height: auto;
  aspect-ratio: var(--gal-ar, 1.33);
  max-height: var(--gal-h);
}
.gallery__track::-webkit-scrollbar { display: none; }
.gallery__track.is-dragging { cursor: grabbing; scroll-snap-type: none; }

/* contain, а не cover. Было cover при высоте до 420px: на широком экране
   дорожка растягивалась за 700px, и вертикальный снимок — а таких в канале
   большинство — сминался в узкую полосу, где от кадра оставалась треть.
   Поля по бокам добирает --tint самой дорожки. */
.gallery__shot {
  /* Ширина у каждого слайда своя, по его собственной пропорции: так кадр
     всегда виден целиком и без полей по бокам. Раньше слайд занимал всю
     ширину ленты, и вертикальное фото рисовалось посреди серой плашки,
     съедавшей половину окна. Когда слайд уже окна, в оставшееся место
     заглядывает следующий снимок — это лента, а не пустота. */
  flex: 0 0 auto;
  width: auto;
  max-width: 100%;
  height: 100%;
  object-fit: contain;
  scroll-snap-align: start;
  border-radius: var(--r-lg);
  /* Перетаскивание не должно превращаться в перетаскивание картинки */
  -webkit-user-drag: none;
  user-select: none;
}

@media (min-width: 1100px) {
  .gallery { --gal-h: min(70vh, 560px); }
}

/* Стрелки поверх снимка. Форма и размер — как у стрелок ленты отзывов
   (06-reviews.css), но фон полупрозрачный: кнопка лежит на фотографии,
   а не на бумаге, и глухая белая таблетка выглядела бы заплаткой. */
.gallery__arrow {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  z-index: 1;
  display: grid;
  place-items: center;
  width: 42px;
  height: 42px;
  border-radius: 50%;
  color: var(--accent-strong);
  background: var(--paper);
  box-shadow: inset 0 0 0 1px var(--line-2), var(--sh-3);
  transition: background var(--t-fast), opacity var(--t-fast);
}
@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .gallery__arrow {
    background: color-mix(in srgb, var(--paper) 78%, transparent);
    -webkit-backdrop-filter: saturate(180%) blur(12px);
    backdrop-filter: saturate(180%) blur(12px);
  }
}
.gallery__arrow--prev { left: var(--sp-3); }
.gallery__arrow--next { right: var(--sp-3); }
.gallery__arrow:hover { background: var(--paper); }
.gallery__arrow[disabled] { opacity: .3; pointer-events: none; }
.gallery__arrow[hidden] { display: none; }

.gallery__dots {
  display: flex;
  justify-content: center;
  gap: 6px;
  margin-top: var(--sp-3);
}
.gallery__dot {
  width: 7px;
  height: 7px;
  border-radius: var(--r-pill);
  background: var(--line-2);
  transition: width var(--t-fast), background var(--t-fast);
}
.gallery__dot.is-on { width: 20px; background: var(--accent); }
/* Точка сама по себе меньше пальца — площадь нажатия расширяем незаметно */
.gallery__dot { position: relative; }
.gallery__dot::after {
  content: '';
  position: absolute;
  top: 50%; left: 50%;
  width: calc(30px / var(--zoom));
  height: calc(30px / var(--zoom));
  transform: translate(-50%, -50%);
}

/* Блок «Другие новости» под статьёй */
.news-more { margin-top: var(--sp-8); padding-top: var(--sp-6); border-top: 1px solid var(--line); }
.news-more__h {
  font-size: 20px;
  font-weight: 720;
  letter-spacing: -0.022em;
  color: var(--ink-900);
}
.news-more__list { margin-top: var(--sp-4); }
.news-more__lead {
  margin-top: var(--sp-3);
  max-width: 64ch;
  font-size: var(--fs-sm);
  line-height: var(--lh-body);
  color: var(--ink-500);
}
/* Ссылки в строку на широком, столбиком на узком — .tlink сам держит 44px */
.news-more__links { display: flex; flex-wrap: wrap; gap: 0 var(--sp-5); margin-top: var(--sp-2); }
/* Карточки короче обычных: здесь нужен только заголовок со ссылкой */
.news-more__list .news__link { padding: var(--sp-4); gap: var(--sp-1); }
.news-more__list .news__title { font-size: 16.5px; }

@media (min-width: 768px) {
  .news-more__list { grid-template-columns: 1fr 1fr; }
}

/* На телефоне блок не показываем. Внизу статьи и без него есть пагинация
   «Новее / Все новости / Старее», а две карточки следом за ней — та же
   мысль второй раз, только длиннее.

   Прячем стилями, а не убираем из разметки: этот блок несёт единственный
   на странице H2. Тело статьи пишет парсер канала MAX, обязать его ставить
   подзаголовки нельзя, и без H2 страница для робота выглядит обрубком.
   Скрытый заголовок из документа никуда не девается, а на десктопе он
   и виден — маскировкой это не является. */
@media (max-width: 767px) {
  .news-more { display: none; }
}

.news__source,
.news__back {
  margin-top: var(--sp-6);
  font-size: var(--fs-sm);
  color: var(--ink-500);
}
.news__source .tlink,
.news__back .tlink { display: inline; min-height: 0; }

/* ── Пагинация ленты ───────────────────────────────── */
/* Лента режется по 12 записей (build.js, newsFeedPages): обложки идут с
   чужого CDN оригиналами, и все 98 разом — это десятки мегабайт. Ссылки
   обычные, без скриптов: с выключенным JS и для робота лента проходима. */
.pager {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  margin-top: var(--sp-6);
  padding-top: var(--sp-5);
  border-top: 1px solid var(--line);
}
.pager__link { font-size: var(--fs-sm); font-weight: 650; white-space: nowrap; }
/* Край ленты: стрелка остаётся на месте, чтобы строка не перестраивалась
   на первой и последней странице, но перестаёт выглядеть нажимаемой. */
.pager__link--off { color: var(--ink-200); }

/* В середине пагинации архива стоит счётчик страниц, а под статьёй —
   ссылка на весь список. Вид у неё тот же служебный, но она кликается,
   и это должно быть видно хотя бы по наведению. */
a.pager__no { color: var(--ink-400); }
a.pager__no:hover { color: var(--accent); }
.pager__no {
  font-size: var(--fs-micro);
  letter-spacing: var(--tr-caps);
  text-transform: uppercase;
  color: var(--ink-300);
  font-variant-numeric: tabular-nums;
  text-align: center;
}

/* Ступенчатое появление карточек. Номер --i шаблон проставлял давно,
   но он никуда не шёл: блок выезжал целиком. В сетке из трёх колонок
   разница заметна — ряды набираются один за другим. */
@media (prefers-reduced-motion: no-preference) {
  .js [data-reveal] .news__item {
    opacity: 0;
    transform: translateY(10px);
    transition: opacity 420ms var(--e-out), transform 420ms var(--e-out);
    transition-delay: calc(var(--i, 0) * 26ms);
  }
  .js [data-reveal].is-in .news__item { opacity: 1; transform: none; }
}

/* ── Догрузка ленты на главной ─────────────────────── */
/* Лента на главной отдаёт первую девятку разметкой, остальное дотягивает
   news-more.js. Кнопка — ссылка на архив: без скриптов клик уводит на
   /novosti/, где та же лента листается пагинацией. Отдельный класс, а не
   .news-more: тот занят блоком перелинковки внизу статьи. */
.news-load {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--sp-3) var(--sp-5);
  margin-top: var(--sp-6);
  padding-top: var(--sp-5);
  border-top: 1px solid var(--line);
}

.news-load__btn[hidden] { display: none; }

/* Пока идёт запрос, надпись меняется на «Загружаем…» — кнопка при этом
   не должна дёргать соседей по строке, поэтому ширина зафиксирована по
   самому длинному состоянию. */
.news-load__btn { min-width: 190px; justify-content: center; }
.news-load__btn[aria-busy="true"] { color: var(--ink-300); }

.news-load__all { font-size: var(--fs-sm); font-weight: 650; }

/* ══════ modules/13-reveal.css ══════ */
/* ─────────────────────────────────────────────
   Появление блоков при скролле
   ───────────────────────────────────────────── */
/* Прячем блоки до появления ТОЛЬКО если JS жив (класс js ставит инлайн-скрипт
   в <head>). Иначе при сбое скрипта страница осталась бы пустой — и для
   человека, и для поискового робота. */
.js [data-reveal] {
  opacity: 0;
  transform: translateY(16px);
  transition: opacity 560ms var(--e-out), transform 560ms var(--e-out);
}
.js [data-reveal].is-in { opacity: 1; transform: none; }
