/* blog.css — дизайн-система блога ("fashion-журнал": Vogue/Harper's
   Bazaar/Kinfolk по ощущению, в рамках бренда сайта), 2026-10-03. ТОЛЬКО
   страницы /blog и /blog/:slug (+ Preview в админке, который использует
   тот же рендер) — подключается явно в src/lib/blog-ssr.js, не трогает
   /css/style.css и не грузится больше ни на одной странице сайта.

   Контент статей (markdown) не меняется этой правкой — всё оформление
   здесь и в src/lib/blog-content.js (разметка, не текст). Классы/id,
   которые читает public/js/app.js (шапка) и инлайн-скрипт корзины
   (.blog-service-embed и его потомки, см. ADD_TO_CART_SCRIPT в
   blog-ssr.js) — НЕ переименованы, только переоформлены.

   Переменные — единственное место, где задаётся палитра (--blog-*),
   см. docs/blog-style-guide.md, раздел "Дизайн-система блога".

   Эффекты — ТОЛЬКО transform/opacity (см. blog-fx.js): никакого
   parallax/position:fixed-фонов/blur/backdrop-filter/анимации
   background-position или тяжёлых анимированных теней. */

/* ---------------------------------------------------------------- Шрифты */

/* Типографика v3 — "книжная" Kinfolk-антиква (2026-10-03, задание
   "приблизить типографику блога к Kinfolk"): заголовки — Cormorant
   Garamond 400 (легче прежнего 500, см. git-историю), текст — Source
   Serif 4 400 (сухая книжная антиква взамен Lora), подписи — Inter 600
   (нейтральный гротеск взамен системного sans). Выбор сделан после
   сравнения на временной public/blog-font-test.html (вариант A против
   B/Spectral и C/текущий — удалена после проверки, см. Итог задания).
   Та же техника самохостинга, что и раньше: latin+cyrillic отдельными
   unicode-range подмножествами, только нужные начертания. */

/* Cormorant Garamond 400 — заголовки H1/H2/H3/нумерация/цитата. */
@font-face {
  font-family: 'Cormorant Garamond';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/fonts/cormorant-garamond-400-cyrillic.woff2') format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
  font-family: 'Cormorant Garamond';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/fonts/cormorant-garamond-400-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;
}
/* Cormorant Garamond 400 italic — H3, цитата, нумерация H2. */
@font-face {
  font-family: 'Cormorant Garamond';
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url('/fonts/cormorant-garamond-400italic-cyrillic.woff2') format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
  font-family: 'Cormorant Garamond';
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url('/fonts/cormorant-garamond-400italic-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;
}

/* Source Serif 4 400 — основной текст статьи (лид/абзацы/список/таблица). */
@font-face {
  font-family: 'Source Serif 4';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/fonts/source-serif-4-400-cyrillic.woff2') format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
  font-family: 'Source Serif 4';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/fonts/source-serif-4-400-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;
}
/* Source Serif 4 400 italic — лид (.blog-lead), подпись/сноска (em). */
@font-face {
  font-family: 'Source Serif 4';
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url('/fonts/source-serif-4-400italic-cyrillic.woff2') format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
  font-family: 'Source Serif 4';
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url('/fonts/source-serif-4-400italic-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;
}

/* Inter 600 — ЕДИНСТВЕННЫЙ нужный вес (все UI-надписи блога — kicker/
   дата/рубрики/ярлыки/"Содержание"/подписи таблиц — уже были и остаются
   font-weight:600, задание меняет только гарнитуру, не остальной дизайн)
   — self-host одного веса вместо диапазона экономит заметный вес файла
   (диапазон 400-600 у variable-инстанса Inter втрое тяжелее одного веса,
   проверено при подборе, см. Итог). */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('/fonts/inter-600-cyrillic.woff2') format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('/fonts/inter-600-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;
}

/* Fallback-шрифты с size-adjust/ascent-override/descent-override —
   метрики подобраны приблизительно (эмпирические значения, не обмерены
   точным инструментом в этой среде) — цель не пиксель-в-пиксель, а
   заметно уменьшить "скачок" высоты строки в момент подмены system-
   шрифта на веб-шрифт (font-display: swap), а не убрать его полностью.
   local() — чтобы вообще не тратить лишний сетевой запрос на сам
   fallback, он всегда уже есть в системе. */
@font-face {
  font-family: 'Cormorant Garamond Fallback';
  src: local('Georgia'), local('Times New Roman');
  size-adjust: 112%;
  ascent-override: 95%;
  descent-override: 25%;
}
@font-face {
  font-family: 'Source Serif 4 Fallback';
  src: local('Georgia'), local('Times New Roman');
  size-adjust: 104%;
  ascent-override: 92%;
  descent-override: 24%;
}
@font-face {
  font-family: 'Inter Fallback';
  src: local('Arial'), local('Helvetica');
  size-adjust: 107%;
  ascent-override: 90%;
  descent-override: 25%;
}

/* ---------------------------------------------------------------- Переменные */

:root {
  /* Тёплый ivory — см. задание. Единственное место определения палитры
     блога, тёмная тема переопределяет те же имена ниже. */
  --blog-bg: #faf7f2;
  --blog-surface: #f3efe7;
  --blog-ink: #1f1b16;
  --blog-muted: #6b6354;
  /* #f5a623 — ТОЧНОЕ значение --color-accent основного сайта (style.css,
     светлая тема) — задание 2026-10-05: "акцентные элементы сейчас
     коричневые... сделать оранжевыми, как на основном сайте... новый
     цвет не выдумывать". БЫЛО #a85d00 — сознательно затемнённый вариант
     (4.64:1 на --blog-bg) именно ПОД эту же проблему контраста, тем же
     приёмом, что --color-quick-link-text (#c25100)/--color-accent-dark
     (#d98c0f) на основном сайте — но задание прямо просит точное
     значение --color-accent, не компенсирующий тёмный вариант. Контраст
     #f5a623 на --blog-bg (#faf7f2) — 1.90:1, НИЖЕ обоих порогов (3:1
     крупный текст, 4.5:1 мелкий — kicker/номера H2/цена). По заданию
     ("оставь для мелкого текста тот же оранжевый... не меняя цвет
     самовольно") — используется ОДИН И ТОТ ЖЕ #f5a623 везде, без
     отдельного мелкого/крупного варианта; цифры контраста — см. Итог
     задания, самостоятельно не скорректировано. --blog-accent-dark —
     аналогично точное значение --color-accent-dark сайта (сам нигде в
     blog.css не используется на момент этой правки, оставлен для
     смысловой парности с --blog-accent). */
  --blog-accent: #f5a623;
  --blog-accent-dark: #d98c0f;
  --blog-line: #e4ddd0;

  --blog-font-head: 'Cormorant Garamond', 'Cormorant Garamond Fallback', Georgia, 'Times New Roman', serif;
  --blog-font-body: 'Source Serif 4', 'Source Serif 4 Fallback', Georgia, 'Times New Roman', serif;
  --blog-font-ui: 'Inter', 'Inter Fallback', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
}

body.dark-theme {
  --blog-bg: #1a1a1a;
  --blog-surface: #242424;
  --blog-ink: #f2ede6;
  --blog-muted: #a89a85;
  /* #ffb648 — 9.98:1 на тёмном --blog-bg, тот же оттенок, что и
     --color-accent в тёмной теме сайта (style.css:body.dark-theme). */
  --blog-accent: #ffb648;
  --blog-accent-dark: #f5a623;
  --blog-line: #3a3632;
}

/* ---------------------------------------------------------------- Базовая колонка */

/* Фон страницы НЕ переопределяется здесь намеренно — .blog-page-content
   общий класс для /blog, /blog/:slug И /blog/cart (последний вне задания,
   см. ниже), а --color-bg сайта (style.css) и так уже тёплый почти-белый
   (#faf9f7), визуально не отличимый от нужного ivory — трогать его ради
   /blog/:slug означало бы попутно перекрасить и cart. Ivory ниже (--blog-bg)
   используется только ТОЧЕЧНО, на конкретных блоках (обложка-заглушка,
   карточки), не как фон всей страницы. */
.blog-page-content { padding-bottom: calc(20px * var(--density-scale)); }

/* ---------------------------------------------------------------- Закреплённая шапка (баг, найден и исправлен 2026-10-04) */

/* ПРИЧИНА: на основном сайте .topbar — ПРЯМОЙ ребёнок .app-shell (той же
   flex-column, что и здесь), на /blog он на уровень глубже — ребёнок
   #blog-header-mount (пустой <div>, см. renderPage в blog-ssr.js,
   заполняется ТОЛЬКО клиентским mountBlogShell() в app.js — та же разметка
   и функции рендера шапки, что и на сайте, renderAuthedTopbarHtml/
   renderPublicTopbarHtml, НЕ задублированы). Статический аудит style.css/
   blog.css (html, body, #app, .app-shell, #blog-header-mount) НЕ нашёл ни
   overflow, ни transform/filter/perspective/contain/will-change ни на
   одном предке — формально цепочка чистая. Тем не менее position: sticky
   у .topbar реально не фиксировался на /blog (прошлая попытка это не
   чинила, потому что чинила не ту причину — конфликт с полосой прогресса,
   а не саму стики-позицию) — known-класс багов WebKit/Safari: sticky
   внутри flex-column работает менее надёжно, когда sticky-элемент НЕ сам
   прямой flex-item, а вложен ещё на уровень глубже в простой div-обёртку
   (ровно наша разница с основным сайтом). Вместо точечного патча под
   Safari — на блоге .topbar переведён на position: fixed (задание
   разрешает fixed ИЛИ sticky) — абсолютно не зависит от flex/grid-
   контекста, ломается только transform/filter/perspective/will-change на
   предке (их нет, см. выше). .topbar на ОСНОВНОМ сайте не трогался —
   правка скопирована сюда, а не перенесена, задание ограничено блогом.
   #blog-header-mount получает явную height: var(--topbar-height) —
   geometric-плейсхолдер, замещающий место, которое .topbar раньше занимал
   в потоке (fixed выключает элемент из потока целиком, flex-колонка без
   этого схлопнула бы шапку до 0 высоты, а контент наехал бы под неё) —
   попутно убирает и лёгкий сдвиг контента (CLS) в момент, когда
   mountBlogShell() ещё не успел асинхронно отрисовать шапку. */
#blog-header-mount {
  height: var(--topbar-height);
}

#blog-header-mount .topbar {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
}

.blog-article-body {
  max-width: 720px;
  margin: 0 auto;
  color: var(--blog-ink);
  font-family: var(--blog-font-body);
  /* 17-18px на мобилке, 18-19px на десктопе (≥780px, см. media-запрос
     ниже) — задание. */
  font-size: 17.5px;
  /* 1.58 (было 1.7) — задание 2026-10-04, "межстрочный интервал...
     читать невозможно", целевой диапазон 1.55-1.6. На ВСЕХ брейкпоинтах
     (десктопное .blog-article-body ниже своего line-height не задаёт —
     наследует отсюда же). Заголовки (h1/h2/h3/.blog-kicker и т.п.) свой
     line-height не трогали — у каждого своё отдельное правило. */
  line-height: 1.58;
  counter-reset: blog-h2;
  /* Старостильные цифры в running-тексте (задание) — наследуется вниз на
     лид/абзацы/списки/подписи (font-variant-numeric — inherited-свойство).
     ОГОВОРКА (см. docs/blog-style-guide.md и Итог задания): у Source
     Serif 4 в сборке Google Fonts фичи onum физически нет (проверено по
     GSUB-таблице и по форме цифр) — дефолт у неё и так lining, эта
     декларация ничего не переключает, просто безвредно ничего не
     делает. Оставлена намеренно: корректна по спецификации и готова,
     если фича когда-нибудь появится/шрифт сменится. */
  font-variant-numeric: oldstyle-nums;
}

/* body { overflow-x: hidden } ЗДЕСЬ БЫЛ (макет v2) как страховка под
   .blog-figure--full (viewport-breakout) — убран вместе с самим "full"
   (задание 2026-10-04, "остаётся ОДИН вариант"), и НЕ просто за
   ненужностью: такое правило на ЛЮБОМ предке .blog-sec сломало бы
   position: sticky у .blog-fig (sticky считается относительно ближайшего
   СКРОЛЛЯЩЕГОСЯ предка — overflow:hidden/auto/scroll на body сделал бы
   ИМЕННО body этим предком вместо реального вьюпорта, см. задание п.2 и
   docs/blog-style-guide.md). Больше никто во всём blog.css не выходит за
   пределы родителя viewport-unit'ами — убирать безопасно без замены.
   `.blog-table-wrap`/`.blog-card-image--placeholder` и т.п. — их
   overflow:hidden/auto НЕ на предке .blog-sec, а на самостоятельных,
   параллельных блоках, sticky не касаются. */

/* 0.9em (было 1.3em) — задание 2026-10-04, "вертикальные отступы между
   абзацами уменьшить примерно вдвое", ориентир 0.8-1em. */
.blog-article-body p { margin: 0 0 0.9em; }
.blog-article-body strong { font-weight: 600; }
.blog-article-body em { font-style: italic; }

/* ---------------------------------------------------------------- Шапка статьи */

.blog-article-header {
  text-align: center;
  padding: calc(28px * var(--density-scale)) 0 calc(20px * var(--density-scale));
}

.blog-kicker {
  font-family: var(--blog-font-ui);
  font-size: 0.72rem;
  font-weight: 600;
  /* 0.08-0.14em — диапазон задания (было 0.22em, заметно шире). */
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--blog-accent);
  margin: 0 0 calc(14px * var(--density-scale));
}

.blog-article-header h1 {
  font-family: var(--blog-font-head);
  font-weight: 400;
  /* Капслок + разрядка — Kinfolk-типографика (2026-10-03): раньше
     uppercase был только у сплит-хиро (.blog-hero-headline h1 ниже),
     здесь добавлен для единообразия (задание прямо считает его уже
     существующим для H1 — приводим оба варианта шапки статьи к одному
     правилу). БЕЗ переноса слова с дефисом (баг, найден и исправлен
     2026-10-05: "слова не переносятся с дефисом") — hyphens:none вместо
     auto, overflow-wrap/word-break:normal вместо break-word — целое слово
     уходит на следующую строку вместо разрыва внутри. text-wrap:balance
     остаётся; переполнение теперь не страхуется разрывом слова, а
     предотвращается размером шрифта (clamp — см. .blog-hero-headline h1
     ниже, расчёт в Итоге задания на самое длинное слово статьи). */
  text-transform: uppercase;
  font-size: clamp(2.1rem, 5vw, 3.2rem);
  line-height: 1.1;
  letter-spacing: 0.03em;
  margin: 0;
  text-wrap: balance;
  overflow-wrap: normal;
  word-break: normal;
  hyphens: none;
}

.blog-header-rule {
  width: 64px;
  height: 1px;
  background: var(--blog-line);
  margin: calc(20px * var(--density-scale)) auto;
}

/* .blog-article-dates — класс существовал и раньше (см. test/blog-ssr.test.js,
   проверяет текст "Опубликовано"/"Published" внутри <time>) — разметка
   этой функции не менялась, только оформление здесь. */
.blog-article-dates {
  font-family: var(--blog-font-ui);
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--blog-muted);
  display: flex;
  justify-content: center;
  gap: calc(16px * var(--density-scale));
  flex-wrap: wrap;
  margin: 0;
}

/* ---------------------------------------------------------------- Обложка (hero) */

/* aspect-ratio (не реальные width/height — у OG-картинки их просто нет в
   БД, см. docs/blog-style-guide.md) — зарезервированное место ДО загрузки
   картинки, тот же эффект против CLS, что и настоящие width/height у
   картинок В ТЕЛЕ статьи (те их имеют — конвейер публикации проставляет
   сам, см. scripts/publish-article.js). 1200/630 — стандартная OG-пропорция,
   та же, что рекомендована для самой og-картинки. */
.blog-article-hero {
  max-width: 1040px;
  margin: 0 auto calc(32px * var(--density-scale));
  aspect-ratio: 1200 / 630;
  overflow: hidden;
  border-radius: var(--radius-sm);
  background: var(--blog-surface);
}

.blog-article-hero img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* ---------------------------------------------------------------- Обложка (cover) — сплит-хиро v2 */

/* Редакционный макет v2 (2026-10-03, "по мотивам Kinfolk") — ТОЛЬКО когда
   у статьи есть новая обложка `cover` (front matter конвейера публикации,
   см. docs/blog-style-guide.md) — БЕЗ cover остаётся прежний вариант выше
   (.blog-article-hero, из article.ogImage), см. blog-ssr.js:
   renderArticlePageForArticle — две полностью независимые ветки рендера.
   Мобилка — просто нормальный поток (текст, затем обложка), сплит 50/50
   на весь экран — только на ≥780px, см. media-запрос ниже. */
/* Отступ после первого экрана (задание 2026-10-05, п.4): раньше его не
   было вовсе — padding-bottom:0 + .blog-sec:first-child margin-top:0
   (см. ниже) давали ровно НОЛЬ пикселей между концом обложки и началом
   текста. --blog-after-cover-gap — через переменную (задание): 52px на
   мобилке (диапазон 48-56px), 88px на ≥780px (диапазон 80-96px,
   переопределяется в media-запросе ниже) — НЕ density-scale (задание
   даёт абсолютные пиксельные диапазоны, не связанные с существующим
   density-множителем). */
:root {
  --blog-after-cover-gap: 52px;
}

.blog-article-header--cover {
  padding: 0 0 var(--blog-after-cover-gap);
}

.blog-hero {
  display: flex;
  flex-direction: column;
}

.blog-hero-text {
  padding: calc(32px * var(--density-scale)) calc(20px * var(--density-scale)) calc(24px * var(--density-scale));
  text-align: center;
}

.blog-hero-headline h1 {
  font-family: var(--blog-font-head);
  font-weight: 400;
  /* uppercase ТОЛЬКО тут, в CSS — текст в HTML (и значит в JSON-LD
     headline, см. buildBlogPostingJsonLd в blog-ssr.js) остаётся как
     автор его написал (задание, пункт 1). БЕЗ hyphens:auto/overflow-wrap:
     break-word (баг, найден и исправлен 2026-10-05, "слова не
     переносятся с дефисом") — целое слово уходит на следующую строку.
     text-wrap:balance остаётся. Clamp-нижняя граница (1.8rem) подобрана
     так, чтобы самое длинное слово реального заголовка статьи
     ("Статистика сторис Instagram: ссылка, геолокация, хэштег" — самое
     длинное слово "Статистика"/"геолокация", 10 букв) помещалось в
     колонку на 320px — расчёт на 320/390/768/1024/1440 см. в Итоге
     задания, запас с кириллическим шрифтом Cormorant Garamond найден
     достаточным без изменения самих clamp-значений. */
  text-transform: uppercase;
  font-size: clamp(1.8rem, 8vw, 2.6rem);
  line-height: 1.1;
  letter-spacing: 0.04em;
  text-wrap: balance;
  overflow-wrap: normal;
  word-break: normal;
  hyphens: none;
  margin: 0;
}

/* Мелкая строка-тизер ВНУТРИ самого хиро (kicker+дата, без префикса
   "Опубликовано" — компактно) — ЕДИНСТВЕННАЯ видимая дата в этой ветке
   (задание 2026-10-04, "дубли даты публикации" — строка метаданных под
   первым экраном с ещё одной датой убрана целиком, см. ниже).
   --blog-h1-meta-gap (задание 2026-10-05, п.1) — было 16px*density-scale
   (11.84px моб./13.92px десктоп), заметно близко к H1. 22px на мобилке
   (диапазон "не меньше 20px"), 36px на ≥780px (диапазон 32-40px,
   переопределяется в media-запросе ниже) — через переменную, НЕ
   density-scale (абсолютные пиксельные диапазоны задания, та же техника,
   что у --blog-after-cover-gap). */
:root {
  --blog-h1-meta-gap: 22px;
}

.blog-hero-meta {
  margin: var(--blog-h1-meta-gap) 0 0;
  display: flex;
  justify-content: center;
  align-items: center;
  gap: calc(14px * var(--density-scale));
  flex-wrap: wrap;
  font-family: var(--blog-font-ui);
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--blog-muted);
}

.blog-hero-meta .blog-kicker { margin: 0; }

/* БЕЗ aspect-ratio:4/5 + object-fit:cover (баг, найден и исправлен
   2026-10-04: кропало обложку) — мобилка: родная пропорция (height:auto).
   БЕЗ background (баг, найден и исправлен 2026-10-05: "цветная плашка под
   обложкой" — var(--blog-surface) красил ВЕСЬ контейнер картинки сплошным
   отличным от фона страницы цветом; теперь контейнер прозрачный, виден
   только настоящий фон страницы body/.app-content, как и просит задание). */
.blog-hero-image {
  background: transparent;
}

.blog-hero-image img {
  height: auto;
  display: block;
}

/* .blog-hero-metabar / .blog-hero-metabar-col / .blog-hero-metabar-label
   (строка метаданных "Рубрика/Дата публикации/Обновлено" под первым
   экраном) УДАЛЕНЫ целиком — задание 2026-10-04, "дубли даты публикации":
   её единственное информативное поле ("Дата публикации") дублировало дату
   уже показанную ВНУТРИ самого хиро (.blog-hero-meta выше), а "Рубрика"
   дублировала kicker там же — см. buildHeroMetabarHtml, убрана из
   src/lib/blog-ssr.js тем же заданием.

   ---------------------------------------------------------------- Лид */

/* БЕЗ border-bottom (баг, найден и исправлен 2026-10-05, задание п.2:
   "две линии подряд после лида") — лид (<p class="blog-lead">) и
   следующий за ним .blog-toc — ПРЯМЫЕ соседи в DOM (TOC вставляется сразу
   после первого абзаца, см. insertAfterFirstParagraph), у .blog-toc своя
   собственная border-top (рамка блока "Содержание", см. ниже) — вместе
   со своей это давало ДВЕ тонких линии почти подряд, с одним и тем же
   --blog-line, визуально читавшиеся как задвоение. Оставлена ОДНА линия
   — border-top у .blog-toc (реальная рамка его собственного блока,
   значимая сама по себе: совпадает с границей "Содержания", не просто
   декоративный разделитель) — она же и есть "разделитель перед первым
   H2" (в статьях БЕЗ TOC — <4 H2 — её роль в точности играет border-top
   самого первого H2, тоже НЕ трогался). padding-bottom/margin-bottom —
   те же значения, просто без видимой черты между ними. */
.blog-lead {
  font-family: var(--blog-font-body);
  font-style: italic;
  font-weight: 400;
  font-size: 1.3rem;
  line-height: 1.55;
  color: var(--blog-ink);
  padding-bottom: calc(20px * var(--density-scale));
  margin-bottom: calc(28px * var(--density-scale)) !important;
}

/* ---------------------------------------------------------------- Заголовки разделов */

/* H2 — БЕЗ капслока (сознательно, задание: "капслок только если читаемо
   на кириллице") — на пробных заголовках H2 этого блога ("Нажатия на
   геолокацию в сторис" и т.п., длиннее H1) капс на кириллице при размере
   H2 (меньше H1) заметно хуже читается, чем обычный регистр — решение в
   пользу читаемости, капслок остаётся только у H1 (крупнее и короче). */
/* scroll-margin-top — переход по якорю оглавления (TOC) или прямой
   ссылке #id (баг, найден и исправлен 2026-10-04): без него браузер
   подводит заголовок ВПЛОТНУЮ к верху вьюпорта, где его закрывает
   закреплённая шапка сайта (.topbar, position: sticky — не часть потока,
   не "сдвигает" остальной контент вниз сама по себе). h3 — тоже якорь
   (FAQ-вопросы ссылаются напрямую через #id, не только из TOC). +16px —
   небольшой зазор под шапкой, не впритык. */
.blog-article-body h2,
.blog-article-body h3 {
  scroll-margin-top: calc(var(--topbar-height) + 16px);
}

.blog-article-body h2 {
  font-family: var(--blog-font-head);
  font-weight: 400;
  font-size: clamp(1.6rem, 3.4vw, 2.1rem);
  line-height: 1.15;
  margin: calc(48px * var(--density-scale)) 0 calc(18px * var(--density-scale));
  padding-top: calc(28px * var(--density-scale));
  border-top: 1px solid var(--blog-line);
  text-wrap: balance;
  /* Явно (задание 2026-10-05) — ранее не задавалось вовсе, совпадало с
     браузерным default (manual/normal/normal), теперь зафиксировано, не
     полагаемся на default неявно. */
  overflow-wrap: normal;
  word-break: normal;
  hyphens: none;
}

.blog-article-body h2:first-of-type { margin-top: calc(32px * var(--density-scale)); }

/* Автонумерация ("01", "02"...) — CSS counter, НЕ меняет markdown/HTML
   заголовков (см. задание): counter-reset на .blog-article-body выше,
   counter-increment здесь. FAQ-заголовок ("Частые вопросы") — тоже H2,
   тоже нумеруется, сознательно (отдельного исключения не делаем — ровный
   счёт разделов статьи, FAQ такой же раздел, как остальные). */
.blog-article-body h2::before {
  counter-increment: blog-h2;
  content: counter(blog-h2, decimal-leading-zero);
  display: block;
  font-family: var(--blog-font-head);
  font-style: italic;
  font-weight: 400;
  font-size: 0.95rem;
  color: var(--blog-accent);
  margin-bottom: calc(6px * var(--density-scale));
  /* "01"/"02" — опорный номер раздела, не running-текст: lining+tabular
     (задание: "в... счётчиках lining-nums+tabular-nums"), а не
     наследуемый от .blog-article-body oldstyle-nums. */
  font-variant-numeric: lining-nums tabular-nums;
}

.blog-article-body h3 {
  font-family: var(--blog-font-head);
  font-style: italic;
  font-weight: 400;
  font-size: 1.3rem;
  line-height: 1.25;
  margin: calc(28px * var(--density-scale)) 0 calc(12px * var(--density-scale));
  /* Явно (задание 2026-10-05) — см. тот же комментарий у h2 выше. */
  overflow-wrap: normal;
  word-break: normal;
  hyphens: none;
}

/* ---------------------------------------------------------------- Списки */

/* margin-bottom 0.9em — тот же, что у абзацев (задание: "отступ списка
   сверху и снизу от соседнего текста оставить как у абзацев"; сверху
   отдельно ничего не задаём — margin-top:0 здесь + margin-bottom:0.9em у
   ПРЕДЫДУЩЕГО абзаца схлопываются в те же 0.9em сами, обычное поведение
   смежных блочных margin). */
.blog-article-body ul, .blog-article-body ol {
  margin: 0 0 0.9em;
  padding-left: 1.4em;
  list-style: none;
}

/* 0.25em (было 0.5em) — задание 2026-10-04, "между li... пустая строка",
   уменьшить минимум вдвое, ориентир 0.2-0.3em. line-height внутри пункта
   отдельно не задаём — наследуется от .blog-article-body (1.58), "как у
   абзацев" — задание. */
.blog-article-body ul > li, .blog-article-body ol > li {
  position: relative;
  margin-bottom: 0.25em;
}

/* Тонкое тире вместо точки — собственный маркер (см. задание), один и тот
   же знак для ul/ol: нумерованность в этом блоге нигде по смыслу не
   нужна (oглавление — отдельный .blog-toc с собственной нумерацией). */
.blog-article-body ul > li::before, .blog-article-body ol > li::before {
  content: '\2013';
  position: absolute;
  left: -1.4em;
  color: var(--blog-accent);
  font-weight: 600;
}

/* ---------------------------------------------------------------- Цитаты */

.blog-article-body blockquote {
  position: relative;
  margin: calc(32px * var(--density-scale)) 0;
  padding: 0 0 0 calc(28px * var(--density-scale));
  border-left: 2px solid var(--blog-accent);
  font-family: var(--blog-font-head);
  font-style: italic;
  font-size: 1.5rem;
  line-height: 1.4;
  color: var(--blog-ink);
}

.blog-article-body blockquote::before {
  content: '\201C';
  position: absolute;
  left: calc(-6px * var(--density-scale));
  top: -0.1em;
  font-family: var(--blog-font-head);
  font-size: 2.4em;
  line-height: 1;
  color: var(--blog-accent);
}

.blog-article-body blockquote p { margin: 0; }

/* Стандалон-абзац, целиком курсивом (p > em, и больше ничего в абзаце) —
   сноска/примечание (например про Meta) ИЛИ подпись к картинке сразу под
   ней (конвенция markdown — см. docs/blog-style-guide.md и
   docs/blog-article-template.md): тот же мелкий приглушённый вид для
   обоих случаев, никакого специального HTML не нужно — отличать их друг
   от друга в разметке незачем, выглядят одинаково уместно в обеих ролях. */
.blog-article-body p > em:only-child:first-child:last-child {
  display: block;
  font-family: var(--blog-font-body);
  font-size: 0.88rem;
  font-style: italic;
  color: var(--blog-muted);
  padding-top: calc(12px * var(--density-scale));
  border-top: 1px solid var(--blog-line);
  line-height: 1.5;
}

/* ---------------------------------------------------------------- Таблица */

.blog-table-wrap {
  overflow-x: auto;
  margin: calc(28px * var(--density-scale)) 0;
  /* Скроллится САМА обёртка, не страница целиком (см. задание) — тот же
     принцип, что у остальных overflow-контейнеров на сайте. */
}

.blog-article-body table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.95rem;
  /* Таблица — lining+tabular (задание), не наследуемый oldstyle от
     .blog-article-body: цифры в таблице должны выстраиваться строго по
     колонке, а не "гулять" по высоте/ширине, как в обычном тексте. */
  font-variant-numeric: lining-nums tabular-nums;
}

.blog-article-body th {
  font-family: var(--blog-font-ui);
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  text-align: left;
  color: var(--blog-muted);
  padding: calc(10px * var(--density-scale)) calc(14px * var(--density-scale));
  border-bottom: 1px solid var(--blog-ink);
  white-space: nowrap;
}

.blog-article-body td {
  padding: calc(10px * var(--density-scale)) calc(14px * var(--density-scale));
  border-bottom: 1px solid var(--blog-line);
}

/* ---------------------------------------------------------------- Картинки в теле */

.blog-article-body img {
  max-width: 100%;
  height: auto;
  border-radius: var(--radius-sm);
  display: block;
  margin: 0 auto;
}

/* ---------------------------------------------------------------- Секции и фото (v3, 2026-10-04) */

/* .blog-sec — одна секция статьи (markdown-токены, см. src/lib/
   blog-content.js: combineFigureTokens/buildArticleSections/
   renderBlogSectionToken). Каждая секция начинается с фото (.blog-fig,
   включая заголовок H2/H3 перед ним, если он есть) и тянется до
   следующего фото/конца статьи; секция без фото — просто текст, левая
   колонка пустая (см. media-запрос ниже). На мобилке (<780px) — обычный
   поток: фото на всю ширину, подпись под ним, текст дальше (задание,
   п.4) — никакого grid/sticky здесь, см. .blog-fig ниже. */
.blog-sec {
  margin: calc(32px * var(--density-scale)) 0;
}

.blog-sec:first-child { margin-top: 0; }

/* БЫЛО ≥780px-only (только у sticky-фото слева, задание 2026-10-04, п.3 —
   "верх фото вровень с линией секции"). Задание 2026-10-05, п.2 нашло ту
   же причину и на МОБИЛКЕ, просто в другом виде: H2, открывающий секцию
   (первый ребёнок .blog-sec-text), margin-top:0 здесь не задавался —
   margin-collapse между .blog-fig (margin-bottom 28px*scale) и H2
   (margin-top 48px*scale) брал МАКСИМУМ из двух, то есть H2-й margin
   (больший) и ПОБЕЖДАЛ — отсюда "слишком большой отступ после первого
   фото" (реальная причина, не сам margin-bottom у .blog-fig). Теперь
   margin-top:0 у ведущего H2 — универсальное правило (убрано из
   ≥780px-блока, см. комментарий там же) — эффективный отступ на мобилке
   после фото определяется ТОЛЬКО .blog-fig'ом ниже. */
.blog-sec-text > h2:first-child {
  margin-top: 0;
}

.blog-fig {
  /* position: static (дефолт) на мобилке — задание, п.4: явно, а не
     просто "не задано", чтобы в коде было видно, что sticky — ТОЛЬКО
     ≥780px (см. media-запрос), не случайный побочный эффект.
     margin-bottom 18px (было 28px*density-scale ≈ 20.72px на мобилке —
     задание 2026-10-05, п.2: "уменьшить отступ до текста под ним",
     ориентир 16-20px). ≥780px переопределяет margin:0 целиком (см.
     .blog-sec > .blog-fig в media-запросе), это значение там не действует
     вовсе — менять его можно, не трогая десктоп. */
  position: static;
  margin: 0 0 18px;
}

/* БЕЗ aspect-ratio/object-fit:cover (баг, найден и исправлен 2026-10-04:
   исходники — вертикальные скриншоты сторис, ~0.57, заметно "уже" 4:5 —
   cover обрезал их по бокам) — высота следует из РЕАЛЬНЫХ width/height
   картинки (HTML-атрибуты, см. buildImgTag в blog-content.js) при
   width:100%/height:auto, картинка показывается ЦЕЛИКОМ в родной
   пропорции. ≥780px (см. media-запрос ниже) добавляет max-height —
   пропорциональное уменьшение (width:auto+max-width/height:auto+
   max-height вместе — классический cross-browser приём "вписать без
   обрезки"), не кроп. */
.blog-fig img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: var(--radius-sm);
  background: var(--blog-surface);
}

.blog-fig figcaption {
  margin-top: calc(8px * var(--density-scale));
  font-family: var(--blog-font-ui);
  /* 12px — верхняя граница диапазона задания (11-12px у подписей) —
     ярлык (.blog-figure-label ниже) не задаёт свой отдельный font-size,
     читается тем же размером, что и остаток подписи, в одну строку. */
  font-size: 12px;
  line-height: 1.4;
  color: var(--blog-muted);
}

.blog-figure-label {
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--blog-ink);
  margin-right: calc(6px * var(--density-scale));
}

/* ---------------------------------------------------------------- Оглавление */

.blog-toc {
  border-top: 1px solid var(--blog-line);
  border-bottom: 1px solid var(--blog-line);
  padding: calc(22px * var(--density-scale)) 0;
  margin: calc(32px * var(--density-scale)) 0;
}

.blog-toc-title {
  font-family: var(--blog-font-ui);
  font-size: 0.72rem;
  font-weight: 600;
  /* 0.08-0.14em — диапазон задания (было 0.2em). */
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--blog-muted);
  margin: 0 0 calc(14px * var(--density-scale));
}

.blog-toc ul {
  list-style: none;
  margin: 0;
  padding: 0;
  counter-reset: blog-toc-item;
  columns: 1;
  column-gap: calc(32px * var(--density-scale));
}

.blog-toc li {
  counter-increment: blog-toc-item;
  margin-bottom: calc(8px * var(--density-scale));
  break-inside: avoid;
}

.blog-toc li::before {
  content: counter(blog-toc-item, decimal-leading-zero);
  font-family: var(--blog-font-head);
  font-style: italic;
  color: var(--blog-accent);
  margin-right: calc(8px * var(--density-scale));
  font-size: 0.9em;
  /* Опорный номер, не running-текст — lining+tabular (как и у счётчика
     H2 выше), не oldstyle. */
  font-variant-numeric: lining-nums tabular-nums;
}

/* БЕЗ подчёркивания вовсе (баг, найден и исправлен 2026-10-05, задание
   п.3) — раньше .blog-toc попадал в ту же группу селекторов, что и
   .blog-article-body a (ниже) — заполняющаяся на hover линия + цвет
   --blog-accent даже в состоянии покоя. Теперь — отдельное правило:
   цвет покоя --blog-ink (нейтральный, как у обычного текста оглавления),
   только цвет меняется на --blog-accent при hover/focus-visible, плавно
   (transition исключительно на color, без transform/подчёркивания в
   каком-либо виде). */
.blog-toc a {
  font-family: var(--blog-font-body);
  color: var(--blog-ink);
  text-decoration: none;
  transition: color 0.2s ease;
}

.blog-toc a:hover,
.blog-toc a:focus-visible {
  color: var(--blog-accent);
}

/* ---------------------------------------------------------------- Ссылки (подчёркивание-«заполнение») */

/* transform:scaleX только — НЕ background-position/width (запрещённые
   анимации, см. задание) — тонкая линия всегда видна (scaleX 0.4),
   "заполняется" до полной ширины на hover/focus только через transform,
   transition тоже только на transform. ТОЛЬКО .blog-article-body a —
   ссылки внутри основного текста статьи (задание 2026-10-05, п.3: "ссылки
   внутри основного текста статьи не менять"); .blog-toc убран из этой
   группы, см. выше. */
.blog-article-body a {
  position: relative;
  text-decoration: none;
  color: var(--blog-accent);
}

.blog-article-body a::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: -2px;
  height: 1px;
  background: currentColor;
  transform: scaleX(0.4);
  transform-origin: left;
  transition: transform 0.25s ease;
}

.blog-article-body a:hover::after, .blog-article-body a:focus-visible::after {
  transform: scaleX(1);
}

/* ---------------------------------------------------------------- Блоки услуг ("лукбук") */

/* Сетка с ЯВНЫМИ grid-row у каждого ребёнка (не авто-размещение) —
   предсказуемая раскладка "название слева сверху, цена+кнопка в правом
   столбце друг под другом" (см. задание), без риска, что auto-placement
   расставит элементы не в том порядке на разных браузерах. */
.blog-service-embed {
  border: 1px solid var(--blog-line);
  border-radius: var(--radius-sm);
  padding: calc(18px * var(--density-scale)) calc(20px * var(--density-scale));
  margin: calc(24px * var(--density-scale)) 0;
  display: grid;
  grid-template-columns: 1fr auto;
  column-gap: calc(18px * var(--density-scale));
  row-gap: calc(10px * var(--density-scale));
  align-items: center;
  transition: transform 0.2s ease, box-shadow 0.2s ease;
}

.blog-service-embed:hover {
  transform: translateY(-2px);
  box-shadow: var(--shadow-soft);
}

/* --blog-font-body (Source Serif 4), НЕ --blog-font-head (баг, найден и
   исправлен 2026-10-04: Cormorant Garamond — заголовочная гарнитура с
   выраженными засечками/"закорючками" в кириллице, плохо читается в
   мелком блочном UI-тексте вроде названия услуги — предназначена для
   H1/H2, не для такого контекста). Тот же шрифт, что и у running-текста
   статьи — просто другая переменная, без дублирования @font-face. */
.blog-service-name {
  font-family: var(--blog-font-body);
  font-weight: 400;
  font-size: 1.15rem;
  grid-row: 1;
  grid-column: 1 / 2;
  text-transform: none;
  letter-spacing: 0;
}

/* --blog-font-body, НЕ --blog-font-ui/Inter (задание 2026-10-04: "Inter не
   использовать" для цены) — та же причина, что у .blog-service-name выше,
   просто другой изначально выбранный шрифт у того же бага. lining+tabular
   (задание: "в карточках услуг (цены)...") работают одинаково в обеих
   гарнитурах — свойство цифр, не шрифта, не теряется при смене семейства. */
.blog-service-price {
  font-family: var(--blog-font-body);
  font-weight: 600;
  font-size: 1.05rem;
  color: var(--blog-accent);
  grid-row: 1;
  grid-column: 2 / 3;
  justify-self: end;
  white-space: nowrap;
  font-variant-numeric: lining-nums tabular-nums;
}

.blog-service-link, .blog-service-quantity {
  font-family: var(--blog-font-ui);
  font-size: 0.85rem;
  padding: calc(7px * var(--density-scale)) calc(10px * var(--density-scale));
  border: 1px solid var(--blog-line);
  border-radius: var(--radius-sm);
  background: var(--blog-bg);
  color: var(--blog-ink);
  box-sizing: border-box;
}

.blog-service-link { grid-row: 2; grid-column: 1 / 2; width: 100%; }
.blog-service-quantity { grid-row: 2; grid-column: 2 / 3; width: 100%; max-width: 110px; }

/* Кнопка контурная, инверсия на hover (только background/color, не layout) —
   переиспользует .btn-primary из style.css (класс в разметке не менялся,
   см. ADD_TO_CART_SCRIPT в blog-ssr.js), здесь только блоговое
   переопределение внешнего вида этой конкретной кнопки. */
.blog-service-embed .btn-primary {
  grid-row: 3;
  grid-column: 2 / 3;
  justify-self: end;
  width: auto;
  background: transparent;
  color: var(--blog-accent);
  border: 1px solid var(--blog-accent);
  border-radius: var(--radius-sm);
  padding: calc(8px * var(--density-scale)) calc(18px * var(--density-scale));
  /* --blog-font-body, НЕ --blog-font-ui/Inter (задание 2026-10-04, та же
     причина, что у .blog-service-name/-price выше). */
  font-family: var(--blog-font-body);
  font-size: 0.8rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  transition: background 0.2s ease, color 0.2s ease;
}

.blog-service-embed .btn-primary:hover {
  background: var(--blog-accent);
  color: var(--blog-bg);
}

.blog-service-add-status {
  grid-row: 3;
  grid-column: 1 / 2;
  justify-self: start;
  font-family: var(--blog-font-ui);
  font-size: 0.8rem;
  color: var(--blog-muted);
}

/* ---------------------------------------------------------------- FAQ */

/* H2 "Частые вопросы"/H3-вопросы уже оформлены общими правилами выше
   (нумерация H2, курсивный serif H3) — здесь только тонкий разделитель
   МЕЖДУ парами вопрос-ответ, чтобы раздел не слипался в один абзац. */
.blog-article-body h3 + p,
.blog-article-body h3 ~ ul,
.blog-article-body h3 ~ ol {
  margin-bottom: calc(20px * var(--density-scale));
}

.blog-article-body h3 {
  padding-top: calc(16px * var(--density-scale));
  border-top: 1px solid var(--blog-line);
}

.blog-article-body h2 + h3 { border-top: none; padding-top: 0; }

/* ---------------------------------------------------------------- Подвал статьи */

.blog-article-footer {
  margin-top: calc(48px * var(--density-scale));
  padding-top: calc(20px * var(--density-scale));
  border-top: 1px solid var(--blog-line);
  text-align: center;
}

/* Без рамки/заливки — у "← Все статьи" их не было и раньше (задание
   2026-10-05, п.3: "если у неё есть рамка/заливка, сохранить" — не было,
   поэтому не добавляю новую). Подчёркивания тоже не было (текст
   наследовал глобальный a{text-decoration:none} из style.css, в группу
   .blog-article-body a/.blog-toc a этот футер-линк не входил никогда) —
   только явно прописан цвет покоя + переход, чтобы hover/focus меняли
   ИМЕННО цвет текста, плавно, без transform. */
.blog-article-footer a {
  font-family: var(--blog-font-ui);
  font-size: 0.8rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--blog-ink);
  text-decoration: none;
  transition: color 0.2s ease;
}

.blog-article-footer a:hover,
.blog-article-footer a:focus-visible {
  color: var(--blog-accent);
}

/* ---------------------------------------------------------------- /blog/cart (вне фикуса этой задачи) */

/* Корзина блога (/blog/cart) — ВНЕ задания ("только /blog и /blog/:slug,
   плюс Preview"), не переоформлялась. Правила перенесены буквально
   (байт-в-байт) из старого инлайн-<style> (BLOG_CONTENT_STYLE в
   blog-ssr.js, теперь удалён — blog.css подключается на ВСЕХ страницах
   блога, включая /blog/cart, см. renderPage) — внешний вид корзины не
   должен измениться ни на пиксель. --color-border/--color-surface/
   --color-text — переменные ОБЩЕГО style.css (он тоже подключён на
   /blog/cart), не --blog-*, сознательно: это копия прежнего поведения,
   не часть новой дизайн-системы блога. */
.blog-cart-table { width: 100%; border-collapse: collapse; margin-bottom: 16px; }
.blog-cart-table th, .blog-cart-table td { text-align: left; padding: calc(8px * var(--density-scale)); border-bottom: 1px solid var(--color-border); }
.blog-cart-link-input { width: 100%; padding: 6px 8px; border: 1px solid var(--color-border); border-radius: var(--radius-sm); background: var(--color-surface); color: var(--color-text); }
.blog-cart-qty-input { width: 80px; padding: 6px 8px; border: 1px solid var(--color-border); border-radius: var(--radius-sm); background: var(--color-surface); color: var(--color-text); }
.blog-cart-total { font-weight: 700; font-size: 1.1rem; margin-bottom: 16px; }
#blog-cart-checkout-btn { width: auto; padding: calc(10px * var(--density-scale)) calc(24px * var(--density-scale)); }
#blog-cart-checkout-result { margin-top: 12px; }

/* ---------------------------------------------------------------- Полоса прогресса чтения */

/* top: var(--topbar-height), НЕ 0 (баг, найден и исправлен 2026-10-04) —
   при top:0 полоса рисовалась НАД шапкой (z-index:500 против .topbar
   z-index:10, та же координата top:0, что и у sticky-шапки) — тонкая
   цветная линия перекрывала верхние 2px закреплённой шапки сайта. Сама
   шапка при этом механически оставалась sticky (ancestor-цепочка
   #blog-header-mount -> .app-shell -> #app -> body проверена — ни
   overflow, ни transform нигде не выставлены ни в style.css, ни в
   blog.css) — именно эта полоса и была единственной найденной причиной
   визуального конфликта "шапка как будто не на месте". */
.blog-progress-bar {
  position: fixed;
  top: var(--topbar-height);
  left: 0;
  right: 0;
  height: 2px;
  background: transparent;
  z-index: 5;
}

.blog-progress-bar-fill {
  height: 100%;
  width: 100%;
  background: var(--blog-accent);
  transform: scaleX(0);
  transform-origin: left;
  /* Сам scaleX обновляется из blog-fx.js через requestAnimationFrame —
     эта transition только смягчает между кадрами скролла, не отдельная
     декоративная анимация. */
  transition: transform 0.1s linear;
}

/* ---------------------------------------------------------------- Плавное появление (reveal) */

/* .blog-reveal добавляется ТОЛЬКО скриптом (public/js/blog-fx.js) — в
   серверной разметке этого класса нет вообще (см. test/blog-ssr.test.js:
   "reveal-классы не присутствуют в серверном HTML"), поэтому без JS/для
   краулеров контент всегда виден сразу, без каких-либо условий. */
.blog-reveal {
  opacity: 0;
  transform: translateY(16px);
  transition: opacity 0.6s ease, transform 0.6s ease;
}

.blog-reveal.is-visible {
  opacity: 1;
  transform: none;
}

/* ---------------------------------------------------------------- Фокус-стили */

.blog-article-body a:focus-visible,
.blog-toc a:focus-visible,
.blog-service-embed .btn-primary:focus-visible,
.blog-service-link:focus-visible,
.blog-service-quantity:focus-visible,
.blog-article-footer a:focus-visible,
.blog-card-link:focus-visible {
  outline: 2px solid var(--blog-accent);
  outline-offset: 2px;
}

/* ---------------------------------------------------------------- Список статей /blog */

.blog-index-header {
  text-align: center;
  padding: calc(28px * var(--density-scale)) 0 calc(36px * var(--density-scale));
}

.blog-index-header h1 {
  font-family: var(--blog-font-head);
  font-weight: 400;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  font-size: clamp(2.2rem, 6vw, 3.4rem);
  line-height: 1.1;
  margin: 0;
  /* БЕЗ hyphens:auto/overflow-wrap:break-word (баг, найден и исправлен
     2026-10-05, тот же, что у .blog-article-header h1/.blog-hero-headline
     h1 выше) — целое слово уходит на следующую строку, не разрывается. */
  text-wrap: balance;
  overflow-wrap: normal;
  word-break: normal;
  hyphens: none;
}

/* Подзаголовок-description под H1 (задание, п.2) — узкий, приглушённый,
   не конкурирует с H1 за внимание; max-width — не во всю ширину страницы
   (на десктопе колонка статей/карточек и так ограничена .blog-page-content
   max-width), короткая строка читается как подзаголовок, не как второй
   абзац. */
.blog-index-subtitle {
  font-family: var(--blog-font-body);
  font-style: italic;
  font-size: 1rem;
  line-height: 1.5;
  color: var(--blog-muted);
  max-width: 640px;
  margin: calc(16px * var(--density-scale)) auto 0;
}

/* Сетка без "лесенки" (2026-10-04, задание): ровно N колонок одинаковой
   ширины (CSS Grid, без featured-карточки/масштабирования/смещений) —
   каждая карточка одного вида и размера, выравнена по верху строки
   (align-items: start — ВЫСОТА description у всех разная из-за
   line-clamp, но верхний край картинки/даты/заголовка у всех карточек
   одной строки должен совпадать, не "плыть"). Нечётная последняя
   карточка при auto-placement CSS Grid САМА остаётся в левой колонке и
   не растягивается — это НЕ требует отдельного правила (span задавался
   бы явно, здесь его нет вовсе). */
.blog-article-grid {
  list-style: none;
  margin: 0 auto;
  padding: 0;
  max-width: 1040px;
  display: grid;
  grid-template-columns: 1fr;
  align-items: start;
  /* 32-48px по горизонтали / 48-64px по вертикали (задание) — на <780px
     (одна колонка) column-gap роли не играет, остаётся только row-gap. */
  column-gap: calc(40px * var(--density-scale));
  row-gap: calc(56px * var(--density-scale));
}

.blog-card-link {
  display: block;
  text-decoration: none;
  color: var(--blog-ink);
  /* Лёгкий подъём всей карточки на hover (не только картинки) — задание
     "мягкий hover: подъём/масштаб картинки" — transform только, без
     теней/blur. */
  transition: transform 0.2s ease;
}

.blog-card-link:hover { transform: translateY(-2px); }

/* БЕЗ единой пропорции 4:5 (баг, найден и исправлен 2026-10-04: кропало
   og-картинку в карточке) — у РЕАЛЬНОЙ обложки своя родная пропорция (см.
   .blog-card-image img ниже, та же техника width:100%/height:auto, что и
   у .blog-fig img/.blog-hero-image img); .blog-article-grid уже
   align-items:start (см. выше) — карточки в ряду по 2 (≥780px, разные
   высоты картинок) выравниваются по верху САМИ, без доп. CSS, когда
   элементы грида не растягиваются на всю высоту строки. У плашки-без-
   обложки (см. .blog-card-image--placeholder ниже) своя фиксированная
   4:5 остаётся — ей неоткуда взять родную пропорцию. */
.blog-card-image {
  overflow: hidden;
  border-radius: var(--radius-sm);
  background: var(--blog-surface);
  margin-bottom: calc(14px * var(--density-scale));
  transition: transform 0.3s ease;
}

.blog-card-image img { width: 100%; height: auto; display: block; }

.blog-card-link:hover .blog-card-image { transform: scale(1.015); }

/* Типографская плашка — у статьи нет og-картинки (задание, п.3): тот же
   тёплый фон, заголовок крупно по центру — своя ФИКСИРОВАННАЯ пропорция
   4:5 (в отличие от реальной обложки выше, задание 2026-10-04) — у плашки
   нет исходного изображения, чтобы взять родную пропорцию откуда-то ещё,
   нужен хоть какой-то заданный размер карточки. line-clamp — на случай
   очень длинного заголовка, чтобы плашка не "раздулась" выше своих же
   соседей по строке. */
.blog-card-image--placeholder {
  aspect-ratio: 4 / 5;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: calc(20px * var(--density-scale));
  box-sizing: border-box;
}

.blog-card-image--placeholder span {
  font-family: var(--blog-font-head);
  font-weight: 400;
  font-size: clamp(1.1rem, 4vw, 1.6rem);
  line-height: 1.25;
  text-align: center;
  color: var(--blog-ink);
  display: -webkit-box;
  -webkit-line-clamp: 5;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* .blog-card-date удалён целиком (задание 2026-10-05, п.3: "убрать дату в
   карточках статей") — класс больше нигде не рендерится (см.
   renderBlogCardHtml в blog-ssr.js), title (ниже) уже margin-top:0 —
   никакого пустого блока/отступа после удаления не остаётся. */
.blog-card-title {
  position: relative;
  /* display:block (по умолчанию у h2) — НЕ inline-block: заголовок
     карточки может переноситься на 2+ строки (длинные заголовки, см.
     задание "без переполнения"), а ::after ниже рисует ОДНУ линию под
     ВСЕМ блоком заголовка целиком (не под каждой строкой отдельно) —
     тот же сознательный приём, что у редакционных сайтов: один акцентный
     штрих под всей шапкой карточки, независимо от переноса строк. */
  font-family: var(--blog-font-head);
  font-weight: 400;
  font-size: 1.4rem;
  line-height: 1.25;
  margin: 0 0 calc(8px * var(--density-scale));
  padding-bottom: calc(4px * var(--density-scale));
  text-wrap: balance;
  /* БЕЗ hyphens:auto/overflow-wrap:break-word (задание 2026-10-05,
     "названия в карточках /blog" — та же страховка от переполнения, что
     и у H1/H2/H3 выше, явно без разрыва слова). */
  overflow-wrap: normal;
  word-break: normal;
  hyphens: none;
}

/* Подчёркивание-«заполнение» заголовка на hover — тот же приём, что у
   ссылок в тексте статьи (transform:scaleX, не background-position) —
   задание: "подчёркивание заголовка", не просто смена цвета. */
.blog-card-title::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: -2px;
  height: 1px;
  background: var(--blog-accent);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.25s ease;
}

.blog-card-link:hover .blog-card-title,
.blog-card-link:focus-visible .blog-card-title {
  color: var(--blog-accent);
}
.blog-card-link:hover .blog-card-title::after,
.blog-card-link:focus-visible .blog-card-title::after {
  transform: scaleX(1);
}

.blog-card-desc {
  font-family: var(--blog-font-body);
  font-size: 0.95rem;
  color: var(--blog-muted);
  line-height: 1.5;
  margin: 0;
  font-variant-numeric: oldstyle-nums;
  /* Не больше 2 строк — карточка не должна "разъезжаться" по высоте от
     длины description конкретной статьи. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.blog-empty {
  text-align: center;
  font-family: var(--blog-font-body);
  font-style: italic;
  color: var(--blog-muted);
  padding: calc(40px * var(--density-scale)) 0;
}

/* ---------------------------------------------------------------- Брейкпоинт (780px, единственный на сайте) */

@media (min-width: 780px) {
  /* 88px (диапазон 80-96px, задание 2026-10-05, п.4) — больше, чем на
     мобилке (52px), не просто density-scale мобильного значения. 36px
     (диапазон 32-40px, задание 2026-10-05, п.1) — тот же принцип у
     --blog-h1-meta-gap. */
  :root {
    --blog-after-cover-gap: 88px;
    --blog-h1-meta-gap: 36px;
  }

  .blog-toc ul {
    columns: 2;
  }

  /* Ровно 2 колонки, без featured-карточки/нечётных отступов (задание
     2026-10-04 "без лесенки") — repeat(2, minmax(0, 1fr)) вместо "1fr 1fr"
     чисто для явности (minmax(0,...) — та же защита от переполнения
     длинным контентом карточки, что и у .blog-sec ниже). */
  .blog-article-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  /* Обложка в карточке — не шире колонки фото секций (задание 2026-10-05,
     п.1). Карточная сетка (.blog-article-grid, max-width:1040px, 2
     колонки) — СОВЕРШЕННО ДРУГОЙ grid-контейнер, чем .blog-sec/.blog-hero
     (max-width:1100px) — у них разные ширины и разное число колонок,
     поэтому "та же формула minmax()" тут не даёт пиксель-в-пиксель
     идентичную ширину на любом экране (в отличие от обложки на первом
     экране статьи выше, где контейнер buквально тот же самый). 420px —
     верхняя граница того, во что обычно резолвится узкая колонка
     .blog-sec (minmax(100px,1fr) при тексте-колонке 640px) на типичных
     десктопных/iPad-ландшафт ширинах (1024-1440px) — карточка гарантированно
     "не шире", с запасом, а не теоретическое приближение. */
  .blog-card-image {
    max-width: 420px;
    margin-inline: auto;
  }
  .blog-card-image--placeholder {
    max-width: none;
    margin-inline: 0;
  }

  /* ---------------------------------------------- Макет v2 (2026-10-03): шире под секции */

  /* 1100px — не 720px: нужно место под левую колонку фото + читательскую
     колонку ~560-680px у каждой секции разом (см. .blog-sec ниже; сам
     .blog-article-content — простая обёртка-поток для списка секций, без
     собственного grid, см. v3 2026-10-04). Заголовок/старый hero/подвал
     просто получают больше воздуха по бокам — ни один из них не ломается
     от более широкого родителя (сами center/max-width у их собственных
     элементов не менялись, задание "без cover: прежний вариант" касается
     разметки/атрибутов <img>, не пиксельной ширины контейнера). */
  .blog-article-body {
    max-width: 1100px;
    /* 18-19px на десктопе (задание) — мобильный размер см. у самого
       .blog-article-body выше (17.5px, <780px). */
    font-size: 18.5px;
  }

  /* Сплит-хиро — на весь экран (минус шапка сайта, --topbar-height уже
     объявлена в style.css:root, см. .topbar). БЫЛО 1fr 1fr (50/50, баг,
     найден и исправлен 2026-10-05: "обложка заметно крупнее фото в
     разделах") — колонки теперь ТЕ ЖЕ формулы, что у .blog-sec ниже
     (minmax(100px,1fr) + minmax(0,640px), тот же column-gap), просто
     ЗЕРКАЛЬНО (текст — широкая 640px-колонка слева, фото — узкая 1fr-
     колонка справа, а не наоборот, как у .blog-sec — т.к. обложка
     остаётся СПРАВА, задание этого не меняло). Тот же контейнер
     (.blog-article-body, max-width:1100px и там, и там) + одинаковые
     формулы колонок = одинаковая итоговая ширина колонки фото на любой
     ширине экрана ≥780px — именно "ширина равна ширине колонки фото
     секций", не приближение. */
  .blog-hero {
    display: grid;
    grid-template-columns: minmax(0, 640px) minmax(100px, 1fr);
    column-gap: clamp(20px, 3vw, 48px);
    min-height: calc(100svh - var(--topbar-height));
    align-items: stretch;
  }

  .blog-hero-text {
    display: flex;
    flex-direction: column;
    /* space-between — H1 (во flex-обёртке .blog-hero-headline с flex:1,
       центрирующей его по вертикали САМА) прижимается кверху доступного
       пространства, а мелкая строка kicker+дата — к низу колонки (задание,
       п.1: "внизу колонки мелкая строка"), без position:absolute. */
    justify-content: space-between;
    align-items: center;
    padding: calc(56px * var(--density-scale)) calc(48px * var(--density-scale));
    box-sizing: border-box;
    height: 100%;
    text-align: center;
  }

  .blog-hero-headline {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
  }

  .blog-hero-headline h1 {
    font-size: clamp(1.7rem, 3.4vw, 2.6rem);
    max-width: 440px;
  }

  /* Правая колонка первого экрана (баг, найден и исправлен 2026-10-04:
     было width:100%/height:100%/object-fit:cover — full-bleed кроп на всю
     половину экрана). Теперь .blog-hero-image СТРЕТЧИТСЯ на всю высоту
     строки грида (height:100%, как раньше) — это и есть "фон страницы"
     под обложкой: сам <img> внутри больше НЕ растягивается на всю эту
     высоту, вписывается по высоте экрана (max-height, width:auto),
     остаётся родная пропорция.

     ЦЕНТРИРОВАНИЕ (баг, найден и исправлен 2026-10-05, задание п.1):
     БЫЛО — обычный block-flow img "прилипал" к верхнему краю колонки
     (единственное, что делает block-уровневый элемент сам по себе), а H1
     в СОСЕДНЕЙ колонке центрирован по вертикали (.blog-hero-headline,
     flex:1 + align-items:center) — обложка и заголовок оказывались на
     разных уровнях, перекос. Теперь .blog-hero-image — flex-контейнер
     (align-items/justify-content: center) — картинка центрируется И по
     вертикали, И по горизонтали внутри всей высоты колонки (той же
     высоты, что и у H1 в соседней колонке — обе колонки одной строки
     грида .blog-hero, высота определяется min-height:100svh-topbar
     контейнера), центр обложки и центр H1 физически на одном уровне.
     Размер (max-width/max-height) не менял — только выравнивание. */
  .blog-hero-image {
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
  }

  .blog-hero-image img {
    width: auto;
    max-width: 100%;
    height: auto;
    max-height: calc(100svh - var(--topbar-height));
  }

  /* Секции (v3, 2026-10-04) — КАЖДАЯ .blog-sec — grid из РОВНО 2 ячеек:
     [.blog-fig (колонка 1)] [.blog-sec-text (колонка 2, ВЕСЬ остальной
     контент секции целиком одним блоком — абзацы/заголовки/списки/
     таблицы/оглавление/карточки услуг/FAQ, см. задание п.2)]. Секция без
     фото — .blog-fig просто нет, .blog-sec-text всё равно встаёт в
     колонку 2, колонка 1 пустая (задание: "левая пустая").

     ПОЧЕМУ одна ячейка на весь текст, а не grid-row-span у фигуры на
     несколько строк: у CSS Grid без явного grid-template-rows (а здесь
     его и нет — число строк заранее не известно) запись "grid-row: 1/-1"
     формально считает -1 от последней ЯВНОЙ строки грида, которых 0 — то
     есть span до implicit-строк НЕ гарантирован спецификацией во всех
     браузерах (хотя на практике большинство современных так и делают).
     Чтобы не зависеть от этой тонкости вовсе — ОБА столбца получают
     ровно ОДНУ ячейку (одну строку), а текст внутри неё просто растёт
     как обычный блок (несколько абзацев друг под другом ВНУТРИ
     .blog-sec-text, не отдельными строками грида) — высота строки грида
     автоматически равна высоте текста (он почти всегда выше фото), и
     sticky у фигуры (ниже) работает относительно ИМЕННО этой высоты. */
  .blog-sec {
    display: grid;
    grid-template-columns: minmax(100px, 1fr) minmax(0, 640px);
    column-gap: clamp(20px, 3vw, 48px);
    align-items: start;
  }

  /* ">" — прямой потомок .blog-sec (реальный grid-item), не любой
     .blog-fig в документе: если в секции когда-нибудь окажется ВТОРОЕ
     фото (см. renderBlogSectionToken — на практике не бывает, но
     defensively), оно рендерится ВНУТРИ .blog-sec-text, не является
     прямым потомком .blog-sec — grid-column/row там браузер просто
     проигнорирует (это не grid-item того грида), и sticky на НЕМ не
     навешивается вовсе, то есть не "уезжает" поверх текста непредсказуемо. */
  .blog-sec > .blog-fig {
    grid-column: 1;
    grid-row: 1;
    align-self: start;
    margin: 0;
    /* Задание п.2: шапка сайта + 24-32px (середина диапазона — 28px). */
    position: sticky;
    top: calc(var(--topbar-height) + 28px);
  }

  .blog-sec-text {
    grid-column: 2;
    grid-row: 1;
  }

  /* Верх фото совпадает с линией секции (border-top у ведущего H2, задание
     п.3, баг найден и исправлен 2026-10-04) — H2, открывающий секцию,
     получает margin-top:0 (см. .blog-sec-text > h2:first-child — теперь
     БАЗОВОЕ правило, не только ≥780px, см. ниже), так что его
     border-top-линия оказывается у САМОГО верха .blog-sec-text — ровно
     там же, где верх .blog-fig (align-items:start, grid-row:1, margin:0 у
     обоих). top у sticky (28px над топбаром) — та же величина, что уже
     используется шапкой секции, поэтому и в закреплённом состоянии фото
     не вылезает выше линии/под шапку сайта. */

  /* Ограничивает высоту фото+подписи так, чтобы ВСЕГДА помещались в
     экран (задание): 100svh минус верхний отступ (топбар+28px, тот же,
     что у sticky top выше, иначе фото прилипло бы, упираясь НИЖНИМ краем
     в край экрана), минус ~60px на подпись (оценка — 1-2 строки 12px
     текста + отступ сверху, см. .blog-fig figcaption; подпись физически
     остаётся ПОД картинкой внутри того же sticky-блока, не обрезается)
     минус 48px свободного запаса снизу. width:auto+max-width:100% вместе
     с height:auto+max-height (баг, найден и исправлен 2026-10-04: было
     width:100%+object-fit:cover — обрезало по высоте; теперь фото
     ПРОПОРЦИОНАЛЬНО уменьшается, если не влезает по высоте — пустых
     боковых полей при этом не возникает, т.к. object-fit:cover раньше
     использовался именно ради заполнения колонки, а не ради сохранения
     пропорций, что и было претензией задания). */
  .blog-sec > .blog-fig img {
    width: auto;
    max-width: 100%;
    height: auto;
    max-height: calc(100svh - var(--topbar-height) - 28px - 60px - 48px);
  }
}

@media (max-width: 779px) {
  /* flex-column, не grid — на мобильном это просто порядок детей сверху
     вниз (название, цена, ссылка, количество, кнопка, статус), без
     горизонтальных пар: явные grid-row/grid-column из десктопных правил
     выше (две колонки) иначе накладывались бы друг на друга в одну
     колонку. Меньше вертикальных отступов (gap), чтобы 7 карточек подряд
     (см. docs/blog-style-guide.md, кейс статьи про статистику Stories) не
     были тяжёлыми на телефоне. */
  .blog-service-embed {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: calc(8px * var(--density-scale));
  }

  .blog-service-price { align-self: flex-start; }
  .blog-service-embed .btn-primary,
  .blog-service-add-status {
    align-self: center;
  }
  .blog-service-embed .btn-primary { text-align: center; width: 100%; }

  /* Фото в секциях статьи (задание 2026-10-04) — было на всю ширину
     колонки (.blog-fig img { width: 100% }, базовое правило выше), на
     телефоне/iPad портрет смотрелось слишком крупно, во весь экран.
     Уменьшено в 1.5 раза: calc(100% / 1.5) ≈ 67% ширины текстовой
     колонки, по центру (margin-inline: auto). Родная пропорция (height:
     auto из базового правила .blog-fig img выше) и скругления
     (border-radius оттуда же) не трогаются — переопределяется ТОЛЬКО
     ширина/центрирование. ≥780px (sticky-фото слева, см. .blog-sec >
     .blog-fig img) — отдельная полностью независимая ветка CSS, этот
     блок её не видит вовсе (media-запрос с противоположной границей). */
  .blog-fig img {
    width: calc(100% / 1.5);
    margin-inline: auto;
  }

  /* Подпись — та же ширина/центрирование, что и у фото (задание:
     "подпись выравнивается по ширине фото") — без этого подпись осталась
     бы во всю ширину колонки, визуально "шире" уменьшенного фото над ней. */
  .blog-fig figcaption {
    width: calc(100% / 1.5);
    margin-inline: auto;
  }

  /* Обложка (.blog-hero-image, первый экран статьи) — ТА ЖЕ ширина/
     центрирование, что у фото секций (задание 2026-10-05, п.1: "на
     мобилке ≈67% ширины колонки, по центру" — обложка была заметно
     крупнее фото в разделах). */
  .blog-hero-image img {
    width: calc(100% / 1.5);
    margin-inline: auto;
  }

  /* Обложка в карточке /blog — та же логика (задание 2026-10-05, п.1):
     карточка (и фон-плейсхолдер img загрузки, и сама картинка — width:
     100% у img наследует от родителя, см. .blog-card-image img выше)
     уменьшена целиком, центрирована — без этого фон-плейсхолдер
     container'а торчал бы шире уменьшенной картинки. Плашка-без-обложки
     (.blog-card-image--placeholder) — сброс обратно в полную ширину, у
     неё своя фиксированная 4:5-пропорция, не фото, не затрагивается
     заданием. */
  .blog-card-image {
    width: calc(100% / 1.5);
    margin-inline: auto;
  }
  .blog-card-image--placeholder {
    width: 100%;
    margin-inline: 0;
  }

  /* ---------------- Заголовки крупнее + узкая колонка текста (задание 2026-10-04) */

  /* H2 ×1.5 (задание, п.4, ТОЛЬКО мобилка): текущее фактическое значение
     на мобилке — 1.6rem (clamp(1.6rem, 3.4vw, 2.1rem) у базового правила
     выше резолвится в свой МИНИМУМ на любой ширине уже 753px, то есть на
     всех реальных телефонах/iPad-портрете) × 1.5 = 2.4rem. Плоское
     значение, не новый clamp — на такой узкой колонке (±780px) vw-
     компонента не даёт заметной пользы, задание прямо просит "× 1.5 от
     текущего", не новую формулу. line-height 1.2 (было 1.15) — "чтобы не
     разваливались на 3-4 строки": при более крупном кегле в той же узкой
     колонке длинные русские H2 чаще переносятся на несколько строк,
     1.15 между ними выглядело бы слишком плотно. Нумерация "01"/"02"
     (h2::before) НЕ трогается — своё правило, отдельный font-size.
     H3 НЕ увеличен (задание: "не менять"; допуск до +15% оставлен на
     усмотрение, если на реальном экране разрыв с укрупнённым H2 будет
     смотреться несоразмерно — см. Итог задания). */
  .blog-article-body h2 {
    font-size: 2.4rem;
    line-height: 1.2;
  }

  /* --blog-gutter-head — текущий боковой отступ (16px * density-scale,
     тот же, что у .app-content в style.css) — заголовки ОСТАЮТСЯ на нём
     (ничего ниже их не трогает, они продолжают наследовать его от
     .app-content как и раньше). --blog-gutter-text = ×3 — задание,
     "основной текст получает 3×G с каждой стороны", через переменные,
     чтобы множитель было легко поменять в одном месте. */
  :root {
    --blog-gutter-head: calc(16px * var(--density-scale));
    --blog-gutter-text: calc(var(--blog-gutter-head) * 3);
    /* Задание 2026-10-05: "линия над фото, не под ним" — отступы между
       линией секции и фото (24-32px) и между фото и номером раздела
       (20-24px), см. .blog-sec--has-fig/.blog-fig ниже. */
    --blog-line-fig-gap: 28px;
    --blog-fig-number-gap: 20px;
  }

  /* Задание 2026-10-05: "линия секции должна стоять НАД фото, а не под
     ним". Причина бага: линия — это border-top САМОГО H2 (базовое правило
     .blog-article-body h2 выше, действует на всех ширинах), а H2 лежит
     внутри .blog-sec-text, которая на мобилке (обычный block-поток, без
     grid — см. комментарий у .blog-sec выше) идёт ПОСЛЕ .blog-fig, потому
     что renderBlogSectionToken (blog-content.js) всегда рендерит fig перед
     .blog-sec-text в DOM независимо от порядка в markdown. На ≥780px это
     не проблема — там .blog-sec-text отдельная grid-колонка РЯДОМ с фото,
     а не "после" него в потоке (см. media-запрос ≥780px, там же и
     объяснение, почему верх фото специально подведён под эту линию).

     Фикс — перенос линии на уровень .blog-sec (модификатор
     .blog-sec--has-fig, добавляется только секциям с фото, см.
     renderBlogSectionToken): border-top + padding-top на самой секции,
     перед фото. Дубль убран — у H2 внутри такой секции своя border-top/
     padding-top снимаются, линия не удваивается. Секции БЕЗ фото этот
     модификатор не получают вовсе — у них линия остаётся ровно там же,
     где была (border-top самого H2), включая вступление после лида (у
     него нет собственного H2-раздела) и FAQ (его H2 не отличается от
     остальных, модификатор не навешивается, т.к. в разделе нет фото). */
  .blog-sec--has-fig {
    border-top: 1px solid var(--blog-line);
    padding-top: var(--blog-line-fig-gap);
  }

  .blog-sec--has-fig > .blog-sec-text > h2:first-child {
    border-top: none;
    padding-top: 0;
  }

  /* Отступ от фото до номера раздела ("01"/"02", h2::before) — было
     flat 18px (margin-bottom у .blog-fig, базовое правило выше), теперь
     через переменную (задание, п.2: 20-24px). */
  .blog-fig {
    margin-bottom: var(--blog-fig-number-gap);
  }

  /* padding-inline — ДОБАВОЧНЫЙ отступ поверх уже унаследованного G от
     .app-content (не вместо него): G (уже есть) + 2G (добавлено здесь) =
     3G итоговых с каждой стороны, ровно --blog-gutter-text. Список целей —
     буквально из задания ("абзацы, списки, blockquote/pull-quote,
     примечания" — последнее, сноски вида `_текст_`, это обычный <p><em>>,
     той же разметки, что и остальные абзацы, отдельного класса нет,
     общий селектор `p` уже покрывает). Явно НЕ трогаем: фото секций
     (.blog-fig), обложку (.blog-hero-image), таблицы (.blog-table-wrap),
     карточки услуг (.blog-service-embed — там нет ни одного p/ul/ol
     вовсе, выборка их и так не касается), оглавление (.blog-toc — ЕСТЬ
     внутри реальные <p>/<ul>, поэтому для него нужен явный сброс ниже,
     иначе общий селектор задел бы и его). */
  .blog-article-body p,
  .blog-article-body ul,
  .blog-article-body ol,
  .blog-article-body blockquote {
    padding-inline: calc(var(--blog-gutter-text) - var(--blog-gutter-head));
    overflow-wrap: break-word;
    hyphens: auto;
  }

  /* Сброс для оглавления (задание: "не затрагивать... оглавление") — его
     <p class="blog-toc-title">/<ul> иначе попали бы под общее правило
     выше по тем же селекторам. */
  .blog-toc-title,
  .blog-toc ul {
    padding-inline: 0;
  }

  /* .blog-lead — тоже <p>, но лид уже сам по себе часть "основного
     текста" (не заголовок) — задание не исключает его явно, оставляем
     под общим правилом выше (без отдельного сброса). */
}

/* ---------------------------------------------------------------- Без анимации */

@media (prefers-reduced-motion: reduce) {
  .blog-reveal {
    opacity: 1 !important;
    transform: none !important;
    transition: none !important;
  }

  .blog-progress-bar {
    display: none;
  }

  .blog-service-embed,
  .blog-service-embed .btn-primary,
  .blog-card-link,
  .blog-card-image,
  .blog-card-title,
  .blog-card-title::after,
  .blog-article-body a::after,
  .blog-toc a,
  .blog-article-footer a {
    transition: none !important;
  }
}
