/* ==========================================================================
   База знаний puzzlebot. Страница статьи целиком: шапка → сайдбар разделов
   → колонка статьи → оглавление → подвал статьи → футер.

   Макет: Figma «Landing» QO1EeXYIc7UBXIwZqAdw20, страница Help,
   нода 1171:8612 (1920×8989). Компоненты сверены с Kit 2.0
   t74DdsUCbV7JieZEdq3IsY.

   Здесь живёт только то, чего нет в дизайн-системе: раскладка страницы,
   оформление тела статьи и блоки, которых на лендинге не было. Цвета,
   типографика, кнопки, карточки, поле ввода — из ../design-system/tokens.css.
   Понадобилось значение, которого нет в токенах, — это повод завести токен,
   а не написать число.

   ── Как это ложится на движок ────────────────────────────────────────────
   help.puzzle.bot — Next.js + Payload CMS, поверх Pico CSS. Тело статьи
   приходит из редактора почти чистой семантикой: <p>, <h2>, <h3>,
   <ol class="list-number">, <ul class="list-bullet">, <blockquote>, <img>,
   <code>, <span class="term-tip-btn">, <iframe>.

   Вложенные пункты идут голым <li> без класса: правила списков написаны
   на селектор потомка, и уровень задаёт сама вложенность, а не имя класса.
   Если Payload проставит пунктам свой класс, вёрстке это безразлично.

   Поэтому всё, что редактор умеет отдать сам, стилизуется по тегу внутри
   .payload-richtext — разработчику не придётся править рендер. Класс
   заводится только там, где блока в редакторе нет и его всё равно
   предстоит собирать кастомным блоком Payload: цветные карточки, таблица,
   код с вкладками, видео, карусель, аккордеон, кнопка-ссылка.

   Имена уже существующих классов движка (.layout, .sidebar, .content,
   .breadcrumb, .article-toc, .payload-richtext, .term-tip-btn) сохранены
   как есть — переименовывать работающее незачем. Новое идёт с префиксом
   kb-, чтобы не столкнуться с Pico.

   ── Вьюпорты ─────────────────────────────────────────────────────────────
   Ступени взяты у лендинга, новых не выдумано:

     ≥1280  Десктоп. Три колонки, как в макете.
   984–1279 Оглавление уходит из сетки и становится раскрывающимся
            блоком над статьёй: колонке статьи нужнее ширина.
    768–983 Сайдбар прячется в выдвижную панель по кнопке в шапке.
      <768  Мобильная раскладка. Кегли заголовков — из режима mobile
            коллекции Typography кита, а не подобраны на глаз.
   ========================================================================== */

/* ── Иконки ────────────────────────────────────────────────────────────────
   Уехали в дизайн-систему (tokens.css, раздел «Иконки») вместе с иконками
   лендинга: набор один на весь сайт. Заодно схлопнулись два дубля, которые
   развело хранение в двух файлах: --icon-x оказался байт в байт равен
   --icon-close, а --icon-anchor — той же цепью, что --icon-link, только
   выгруженной второй раз.

   Классы теперь без приставки kb-: иконка общая, и приставка врала бы,
   что она только для справки. Соответствие старых имён новым:
   kb-caret → caret, kb-thumb-up → thumbs-up, kb-anchor → link,
   kb-x → close, kb-start → section-start, и так далее */

/* ── Собственные величины страницы ─────────────────────────────────────────
   Значения, которых нет в шкалах дизайн-системы: это не ступени ритма,
   а размеры конкретных блоков, снятые с макета 1171:8612. Держим их здесь,
   а не разбрасываем числами по правилам.

   Всё, кроме ширин колонок, объявлено на корне, а не на .kb-layout: тултип
   и просмотр картинки живут в body, а на витрине вне раскладки лежат и сами
   блоки — var() до величины, объявленной на .kb-layout, оттуда не дотянется
   и правило молча отвалится. Переопределения по ширине экрана при этом
   работают как прежде: наследование от корня они перебивают */
:root {
  /* Потолок высоты картинки. Вертикальный скриншот телефона иначе занимает
     три экрана прокрутки: читатель теряет место в инструкции. Полный
     размер открывается по клику.

     Потолок 600: на ноутбуке под картинкой остаётся видно начало
     следующего абзаца, а вертикальный скриншот телефона не растёт вместе
     с высотой окна. Что не влезло по пропорциям — не обрезается: картинка
     вписывается целиком, а поля по бокам закрашивает её же размытая копия */
  --kb-image-max-height: 600px;

  --kb-column: 800px;          /* колонка статьи */

  /* Вынос колонки маркеров влево за колонку статьи. Живёт на корне, а не
     у списка: это же число — минимальное поле .kb-main, иначе вынесенным
     наружу маркерам негде стоять и они ложатся на сайдбар */
  --kb-marker-out: 48px;

  /* Ниже этой ширины таблица не ужимается, а прокручивается внутри рамки.
     480 — та ширина, на которой строка ещё читается: имя поля и тип целиком,
     описание переносится по словам. Дальше ужимать нельзя, там начинается
     перенос по буквам (у статьи стоит overflow-wrap: anywhere ради длинных
     идентификаторов, и в узкой ячейке он рвёт обычные слова) */
  --kb-table-min: 480px;

  /* Ниже этой ширины плитка главной перестаёт вмещать подпись: самое
     длинное слово в ней — «подключить», 124 в кегле H3, плюс поля 12
     по краям и рамка 2. По этому порогу сетка сама решает, встанут плитки
     на телефоне по две или по одной: на 360 и шире — по две, на 320 —
     по одной */
  --kb-tile-min: 156px;

  /* Фон просмотра картинки на телефоне (1171:9008) — сплошной чёрный.
     Чистого чёрного в ките нет: Overlay/L это 60% чёрного, а сквозь него
     просвечивает страница и мешает разглядывать скриншот */
  --kb-lightbox-bg: #000;

  --kb-tooltip-max: 336px;     /* тултип глоссария, 1171:8943 */
  /* Хвостик тултипа: 12 в ширину, 5 в высоту (кит 133:14775). Отсюда же
     скрипт берёт отступ тултипа от цели — иначе хвостик висел бы
     в воздухе, а два числа разъезжались бы по разным файлам */
  --kb-tooltip-tail: 5px;
  --kb-popup-width: 256px;     /* попап оценки страницы, 1171:9021 */
  --kb-textarea-height: 82px;  /* поле в попапе, там же */

  /* Drawer снизу — компонент Dropdown кита 2.0 (3203:38180). Ручка живёт
     над панелью, на затемнении: 32 в высоту, полоска 60×6 у нижнего края */
  --kb-drawer-handle: 32px;
  --kb-drawer-line: 60px;
  --kb-drawer-line-h: 6px;

  --kb-empty-sticker: 96px;    /* стикер «ничего не найдено», 1677:33036 */

  /* Размытая копия картинки в полях кадра. Токена под такой радиус в системе
     нет и заводить его незачем: в ките шкала блюра из двух ступеней (8 и 16),
     обе для стекла под шапкой, а здесь другая задача — увести подложку
     на второй план, чтобы глаз не читал её как вторую картинку. На 16 она
     ещё различима, на 24 превращается в цветовое пятно.

     Подложка приглушена и по цвету: чуть насыщеннее и темнее самой картинки,
     иначе светлый скриншот на светлых полях сливается с ними в кашу.
     Увеличение прячет размытый край — фильтр размывает и границу кадра */
  --kb-letterbox-blur: 24px;
  --kb-letterbox-saturate: 1.15;
  --kb-letterbox-dim: 0.9;
  --kb-letterbox-zoom: 1.15;

  /* Сглаживание углов (суперэллипс) — примета лендинга: там оно стоит
     у карточек и плиток. В базе знаний углы рисуются обычной дугой.
     Правило выключено одной переменной, а не вычищено из тридцати правил:
     corner-shape в них остаётся, но получает round. Вернуть сглаживание —
     снять эту строку */
  --corner-smooth: round;
}

/* Ширины колонок — только у раскладки: за её пределами колонок нет */
.kb-layout {
  --kb-sidebar: 320px;         /* левая колонка целиком, 1171:8612 */
  --kb-toc: 240px;             /* оглавление справа */
  --kb-toc-gap: 38px;          /* от колонки до линии оглавления */
}

/* ── Раскладка ─────────────────────────────────────────────────────────────
   Две колонки: сайдбар и всё остальное. Оглавление живёт внутри второй,
   рядом с колонкой статьи.

   В макете справа есть третья колонка шириной с сайдбар, но это не часть
   раскладки, а симметрия при 1920: контент там стоит по центру экрана.
   Если завести её колонкой сетки, на 1440 статье достанется 490 вместо
   800 — то есть ради симметрии придётся отобрать треть ширины у текста */
/* Переход по ссылке из оглавления доезжает, а не прыгает: читателю видно,
   куда его увезли. Тем, кто отключил анимации, — прыжком, как раньше */
html { scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
}

.kb-layout {
  display: grid;
  grid-template-columns: var(--kb-sidebar) minmax(0, 1fr);
  align-items: start;
}

/* Линия под шапкой. На лендинге её нет — там под шапкой лежит градиентная
   подложка, — а в макете базы знаний шапка отделена от колонок линией
   (1171:8613, border-bottom 2px). Только на страницах с раскладкой
   справки: kb.css подключает и лендинг (price-new-info), и шапка там
   должна остаться лендинговой, без линии и сплошной заливки */
body:has(.kb-layout) .site-header {
  border-bottom: var(--size-stroke-m) solid var(--color-border);
  background: var(--color-surface);
}

/* Колонка разделов: своя заливка и линия справа, как в макете. Красится
   вся колонка до низа страницы, а не только видимая часть списка —
   поэтому фон и линия висят на ячейке сетки, а залипает то, что внутри */
.kb-sidebar {
  /* Цвет колонки держит переменная: под него красится и залипшее поле
     поиска, и затухание у краёв списка. В выезжающей панели на узком
     экране колонка другого цвета — там переменная переопределяется */
  --kb-sidebar-bg: var(--color-neutral-01);
  align-self: stretch;
  padding: var(--spacing-m);
  background: var(--kb-sidebar-bg);
  border-right: var(--size-stroke-m) solid var(--color-border);
}

/* Список разделов и оглавление залипают под шапкой. Между шапкой и залипшим
   списком остаётся тот же воздух, что и до прокрутки (поле сайдбара, 16), —
   иначе поиск приклеивается к шапке вплотную. Высота ограничена окном, а
   внутри своя прокрутка: дерево разделов длиннее экрана, и без этого низ
   списка был бы недостижим */
.kb-sidebar__inner,
.article-toc {
  position: sticky;
  top: calc(var(--size-header) + var(--spacing-m));
  max-height: calc(100vh - var(--size-header) - var(--spacing-m) * 2);
  overflow-y: auto;
  overscroll-behavior: contain;
  /* Полосы прокрутки нет: в макете её нет, а на макбуке с включённым
     «показывать всегда» она стояла серой чертой поперёк колонки. Что
     список длиннее окна, видно по затуханию у краёв */
  scrollbar-width: none;
}
.kb-sidebar__inner::-webkit-scrollbar,
.article-toc::-webkit-scrollbar { display: none; }
/* У оглавления воздух под шапкой шире: тот же, на который встаёт заголовок,
   когда к нему переходят по ссылке из оглавления */
.article-toc {
  top: calc(var(--size-header) + var(--spacing-xl));
  max-height: calc(100vh - var(--size-header) - var(--spacing-xl) - var(--spacing-m));
}

.kb-sidebar__inner {
  /* Поля списка держит переменная: залипшее поле поиска перекрывает их
     своей заливкой, иначе над ним оставалась бы щель с уезжающим текстом.

     Вдвое шире кольца фокуса, а не вровень с ним: своя прокрутка режет всё,
     что вышло за край (overflow-y: auto делает и overflow-x обрезающим), и
     при запасе ровно в кольцо у него срезало боковые грани */
  --kb-list-pad: calc(var(--focus-ring-width) * 2);
  display: flex;
  flex-direction: column;
  gap: var(--spacing-m);
  /* Поле по краям тут же снимается отрицательным margin: ширина колонки не
     меняется, но своя прокрутка (overflow-y) больше не срезает кольцо у
     поля поиска. Точку залипания и потолок высоты сдвигаем на ту же
     величину — иначе список встал бы ниже, чем задумано */
  padding: var(--kb-list-pad);
  margin: calc(-1 * var(--kb-list-pad));
  /* Край, за которым есть ещё пункты, растворяется в фоне: обрыв списка на
     границе окна читается как конец списка, а не как «прокрутите дальше»
     (1212:16997). Нижний край затухает маской, верхний — заливкой под
     залипшим полем поиска (см. .kb-search::after).

     Множители 0/1 ставит скрипт: прокручен ли список и остались ли пункты
     ниже, CSS сам не знает. Прокрутки нет — оба нуля, и затухания нет */
  --kb-fade: var(--spacing-l);
  -webkit-mask-image: linear-gradient(to bottom, #000
    calc(100% - var(--kb-fade) * var(--kb-fade-end, 0)), transparent);
  mask-image: linear-gradient(to bottom, #000
    calc(100% - var(--kb-fade) * var(--kb-fade-end, 0)), transparent);
  top: calc(var(--size-header) + var(--spacing-m) - var(--kb-list-pad));
  max-height: calc(100vh - var(--size-header) - var(--spacing-m) * 2
    + var(--kb-list-pad) * 2);
}

/* Центральная ячейка: колонка статьи по центру, оглавление справа от неё.
   Колонка не растягивается шире 800 — строка длиннее уже не читается.

   Сетка именованная, и это не украшательство. Оглавление начинается на
   уровне первого блока статьи, а не на уровне h1: заголовок занимает всю
   ширину колонки, оглавление стоит ниже и правее. При этом на узком
   экране те же области перестраиваются так, что оглавление встаёт ПОСЛЕ
   заголовка, а не перед ним — иначе читатель видит содержание раньше,
   чем узнаёт название статьи */
.kb-main {
  display: grid;
  grid-template-columns: minmax(0, var(--kb-column)) var(--kb-toc);
  grid-template-areas:
    "head ."
    "body toc";
  gap: 0 var(--kb-toc-gap);
  justify-content: center;
  /* Поле сверху — тот самый квадрат 64 над крошками в макете (1171:8644).
     У сайдбара его нет: там список начинается сразу под шапкой.

     Поле снизу — такой же квадрат-распорка 160 под подвалом статьи
     (1171:8906), то есть шаг между блоками с лендинга: 160 на десктопе,
     128 на планшете, 96 на телефоне. Отступ несёт колонка статьи, а не
     раскладка целиком: заливка сайдбара в макете доходит до подвала
     сайта, и padding на раскладке оставлял бы под ней белую полосу.

     Боковое поле не меньше выноса маркеров: колонка статьи ужимается
     раньше, чем упрётся в сайдбар, а список остаётся в поле. Поле по обе
     стороны одинаковое — центр колонки от него не съезжает */
  padding: var(--spacing-xxl) max(var(--page-padding-desktop), var(--kb-marker-out))
    var(--section-gap);
}
.kb-article-head { grid-area: head; }
.content { grid-area: body; }
.article-toc { grid-area: toc; }

.content {
  min-width: 0;   /* иначе длинный код распирает грид-ячейку */
}

/* ── Пункт сайдбара ────────────────────────────────────────────────────────
   Один компонент на две роли: заголовок раздела и статья внутри него.
   Раздел — такая же ссылка, ведёт на обзорную статью вкладки; отличается
   иконкой слева и тем, что подпись у него основного цвета.

   Открытый пункт и наведение делят одну подложку — различает их цвет
   подписи. Подсветка фона говорит «сюда указывают», цвет текста — «вы
   здесь» */
.kb-nav-item {
  display: flex;
  align-items: center;
  gap: var(--spacing-xs);
  padding: 0 var(--spacing-xs);
  min-height: var(--size-button-m);
  border-radius: var(--radius-l);
  color: var(--color-text-secondary);
  font-size: var(--text-p2);
  line-height: var(--text-p2--line-height);
  font-weight: var(--font-weight-body-medium);
  text-decoration: none;
  transition: background-color var(--duration-fast) var(--ease-brand),
              color var(--duration-fast) var(--ease-brand);
}
/* Многострочный пункт: поля сверху и снизу появляются, высота растёт
   по содержимому — в макете это отдельный вариант компонента (1171:8612) */
.kb-nav-item {
  padding-block: var(--spacing-xs);
}
@media (hover: hover) {
  .kb-nav-item:hover { background: var(--color-button-ghost-hover); }
}
.kb-nav-item:active { background: var(--color-button-ghost-pressed); }
.kb-nav-item[aria-current] {
  background: var(--color-button-ghost-hover);
  color: var(--color-text-primary);
}
.kb-nav-item:focus-visible {
  outline: var(--focus-ring-width) solid var(--color-brand);
  outline-offset: var(--focus-ring-offset);
}

/* Раздел: подпись основного цвета и иконка слева */
.kb-nav-item--section { color: var(--color-text-primary); }
/* Статья внутри раздела: подпись встаёт вровень с подписью раздела. В макете
   у неё та же иконка, только прозрачная — здесь достаточно поля слева */
.kb-nav-item:not(.kb-nav-item--section) {
  padding-left: calc(var(--spacing-xs) * 2 + var(--size-button-icon-m));
}
.kb-nav-item__icon {
  flex: none;
  display: grid;
  place-items: center;
  width: var(--size-button-icon-m);
  height: var(--size-button-icon-m);
  color: var(--color-text-primary);
}
.kb-nav-item__icon .icon {
  width: var(--size-button-icon-s);
  height: var(--size-button-icon-s);
}

/* Группа разделов: внутри группы пункты стоят вплотную, воздух — только
   между группами, он же между поиском и первой группой (1171:8615) */
.kb-nav-group { display: flex; flex-direction: column; }
/* Отсеянное поиском прячется совсем. Без этого правила display у пункта и
   группы перебивает браузерное скрытие по hidden, и фильтр молчит */
.kb-nav-group[hidden],
.kb-nav-item[hidden] { display: none; }

/* ── Поле поиска ───────────────────────────────────────────────────────────
   Компонент Input из кита (3194:20622). Обводка ходит по шкале tertiary:
   покой — Stroke/Normal, наведение — Stroke/Hover, нажатие — Stroke/Pressed.
   В фокусе обводка становится синей и добавляется кольцо */
/* Поле держится у верхнего края списка: разделов много, и без залипания
   искать пришлось бы, домотав обратно наверх. Заливка непрозрачная —
   пункты уезжают под поле, а не сквозь него.

   Поле поднято на толщину кольца фокуса: у списка свои поля ровно в эту
   величину (см. .kb-sidebar__inner), и без поправки над залипшим полем
   оставалась бы щель, в которой видно уезжающие пункты */
.kb-search {
  position: sticky;
  /* Ноль здесь — это внутренний край списка, то есть под полем списка:
     отсчёт у залипшего элемента идёт от него, а не от края прокрутки.
     В этом поле и помещается кольцо фокуса — ради него оно вдвое шире
     самого кольца (см. --kb-list-pad) */
  top: 0;
  z-index: 10;
  /* Заливка равна фону колонки, а не белому: иначе под залипшим полем
     видна светлая полоса шириной со всю колонку. Запасное значение — для
     витрины, где блок лежит вне сайдбара, на фоне страницы */
  background: var(--kb-sidebar-bg, var(--color-bg-primary));
}
/* Полосы над полем и под ним. Сверху лежит поле списка, снизу — зазор до
   первого пункта; в обоих был бы виден уезжающий текст. Залипший блок не
   может подняться выше содержимого родителя, а лишние поля у самой формы
   увели бы лупу и крестик из центра поля — поэтому заливку доносят
   псевдоэлементы, размеров формы они не меняют.

   Кольцо фокуса они не закрывают: тень поля рисуется вместе с ним, а поле
   в дереве стоит после этих блоков */
.kb-search::before,
.kb-search::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  background: var(--kb-sidebar-bg, var(--color-bg-primary));
}
.kb-search::before {
  bottom: 100%;
  height: var(--kb-list-pad);
}
/* Полоса под полем заодно доносит верхнее затухание: пока список прокручен,
   она вырастает на --kb-fade и уходит в прозрачность, и уезжающие пункты
   не выныривают из-под поля резким срезом. Список не прокручен — множитель
   нулевой, полоса ровно та же сплошная 16, что и была */
.kb-search::after {
  top: 100%;
  height: calc(var(--spacing-m) + var(--kb-fade, 0px) * var(--kb-fade-start, 0));
  background: linear-gradient(to bottom,
    var(--kb-sidebar-bg, var(--color-bg-primary)) var(--spacing-m), transparent);
}
.kb-search__field {
  width: 100%;
  height: var(--size-button-m);
  padding: 0 calc(var(--spacing-xs) * 2 + var(--size-button-icon-m)) 0
             calc(var(--spacing-xs) * 2 + var(--size-button-icon-m));
  border: var(--size-stroke-m) solid var(--color-border);
  border-radius: var(--radius-l);
  background: transparent;
  color: var(--color-text-primary);
  /* Свой крестик уже есть; браузерный у type=search рисуется вторым */
  appearance: none;
  font: inherit;
  font-size: var(--text-p2);
  line-height: var(--text-p2--line-height);
  transition: border-color var(--duration-fast) var(--ease-brand),
              box-shadow var(--duration-fast) var(--ease-brand);
}

/* Пустое поле — только обводка (819:2352). Заливка появляется, когда поле
   в работе (817:31194) */
.kb-search__field:focus,
.kb-search__field:not(:placeholder-shown) { background: var(--color-surface); }
.kb-search__field::-webkit-search-cancel-button,
.kb-search__field::-webkit-search-decoration { -webkit-appearance: none; display: none; }
.kb-search__field::placeholder { color: var(--color-text-tertiary); }
@media (hover: hover) {
  .kb-search__field:hover { border-color: var(--color-border-hover); }
}
.kb-search__field:active { border-color: var(--color-border-pressed); }
.kb-search__field:focus {
  outline: 0;
  border-color: var(--color-brand);
  box-shadow: 0 0 0 var(--focus-ring-width) var(--color-focus);
}
/* Лупа и крестик лежат поверх поля. Лупа некликабельна — она метка, а не
   кнопка: нажатие на неё ничего не делает и курсор это показывает */
.kb-search__icon {
  position: absolute;
  top: 50%;
  left: var(--spacing-xs);
  transform: translateY(-50%);
  color: var(--color-text-tertiary);
  pointer-events: none;
}
/* Лупа темнеет, когда поле в работе: в ките у состояний Placeholder, Hover
   и Pressed она Tertiary (#b5b5b8), а у Focus и Value — Secondary
   (#808185), это разные ассеты в компоненте (3194:20829). Лупа стоит в
   разметке перед полем, соседним селектором до неё не дотянуться —
   поэтому :has на форме */
.kb-search:has(.kb-search__field:focus) .kb-search__icon,
.kb-search:has(.kb-search__field:not(:placeholder-shown)) .kb-search__icon {
  color: var(--color-text-secondary);
}
/* Кнопка 22, крестик внутри 16 — как Clear в ките (817:31194). Подложки у
   неё нет ни в покое, ни под курсором — потому и не .btn--ghost, у того
   под курсором появляется заливка: наведение ходит только по цвету значка,
   шкала tertiary */
.kb-search__clear {
  position: absolute;
  top: 50%;
  right: var(--spacing-xs);
  transform: translateY(-50%);
  background: none;
  color: var(--color-text-tertiary);
}
@media (hover: hover) {
  .kb-search__clear:hover { color: var(--color-text-tertiary-hover); }
}
.kb-search__clear:active { color: var(--color-text-tertiary-pressed); }
.kb-search__clear .icon {
  width: var(--size-button-icon-s);
  height: var(--size-button-icon-s);
}
.kb-search__clear[hidden] { display: none; }

/* ── Ничего не нашлось ─────────────────────────────────────────────────────
   Пустой результат поиска по разделам (1677:33036 в ките 2.0): стикер 96,
   строка под ним и кнопка сброса. Показывает скрипт — без него фильтровать
   нечем, и пустым список не станет */
.kb-search-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  /* Забирает остаток списка и встаёт в его середине: в панели, которая
     держит размер, прижатый к полю поиска стикер оставлял бы под собой
     полупустой экран */
  flex: 1;
  justify-content: center;
  /* Стикер отбит от строки на 16, кнопка от строки на 8 (1677:33036) */
  gap: var(--spacing-xs);
  padding: var(--spacing-xl) var(--spacing-l);
  text-align: center;
}
.kb-search-empty[hidden] { display: none; }
.kb-search-empty__sticker {
  width: var(--kb-empty-sticker);
  height: var(--kb-empty-sticker);
  margin-bottom: var(--spacing-s);
}
.kb-search-empty__sticker svg { display: block; }
.kb-search-empty__title {
  margin: 0;
  font-size: var(--text-p1);
  line-height: var(--text-p1--line-height);
  font-weight: var(--font-weight-body-medium);
  color: var(--color-text-primary);
}

/* ── Хлебные крошки ────────────────────────────────────────────────────────
   Последний уровень — раздел, а не сама статья: название статьи стоит
   строкой ниже, в заголовке, и повторять его незачем */
.breadcrumb {
  display: flex;
  align-items: center;
  gap: var(--spacing-xs);
  font-size: var(--text-p2);
  line-height: var(--text-p2--line-height);
  font-weight: var(--font-weight-body-medium);
}
.breadcrumb a {
  color: var(--color-text-tertiary);
  text-decoration: none;
  transition: color var(--duration-fast) var(--ease-brand);
}
@media (hover: hover) {
  .breadcrumb a:hover { color: var(--color-text-tertiary-hover); }
}
.breadcrumb a:active { color: var(--color-text-tertiary-pressed); }
.breadcrumb__sep {
  flex: none;
  width: var(--size-button-icon-s);
  height: var(--size-button-icon-s);
  color: var(--color-text-tertiary);
}
.breadcrumb a.breadcrumb__current {
  display: flex;
  align-items: center;
  gap: var(--spacing-xs);
  color: var(--color-text-primary);
}

/* ── Оглавление ────────────────────────────────────────────────────────────
   Слева серая линия ростом ровно в список, по ней ездит тёмный отрезок
   активного пункта. Линия не должна тянуться до низа статьи, поэтому
   колонка не растягивается по ячейке сетки (1171:8886 — высота линии
   равна высоте списка).

   Высота отрезка не задана числом: он тянется по высоте пункта — тот
   бывает в две и три строки — и добирает половину зазора сверху и снизу,
   как в макете (46 против 38 у самого пункта, 1171:8891) */
.article-toc {
  align-self: start;
  padding-inline: var(--spacing-m);
  /* Линия — фон, а не border. У колонки своя прокрутка (overflow-y), а она
     режет всё, что вылезло за padding-box: тёмный отрезок активного пункта
     лежит ровно на линии, и с бордером его срезало бы по границе */
  background: linear-gradient(var(--color-divider) 0 0)
    left / var(--size-stroke-m) 100% no-repeat;
  display: flex;
  flex-direction: column;
  gap: var(--spacing-xs);
}
.article-toc a {
  position: relative;
  display: block;
  padding: var(--spacing-xs);
  border-radius: var(--radius-l);
  color: var(--color-text-secondary);
  font-size: var(--text-p2);
  line-height: var(--text-p2--line-height);
  font-weight: var(--font-weight-body-medium);
  text-decoration: none;
  transition: color var(--duration-fast) var(--ease-brand);
}
@media (hover: hover) {
  .article-toc a:hover { color: var(--color-text-secondary-hover); }
}
.article-toc a[aria-current] { color: var(--color-text-primary); }
/* Отрезок рисуется поверх линии родителя: она проходит по левому краю
   колонки, поэтому отрезок сдвинут ровно на её толщину влево */
.article-toc a[aria-current]::before {
  content: "";
  position: absolute;
  inset-block: calc(-1 * var(--spacing-xs) / 2);
  left: calc(-1 * var(--spacing-m));
  width: var(--size-stroke-m);
  background: var(--color-text-primary);
}
/* Уровни не расставляются отступом: h3 стоит на той же вертикали, что h2.
   Класс toc-level-3 движок всё равно проставляет, но вёрстке он не нужен */

/* Переход по ссылке из оглавления: заголовок встаёт под шапку с тем же
   воздухом, на котором залипает само оглавление */
.kb-article-head [id],
.payload-richtext :where(h2, h3, h4)[id] {
  scroll-margin-top: calc(var(--size-header) + var(--spacing-xl));
}

/* ══ ТЕЛО СТАТЬИ ══════════════════════════════════════════════════════════
   Всё в этом разделе движок отдаёт готовой семантикой, поэтому правила
   висят на тегах внутри .payload-richtext. Разработчику не нужно ничего
   менять в рендере Payload.

   ── Вертикальный ритм ─────────────────────────────────────────────────────
   У каждого блока своя полоса padding-block 12, соседние блоки дают между
   собой 24. У заголовков полоса 32/12: заголовок отбивается от предыдущего
   блока, а не от следующего.

   У текста полоса набрана padding'ом: margin у соседей схлопнется и даст
   12 вместо 24 — весь ритм страницы уехал бы вдвое.

   А блокам с собственным полем — карточке, коду, видео, картинке —
   padding отдать нельзя: он у них занят внутренним полем 24, а у
   крашеных попал бы ещё и под заливку, дав полосу фона над содержимым.
   Им полоса достаётся margin'ом, и чтобы он не схлопывался, колонка
   статьи и остальные контейнеры блоков разложены flex-колонками: между
   flex-элементами margin не схлопываются, и 12 + 12 честно дают 24.

   Так обе половины ритма живут рядом и складываются друг с другом:
   абзац отдаёт снизу свои 12 padding'ом, карточка сверху — свои 12
   margin'ом */
.payload-richtext,
.payload-richtext li,
.kb-card,
.payload-richtext > blockquote,
.kb-quote__body,
.kb-faq__body {
  display: flex;
  flex-direction: column;
}

.payload-richtext {
  font-size: var(--text-p1);
  line-height: var(--text-p1--line-height);
  color: var(--color-text-primary);
  /* В справке есть длинные адреса и snake_case_идентификаторы. Без этого
     такая строка распирает колонку и уводит страницу вбок. Именно
     anywhere: только оно уменьшает min-content колонки */
  overflow-wrap: anywhere;
}

/* Сброс и базовая полоса набраны через :where() — с нулевой
   специфичностью. Тогда порядок в файле решает спор сброса с полосой,
   а любой класс перебивает оба без !important.

   Полоса нужна не только прямым детям статьи: те же 12 обязаны получить
   блоки внутри карточки, цитаты, ответа на вопрос и пункта списка. Иначе
   два абзаца в карточке сядут вплотную — а это ровно то, что редактор
   отдаёт чаще всего */
/* Сброс до самого низа, а не только у прямых детей: абзац внутри пункта
   списка приходит с браузерным margin 1em, и от него номер шага уезжает
   вверх относительно своего текста */
:where(.payload-richtext, .payload-richtext *) { margin-block: 0; }
:where(
  .payload-richtext > *,
  .payload-richtext li > *,
  .kb-card > *,
  .payload-richtext > blockquote > *,
  .kb-quote__body > *,
  .kb-faq__body > *
) { padding-block: var(--spacing-s); }

/* Блоки с собственным полем: полоса margin'ом, ритмовый padding с них
   снят — он бы прибавился к их полю 24, а у крашеных ушёл бы под заливку.
   Правило стоит ниже базового и тоже нулевой специфичности, поэтому
   спор решает порядок */
:where(
  .kb-card,
  .payload-richtext blockquote,
  .kb-quote,
  .kb-figure,
  .payload-richtext figure,
  .kb-table-wrap,
  .kb-code,
  .kb-video,
  .kb-linkbutton,
  .kb-faq,
  .kb-gallery,
  .payload-richtext hr
) {
  margin-block: var(--spacing-s);
  padding-block: 0;
}

/* Первый и последний потомок не добавляют своей полосы к полю контейнера:
   иначе поле 24 превращается в 36 сверху и снизу */
:where(
  .payload-richtext li,
  .kb-card,
  .payload-richtext > blockquote,
  .kb-quote__body,
  .kb-faq__body
) > :first-child { padding-top: 0; margin-top: 0; }
:where(
  .payload-richtext li,
  .kb-card,
  .payload-richtext > blockquote,
  .kb-quote__body,
  .kb-faq__body
) > :last-child { padding-bottom: 0; margin-bottom: 0; }

/* Заголовок статьи. Кнопки якоря у него нет: ссылка на статью — это
   адрес страницы */
.article-title {
  margin: 0;
  padding-block: var(--spacing-l) var(--spacing-m);
  font-size: var(--text-h1);
  line-height: var(--text-h1--line-height);
  letter-spacing: var(--text-h1--letter-spacing);
  font-weight: var(--font-weight-heading);
  color: var(--color-text-primary);
}

/* Заголовки разделов. h4 набран кеглем основного текста — отличается
   только весом; пятого уровня нет намеренно: если он понадобился, значит
   развалилась структура статьи, и это правится текстом, а не вёрсткой */
.payload-richtext :where(h2, h3, h4) {
  position: relative;   /* от них позиционируется кнопка якоря */
  padding-block: var(--spacing-xl) var(--spacing-s);
  font-weight: var(--font-weight-heading);
  color: var(--color-text-primary);
}
.payload-richtext h2 {
  font-size: var(--text-h2);
  line-height: var(--text-h2--line-height);
  letter-spacing: var(--text-h2--letter-spacing);
}
.payload-richtext h3 {
  font-size: var(--text-h3);
  line-height: var(--text-h3--line-height);
}
.payload-richtext h4 {
  font-size: var(--text-p1);
  line-height: var(--text-p1--line-height);
}

/* Ссылка. Вес medium — общее правило дизайн-системы: всё, на что можно
   нажать, набрано medium. Подчёркивания нет ни в одном состоянии */
/* Кнопка — тоже <a>, но у неё свой цвет и вес: без исключения синяя
   подпись ложилась на синюю заливку, и кнопка выглядела пустой.
   Исключение в :where(), чтобы вес правила не вырос и соседи вроде
   .kb-linkbutton по-прежнему его перебивали */
.payload-richtext a:where(:not(.btn)) {
  color: var(--color-text-link);
  font-weight: var(--font-weight-body-medium);
  text-decoration: none;
  transition: color var(--duration-fast) var(--ease-brand);
}
@media (hover: hover) {
  .payload-richtext a:where(:not(.btn)):hover { color: var(--color-text-link-hover); }
}
.payload-richtext a:where(:not(.btn)):active { color: var(--color-text-link-pressed); }

.payload-richtext strong { font-weight: var(--font-weight-body-bold); }
.payload-richtext em { font-style: italic; }

/* Разделитель. Перед заголовком не ставится: тот отбивается сам, две
   отбивки подряд читаются как ошибка вёрстки */
/* Линия рисуется заливкой, а не рамкой: рамка лежит выше padding'а, и
   вертикальная отбивка уходила бы только вниз — сверху 12, снизу 56.
   Заливка на своей высоте отбивается margin'ом поровну в обе стороны */
.payload-richtext hr {
  height: var(--size-stroke-m);
  border: 0;
  background: var(--color-divider);
  /* Браузерное margin-inline: auto снимается обязательно: колонка статьи —
     flex-колонка, и авто-поля по бокам отменяют растяжку flex-элемента.
     Линия при этом ужимается до нуля и не рисуется вовсе */
  margin-inline: 0;
}

/* ── Инлайн-код ────────────────────────────────────────────────────────────
   Плашка ниже строки абзаца (22 против 24) и потому не распирает
   межстрочный интервал.

   Копируется по клику — курсор это показывает. Если внутри плашки есть
   ссылка, клик по ней ведёт по адресу, а клик по остальному тексту
   копирует: развод по цели живёт в kb.js */
.payload-richtext code {
  display: inline-block;
  padding-inline: var(--spacing-xxs);
  border-radius: var(--radius-s);
  background: var(--color-divider);
  font-family: var(--font-mono);
  font-size: var(--text-mono);
  line-height: var(--text-mono--line-height);
  font-weight: var(--font-weight-body-medium);
  cursor: pointer;
  transition: background-color var(--duration-fast) var(--ease-brand);
}
@media (hover: hover) {
  .payload-richtext code:hover { background: var(--color-neutral-22); }
}
.payload-richtext code a { display: inline; }

/* Блок кода сам содержит <code>, и плашка ему не нужна — ни в покое, ни
   под курсором: копируется он кнопкой в углу, а не щелчком по тексту */
.payload-richtext pre code,
.kb-code code {
  display: block;
  padding: 0;
  border-radius: 0;
  background: none;
  cursor: auto;
}
@media (hover: hover) {
  .payload-richtext pre code:hover,
  .kb-code code:hover {
    display: block;
    padding: 0;
    border-radius: 0;
    background: none;
    cursor: auto;
}
}

/* ── Глоссарий ─────────────────────────────────────────────────────────────
   Термин прямо в тексте: никуда не уводит, по клику показывает
   определение. Этим он и отличается от ссылки — читатель узнаёт значение
   слова и продолжает читать ту же строку.

   Пунктир нарисован градиентом, а не text-decoration: точка 2 и пропуск 2,
   а браузерный dashed рисует свой шаг и подогнать его нечем */
.term-tip-btn {
  padding-bottom: var(--size-stroke-m);
  background-image: linear-gradient(to right,
    currentColor 0 var(--size-stroke-m),
    transparent var(--size-stroke-m) calc(var(--size-stroke-m) * 2));
  background-repeat: repeat-x;
  background-size: calc(var(--size-stroke-m) * 2) var(--size-stroke-m);
  background-position: 0 100%;
  font-weight: var(--font-weight-body-medium);
  cursor: help;
}
.term-tip-btn:focus-visible {
  outline: var(--focus-ring-width) solid var(--color-brand);
  outline-offset: var(--spacing-xxxs);
  border-radius: var(--radius-s);
}

/* ── Тултип ────────────────────────────────────────────────────────────────
   Один элемент на три роли: «Скопировано» у кнопки якоря, «Скопировано»
   у инлайн-кода и определение термина. Ширина по содержимому, потолок —
   чтобы длинное определение не растянулось в строку через весь экран.

   position: fixed, координаты считает скрипт от прямоугольника цели */
.kb-tooltip {
  position: fixed;
  z-index: 40;
  /* На телефоне потолок — ширина экрана без полей страницы: 336 в неё
     не входят, и тултип уходил за край */
  max-width: min(var(--kb-tooltip-max), calc(100vw - 2 * var(--page-padding-mobile)));
  padding: var(--spacing-xxs) var(--spacing-xs);
  border-radius: var(--radius-s);
  background: var(--color-tooltip-surface);
  color: var(--color-tooltip-text);
  font-size: var(--text-p3);
  line-height: var(--text-p3--line-height);
  transform: translate(-50%, 0);
  opacity: 0;
  visibility: hidden;
  transition: opacity var(--duration-fast) var(--ease-brand),
              visibility var(--duration-fast) var(--ease-brand);
}
.kb-tooltip.is-visible { opacity: 1; visibility: visible; }
/* Хвостик 12×5 из кита (133:14775). Треугольник рисуется рамкой:
   отдельная нода ради пяти пикселей не нужна */
.kb-tooltip::after {
  content: "";
  position: absolute;
  bottom: 100%;
  /* У края экрана тултип прижат скриптом, а хвостик остаётся под целью:
     сдвиг скрипт кладёт в --kb-tooltip-tail-x */
  left: var(--kb-tooltip-tail-x, 50%);
  transform: translateX(-50%);
  /* Половина ширины с каждой стороны даёт основание 12 при высоте 5 */
  border: calc(var(--kb-tooltip-tail) + 1px) solid transparent;
  border-top: 0;
  border-bottom: var(--kb-tooltip-tail) solid var(--color-tooltip-surface);
}

/* ── Кнопка якоря у заголовка ──────────────────────────────────────────────
   Перенесена с лендинга (.doc__anchor в doc.css): компонент один и тот же,
   разъезжаться им незачем. Отличие одно — здесь она есть у h2, h3 и h4,
   а в договоре была у пунктов.

   Зона клика шире рисованного квадрата на 8 с каждой стороны и на всю
   высоту заголовка: вести курсор точно в квадрат 22×22 неудобно.
   Подложку рисует ::before размером в саму кнопку */
/* opacity, а не display: none. Скрытый через display элемент выпадает из
   порядка обхода, и до кнопки нельзя добраться с клавиатуры — а показывалась
   она как раз по :focus-visible, то есть условие было мёртвым.
   pointer-events выключены, пока кнопка невидима: иначе по ней можно
   промахнуться мышью в пустое поле справа от заголовка */
.kb-anchor {
  position: absolute;
  left: 100%;
  top: var(--spacing-xl);
  display: flex;
  opacity: 0;
  pointer-events: none;
  align-items: flex-start;
  justify-content: center;
  width: calc(var(--size-button-xs) + 2 * var(--spacing-xs));
  padding: 0;
  border: 0;
  background: none;
  color: var(--color-text-primary);
  cursor: pointer;
}
.kb-anchor::before {
  content: "";
  position: absolute;
  top: 0;
  left: var(--spacing-xs);
  width: var(--size-button-xs);
  height: var(--size-button-xs);
  border-radius: var(--radius-s);
  background: var(--color-button-ghost);
  transition: background-color var(--duration-fast) var(--ease-brand);
}
@media (hover: hover) {
  .kb-anchor:hover::before { background: var(--color-button-ghost-hover); }
}
.kb-anchor:active::before { background: var(--color-button-ghost-pressed); }
.kb-anchor:focus-visible { outline: 0; }
.kb-anchor:focus-visible::before {
  outline: var(--focus-ring-width) solid var(--color-brand);
  outline-offset: var(--spacing-xxs);
}
.kb-anchor .icon {
  position: relative;   /* над подложкой: ::before идёт первым в потоке слоёв */
  width: var(--size-button-icon-s);
  height: var(--size-button-icon-s);
  /* Иконка встаёт по центру рисованного квадрата, а не по краю зоны */
  margin-top: calc((var(--size-button-xs) - var(--size-button-icon-s)) / 2);
  opacity: 0.5;
}
@media (hover: hover) {
  .kb-anchor:hover .icon { opacity: 1; }
}

/* Кнопка живёт только на десктопе, где её проявляет наведение. На планшете
   и телефоне наводить нечем: кнопка либо висела бы у каждого заголовка
   постоянно, споря с ним за внимание, либо всплывала от случайного касания.
   Адрес раздела там берут из строки браузера */
@media (max-width: 1279px) {
  .kb-anchor { display: none; }
}

/* Показывается при наведении на заголовок и при фокусе с клавиатуры */
.kb-anchor:focus-visible {
  opacity: 1;
  pointer-events: auto;
}
@media (hover: hover) {
  .payload-richtext :where(h2, h3, h4):hover .kb-anchor {
    opacity: 1;
    pointer-events: auto;
}
}

/* ══ СПИСКИ ═══════════════════════════════════════════════════════════════
   Устройство одно на оба вида и на любую глубину: маркер стоит в своей
   колонке слева, от него вниз идёт линия до конца пункта, у последнего
   пункта линии нет.

   Линия не украшение. Пункт инструкции почти всегда содержит не только
   строку текста: под ней лежат скриншот, заголовок, код. Линия держит
   взглядом, где кончается шаг.

   Одно число задаёт всю сетку — --kb-marker-col. Оно же ширина колонки
   маркера, оно же шаг вложенности: каждый следующий уровень отодвигается
   ровно на колонку, поэтому зазор маркер→текст одинаков на всех уровнях.
   Глубина не ограничена: правило вложенности рекурсивно, пятый уровень
   встанет ровно так же, как второй. Ограничивать стоит редактору, а не
   стилям: третий уровень в справке обычно значит, что инструкцию пора
   делить на статьи.

   Первый уровень отличается только тем, что его колонка вынесена влево ЗА
   колонку статьи (48): маркеры висят в поле слева, и текст не теряет
   ширину. Внутри списка колонка обычная.

   Маркеры рисуются абсолютом, а не list-style: только так внутрь пункта
   встают картинка, код и заголовок, не ломая колонку номеров */
.payload-richtext :where(ol, ul) {
  --kb-marker: 24px;                 /* сам маркер: квадрат номера, 1171:8612 */
  --kb-marker-col: 36px;             /* колонка маркера, она же шаг вложенности */
  /* --kb-marker-out — вынос колонки наружу на первом уровне — объявлен на
     корне: под него держит поле .kb-main */
  list-style: none;
  padding: 0;
}

/* Первый уровень: колонка маркеров уходит влево за край статьи. В макете
   сам маркер стоит на 12 от края выносной колонки 48, то есть его центр —
   на 24 слева от текста. Ровно это и даёт колонка 36 */
.payload-richtext > :where(ol, ul) {
  margin-left: calc(-1 * var(--kb-marker-out));
  padding-left: var(--kb-marker-out);
}
/* Вложенный список стоит внутри колонки, наружу не вылезает. Селектор
   потомка, а не дочернего элемента: отступ так копится сам на каждом
   уровне, и глубина не упирается в число написанных правил */
.payload-richtext :where(ol, ul) :where(ol, ul) {
  margin-left: 0;
  padding-left: var(--kb-marker-col);
}

.payload-richtext li {
  position: relative;
  padding-block: var(--spacing-s);
}

/* Маркер и линия висят на одной вертикальной оси — центре колонки маркера.
   Через translateX они центрируются по ней сами, поэтому номер, точка и
   линия держат общую вертикаль независимо от собственной ширины: номер
   «100» шире номера «1», но ось у них одна, и линия не идёт ступенькой */
.payload-richtext li::before,
.payload-richtext li::after {
  content: "";
  position: absolute;
  left: calc(var(--kb-marker) / 2 - var(--kb-marker-col));
  transform: translateX(-50%);
}
/* Линия: от низа маркера до середины следующего маркера. Останавливать её
   у края маркера нельзя: край круглый, и между линией и номером остаётся
   светлая щель. Заходит под маркер она безнаказанно — следующий пункт
   стоит ниже по разметке, и его маркер рисуется поверх.

   У последнего пункта линии нет — вести её в пустоту не за чем */
.payload-richtext li::after {
  top: calc(var(--spacing-s) + var(--kb-marker));
  bottom: calc(-1 * (var(--spacing-s) + var(--kb-marker) / 2));
  width: var(--size-stroke-m);
  background: var(--color-neutral-05);
}
.payload-richtext li:last-child::after { content: none; }

/* Номер: плашка с цифрой. Цифру даёт счётчик списка, а не разметка —
   тогда её не нужно дублировать в атрибуте value.

   Встроенный счётчик list-item сюда не годится, хотя сам умеет и start,
   и value: в ::before он читается ДО прибавления, и весь список
   нумеруется на единицу меньше — первый пункт получает ноль. Свой
   счётчик такой особенности не имеет.

   Ширина не фиксированная, а минимальная: двузначный номер раздвигает
   плашку вместо того, чтобы вылезти цифрой за фон. Растёт она в обе
   стороны от оси, так что колонка и текст с места не двигаются */
.payload-richtext ol { counter-reset: kb-step; }
.payload-richtext ol > li { counter-increment: kb-step; }
.payload-richtext ol > li::before {
  content: counter(kb-step);
  top: var(--spacing-s);
  display: grid;
  place-items: center;
  min-width: var(--kb-marker);
  height: var(--kb-marker);
  padding-inline: var(--spacing-xxs);
  border-radius: var(--radius-l);
  background: var(--color-neutral-05);
  font-size: var(--text-p1);
  line-height: var(--text-p1--line-height);
  font-weight: var(--font-weight-body-medium);
  color: var(--color-text-primary);
}
/* Движок проставляет номер в атрибуте value — там, где он есть, он и
   выигрывает: нумерация статьи может начинаться не с единицы */
.payload-richtext ol > li[value]::before { content: attr(value); }

/* Точка: круг по центру первой строки пункта. Центр строки — половина
   интерлиньяжа, а не половина маркера: у пункта в две строки точка
   обязана стоять у первой */
.payload-richtext ul > li::before {
  top: calc(var(--spacing-s) + (var(--text-p1--line-height) - var(--size-dot)) / 2);
  width: var(--size-dot);
  height: var(--size-dot);
  border-radius: var(--radius-full);
  background: var(--color-text-primary);
}
/* Линия маркированного списка стартует от точки, а не от квадрата, и
   доходит до середины точки следующего пункта: та стоит по центру первой
   строки, то есть ниже границы пункта на поле плюс полстроки.

   Верх линии — нижний край точки, а не низ строки: строка выше точки на
   8, и линия начиналась с зазором, будто не доходит до пункта */
.payload-richtext ul > li::after {
  top: calc(var(--spacing-s) + (var(--text-p1--line-height) + var(--size-dot)) / 2);
  bottom: calc(-1 * (var(--spacing-s) + var(--text-p1--line-height) / 2));
}

/* ══ БЛОКИ ════════════════════════════════════════════════════════════════
   Всё ниже редактор Payload сам отдать не может — это кастомные блоки,
   которые разработчику предстоит завести в CMS. У каждого написано, какие
   поля для этого нужны.

   Геометрия у всех одна — скругление 20 со сглаживанием и поле 16, — но
   общего класса-оболочки под неё нет намеренно: он был, им никто не
   пользовался, а каждый блок всё равно задаёт её сам. Появится пятый блок
   с той же рамкой — тогда и вынести, по факту, а не на всякий случай */

/* ── Карточка ──────────────────────────────────────────────────────────────
   Содержимое свободное: абзацы, списки, ссылки — что угодно. Никакой
   зашитой структуры вроде «первое слово жирным» в элементе нет, это пишет
   автор в теле карточки.

   Цвет выбирает автор. Смысл за цветами стоит закрепить редакционным
   правилом, иначе через полгода красные карточки будут стоять на
   безобидных примечаниях, — но в вёрстку это правило не зашивается.

   Поля из CMS: цвет (три значения) и richtext, урезанный до текста —
   абзац, выделение, ссылка. Картинки, списки и вложенные блоки карточка
   не берёт: в цветной рамке они читаются как экран внутри экрана.

   Голый <blockquote> из редактора получает вид голубой карточки: так он
   и используется в нынешних статьях. Жёлтую и красную без правки CMS
   получить нельзя */
.kb-card,
.payload-richtext > blockquote {
  /* Сброс выше снимает только margin-block: браузерные 40 по бокам у
     blockquote оставались, и карточка была уже колонки на 80 */
  margin-inline: 0;
  padding: var(--spacing-m);
  border-radius: var(--radius-xl);
  corner-shape: var(--corner-smooth);
  background: var(--color-callout-info);
}
.kb-card--warning { background: var(--color-callout-warning); }
.kb-card--error { background: var(--color-callout-error); }


/* ── Карточка с автором ────────────────────────────────────────────────────
   Берётся под личное мнение, комментарий сотрудника, объяснение «почему
   так сделано» — всё, за что отвечает человек, а не документация. Под
   обычное примечание не берётся: подпись сотрудника под фактом из
   документации выглядит как перекладывание ответственности.

   Поля из CMS: автор (аватар, имя, должность) и richtext — тоже только
   текст: реплика человека остаётся репликой, а не вложенной инструкцией */
.kb-quote {
  display: grid;
  grid-template-columns: var(--size-ava-l) minmax(0, 1fr);
  /* Аватар стоит не у текста, а у подписи — на одном уровне с именем, как
     у автора статьи в подвале. Текст цитаты занимает обе колонки: портрет
     сбоку от него отбирал бы у строки 54 пикселя ширины */
  grid-template-areas:
    "text text"
    "ava  author";
  gap: var(--spacing-m);
  padding: var(--spacing-m);
  border: var(--size-stroke-m) solid var(--color-border);
  border-radius: var(--radius-xl);
  corner-shape: var(--corner-smooth);
  background: var(--color-surface);
}
.kb-quote__avatar {
  grid-area: ava;
  align-self: center;
  width: var(--size-ava-l);
  height: var(--size-ava-l);
  border-radius: var(--radius-full);
  object-fit: cover;
}
.kb-quote__body { grid-area: text; }
/* Имя и должность встают по центру портрета: у подписи две строки, у
   портрета одна высота, и по верхнему краю они бы разъехались */
.kb-quote__author {
  grid-area: author;
  align-self: center;
}
.kb-quote__name {
  font-size: var(--text-p1);
  line-height: var(--text-p1--line-height);
  font-weight: var(--font-weight-body-medium);
}
.kb-quote__role {
  font-size: var(--text-p3);
  line-height: var(--text-p3--line-height);
  color: var(--color-text-secondary);
}

/* ── Картинка ──────────────────────────────────────────────────────────────
   Обёртка серая, картинка занимает её верх целиком, подпись лежит под
   картинкой полем 24 — получается полка, на которой стоит скриншот.

   Поля из CMS: файл, обязательный alt, необязательная подпись и реальные
   размеры файла (см. ниже, зачем).

   Обводка блока — 2px Basic/Stroke/Normal (1171:8697), заливка под
   изображением — Berlin/03: картинка не доходит до края блока, и по кромке
   остаётся полка. Она прячет то, что у скриншотов на краю всегда мусорное —
   артефакты JPEG и чужую рамку окна.

   Именно border, а не inset-тень: у замещённых элементов (img, iframe)
   внутренняя тень рисуется ПОД содержимым, картинка её перекрывает, и
   обводки не видно вовсе.

   Скругление и обводка живут на обёртке, картинка внутри — прямоугольная
   (1171:8698 против 1171:8699): углы ей срезает overflow обёртки. Двойное
   скругление — на обёртке и на самой картинке — даёт разный радиус на
   внешнем и внутреннем крае, и по кромке проступает светлая скобка */
.kb-figure,
.payload-richtext > figure {
  margin-inline: 0;   /* браузерные 40 по бокам; вертикаль держит полоса */
  border: var(--size-stroke-m) solid var(--color-border);
  border-radius: var(--radius-xl);
  corner-shape: var(--corner-smooth);
  background: var(--color-neutral-03);
  overflow: hidden;
}
.kb-figure img,
.payload-richtext > figure img,
.payload-richtext > p > img,
.payload-richtext > img {
  display: block;
  width: 100%;
  height: auto;
  /* Потолок высоты: вертикальный скриншот телефона иначе занимает три
     экрана прокрутки. Полный размер открывается по клику */
  max-height: var(--kb-image-max-height);
  object-fit: contain;
}
/* Увеличение открывает весь кадр, поэтому и курсор на нём: поля вокруг
   картинки — та же кнопка, что и она сама. Подпись из зоны исключена */
.kb-figure,
.payload-richtext > figure,
.kb-gallery__slide,
.payload-richtext > p > img,
.payload-richtext > img { cursor: zoom-in; }
.kb-figure figcaption,
.payload-richtext figcaption { cursor: auto; }
/* Картинка без обёртки: скруглять и обводить некому, поэтому оба свойства
   берёт она сама — как кадр одиночного видео (1171:8844) */
.payload-richtext > p > img,
.payload-richtext > img {
  border: var(--size-stroke-m) solid var(--color-border);
  border-radius: var(--radius-xl);
  corner-shape: var(--corner-smooth);
}
.kb-figure figcaption,
.payload-richtext figcaption {
  padding: var(--spacing-m);
  font-size: var(--text-p1);
  line-height: var(--text-p1--line-height);
}

/* Узкая картинка (уже колонки) сейчас растягивается на всю ширину и мылится.
   Правило под это было — .kb-figure--small, — но включать его некому: порог
   должна ставить CMS, сравнивая ширину файла с колонкой, а CMS ещё нет.
   Возвращать так: селектор обязан начинаться с тега (figure.kb-figure--small
   img), иначе он проигрывает по весу общему .payload-richtext > figure img
   и молча не работает — на этом уже попались один раз */

/* Поля вокруг картинки, которая не заполняет свой кадр — вертикальный
   скриншот упёрся в потолок высоты, кадр карусели ниже соседнего, — 
   заполняются её же размытой копией: цвет полей сам собой совпадает с
   картинкой. Второй запрос в сеть не уходит, тот же src берётся из кэша.
   Копию ставит скрипт и только там, где поля реально появились; от чтения
   с экрана она спрятана.

   Класс общий для картинки в статье и для кадра карусели: правило одно,
   кадр разный */
.kb-letterbox { position: relative; }
/* Картинка и подпись ложатся поверх копии: позиционированный фон иначе
   перекрыл бы их, оставшись в потоке выше. Подписи вдобавок нужен свой
   фон — размытие не должно заезжать под текст */
.kb-letterbox img:not(.kb-letterbox__bg) { position: relative; }
.kb-letterbox figcaption {
  position: relative;
  background: var(--color-neutral-03);
}
/* Селектор из двух классов не случаен: правила картинки в блоке
   (.kb-figure img, .kb-gallery__slide img) специфичнее одиночного класса и
   перебивали подложке и высоту, и object-fit — она вставала ростом с саму
   картинку и закрывала поля только сверху */
.kb-letterbox .kb-letterbox__bg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  max-height: none;
  object-fit: cover;
  filter: blur(var(--kb-letterbox-blur))
          saturate(var(--kb-letterbox-saturate))
          brightness(var(--kb-letterbox-dim));
  transform: scale(var(--kb-letterbox-zoom));
  border: 0;
  border-radius: 0;
  cursor: auto;
  pointer-events: none;   /* клик по полям открывает картинку, а не копию */
}

/* ── Таблица ───────────────────────────────────────────────────────────────
   Внутренних вертикальных линий нет: колонок мало, и без них строка
   читается как строка, а не как решётка.

   На узком экране таблица не разваливается в карточки, а скроллится
   внутри рамки: таблицу параметров делают ради сравнения строк взглядом,
   карточки это убивают */
.kb-table-wrap {
  border: var(--size-stroke-m) solid var(--color-border);
  border-radius: var(--radius-xl);
  corner-shape: var(--corner-smooth);
  overflow-x: auto;
  /* Без перетяга за край: на iOS прокрутка пружинит, и таблица на секунду
     уезжала из-под рамки — заливка шапки и линии строк обрывались, а справа
     в рамке зияла пустота. Читалось как обрезанная таблица */
  overscroll-behavior-x: none;
}
.kb-table,
.payload-richtext table {
  width: 100%;
  min-width: var(--kb-table-min);
  border-collapse: collapse;
  font-size: var(--text-p1);
  line-height: var(--text-p1--line-height);
}
.kb-table th,
.payload-richtext table th {
  padding: var(--spacing-m);
  background: var(--color-neutral-03);
  color: var(--color-text-secondary);
  font-weight: var(--font-weight-body);
  text-align: left;
}
/* Имя поля не рвётся по буквам: на узком экране колонка ужималась до
   «quer/y_id», и таблица переставала читаться вместо того, чтобы
   прокрутиться внутри рамки — ради этого у неё и стоит overflow */
.kb-table code,
.payload-richtext table code { white-space: nowrap; }

/* Линия между строк тоньше внешней рамки: в макете (1171:8795) контур
   таблицы — Stroke/M, а разделители строк — Stroke/S. Толстая линия
   внутри читалась бы как граница отдельного блока */
.kb-table td,
.payload-richtext table td {
  padding: var(--spacing-m);
  border-top: var(--size-stroke-s) solid var(--color-border);
  vertical-align: top;
}
/* Последняя строка отбивается снизу сильнее: у промежуточных воздух
   держит линия следующей строки, а у последней её нет, и текст лип бы
   к скруглению рамки */
.kb-table tr:last-child td,
.payload-richtext table tr:last-child td {
  padding-bottom: var(--spacing-m);
}

/* ── Блок кода ─────────────────────────────────────────────────────────────
   Переносов нет: строка кода со сдвинутым переносом читается хуже, чем
   строка, уехавшая за край. Длинные строки скроллятся внутри блока */
.kb-code {
  position: relative;
  padding: var(--spacing-m);
  border: var(--size-stroke-m) solid var(--color-border);
  border-radius: var(--radius-xl);
  corner-shape: var(--corner-smooth);
  background: var(--color-neutral-03);
}
.kb-code pre,
.payload-richtext pre {
  margin: 0;
  /* Прокручиваемая область растянута на поля блока, а поля вернулись ей
     внутрь. Иначе длинная строка обрывалась за 16 до края блока: буквы
     срезаны, а дальше пустая полоса — будто текст оборвали посреди слова.
     Теперь строка уходит под самый край, а в конце прокрутки остаётся
     то же поле 16, что и в начале */
  margin-inline: calc(-1 * var(--spacing-m));
  padding-inline: var(--spacing-m);
  overflow-x: auto;
  font-family: var(--font-mono);
  font-size: var(--text-mono);
  line-height: var(--text-mono--line-height);
  font-weight: var(--font-weight-body-medium);
  color: var(--color-text-primary);
  tab-size: 4;
}
/* Отбивка от строки вкладок. Считать по :first-child нельзя: перед <pre>
   стоит кнопка копирования, и блок без вкладок получал бы её ни за что */
.kb-tabs ~ pre { margin-top: var(--spacing-l); }
/* Кнопка копирования в углу: рисованный квадрат 30 стоит на 8 от края
   блока, как в макете (1171:8823).

   Зона клика шире рисованной кнопки — 46 против 30. Целиться курсором в
   квадрат 30 у самого угла блока неудобно, а места вокруг него всё равно
   никто не занимает. Подложку рисует ::before, поэтому на курсор отзывается
   именно квадрат, а не вся зона.

   Кнопка есть везде, включая телефон: выделить пальцем строку кода в
   прокручиваемом блоке — задача не для читателя справки */
.kb-code__copy {
  position: absolute;
  top: 0;
  right: 0;
  display: grid;
  place-items: center;
  width: var(--size-button-l);
  height: var(--size-button-l);
  border: 0;
  background: none;
  /* Вариант Opacity из кита (1171:8823): иконка приглушена, а не чёрная —
     кнопка лежит поверх кода и не должна спорить с ним за внимание */
  color: var(--color-button-opacity-text);
  cursor: pointer;
}
.kb-code__copy::before {
  content: "";
  position: absolute;
  width: var(--size-button-s);
  height: var(--size-button-s);
  border-radius: var(--radius-l);
  background: var(--color-button-ghost);
  transition: background-color var(--duration-fast) var(--ease-brand);
}
@media (hover: hover) {
  .kb-code__copy:hover::before { background: var(--color-button-ghost-hover); }
}
.kb-code__copy:active::before { background: var(--color-button-ghost-pressed); }
/* Кольцо фокуса обводит кнопку, а не всю зону клика */
.kb-code__copy:focus-visible { outline: 0; }
.kb-code__copy:focus-visible::before {
  outline: var(--focus-ring-width) solid var(--color-brand);
  outline-offset: var(--spacing-xxs);
}
.kb-code__copy .icon {
  position: relative;   /* над подложкой: ::before идёт первым в потоке слоёв */
  width: var(--size-button-icon-s);
  height: var(--size-button-icon-s);
}

/* ── Вкладки ───────────────────────────────────────────────────────────────
   Компонент .Tab Item из кита (3173:20490). До пяти штук. Используется в
   двух местах — код и видео, — поэтому живёт отдельным блоком.

   ⚠️ Подложка наведения добавлена сверх кита, по решению Александра: в
   самом ките у неактивной вкладки меняется только цвет подписи, фона нет
   ни в одном из 18 вариантов. Берётся та же подложка, что у остальных
   нажимаемых элементов страницы. Расхождение стоит закрыть правкой Figma */
.kb-tabs {
  display: flex;
  width: max-content;
  max-width: 100%;
  overflow-x: auto;
  border: var(--size-stroke-m) solid var(--color-border);
  border-radius: var(--radius-l);
}
.kb-tab {
  flex: none;
  display: flex;
  align-items: center;
  gap: var(--spacing-xs);
  height: var(--size-button-m);
  padding-inline: var(--spacing-m);
  border: 0;
  border-right: var(--size-stroke-m) solid var(--color-border);
  background: none;
  color: var(--color-text-secondary);
  /* Кнопка не наследует шрифт страницы: без этой строки браузер рисует
     вкладки своим Arial, а не Geist */
  font-family: inherit;
  font-size: var(--text-p2);
  line-height: var(--text-p2--line-height);
  font-weight: var(--font-weight-body-medium);
  cursor: pointer;
  transition: background-color var(--duration-fast) var(--ease-brand),
              color var(--duration-fast) var(--ease-brand);
}
.kb-tab:last-child { border-right: 0; }
@media (hover: hover) {
  .kb-tab:hover {
    background: var(--color-button-ghost-hover);
    color: var(--color-text-secondary-hover);
}
}
.kb-tab:active {
  background: var(--color-button-ghost-pressed);
  color: var(--color-text-secondary-pressed);
}
.kb-tab[aria-selected="true"] {
  background: var(--color-surface);
  color: var(--color-text-primary);
}
.kb-tab:disabled {
  color: var(--color-disabled);
  cursor: not-allowed;
}

/* ── Видео ─────────────────────────────────────────────────────────────────
   Вкладки площадок появляются только если площадок несколько: выбирать
   не из чего, а пустая строка вкладок занимает 62 пикселя ни за чем.
   Название вкладки скрипт берёт из ссылки — автору его вводить не нужно.

   Поля из CMS: массив ссылок. Всё остальное вёрстка и скрипт делают сами.

   Кадр обведён так же, как блок картинки: 2px Basic/Stroke/Normal */
/* Кадр и полоса площадок лежат в одной рамке вплотную (1171:8839): кадр
   сверху во всю ширину, вкладки под ним. Своего поля у карточки нет —
   рамка обводит кадр, а не отступ вокруг него, — и внутренней рамки у
   кадра тоже: её роль играет внешняя. overflow: clip держит углы кадра
   скруглёнными по рамке */
.kb-video {
  border: var(--size-stroke-m) solid var(--color-border);
  border-radius: var(--radius-xl);
  corner-shape: var(--corner-smooth);
  overflow: clip;
  background: var(--color-surface);
}
.kb-video__frame {
  position: relative;
  aspect-ratio: 16 / 9;
}
/* Полоса площадок: во всю ширину карточки, вкладки равными долями. Своей
   рамки у неё нет — только линия, отделяющая её от кадра. На узком экране
   подписи не жмутся, а полоса прокручивается: «Kinescope» в четверти
   телефонного экрана превратился бы в «Kine…» */
.kb-video .kb-tabs {
  width: 100%;
  max-width: none;
  border: 0;
  border-top: var(--size-stroke-m) solid var(--color-border);
  border-radius: 0;
}
.kb-video .kb-tab {
  /* Доли равные, но не уже своей подписи: min-width держит нижний предел,
     и когда четыре названия перестают помещаться, полоса прокручивается */
  flex: 1 0 0;
  min-width: max-content;
  justify-content: center;
  white-space: nowrap;
}
/* Пока плеера нет, кадр — обычная картинка, и по ней не видно, что это
   видео. Треугольник поверх превью это и говорит; после клика в кадре
   стоит iframe, и кнопка пропадает вместе с превью */
.kb-video__frame:not(:has(iframe)) { cursor: pointer; }
.kb-video__frame:not(:has(iframe))::before,
.kb-video__frame:not(:has(iframe))::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  pointer-events: none;
}
.kb-video__frame:not(:has(iframe))::before {
  width: var(--size-button-l);
  height: var(--size-button-l);
  border-radius: var(--radius-full);
  background: var(--color-overlay-l);
  backdrop-filter: blur(var(--blur-s));
  -webkit-backdrop-filter: blur(var(--blur-s));
}
.kb-video__frame:not(:has(iframe))::after {
  width: var(--size-button-icon-m);
  height: var(--size-button-icon-m);
  background: var(--color-text-on-primary);
  -webkit-mask: var(--icon-play) center / contain no-repeat;
  mask: var(--icon-play) center / contain no-repeat;
}
.kb-video__frame iframe,
.kb-video__frame img {
  /* block, а не inline: у строчного бокса под картинкой остаётся место под
     нижние выносные, и кадр становился на 6 пикселей выше своих 16:9 */
  display: block;
  width: 100%;
  height: 100%;
  border: 0;
  object-fit: cover;
}
/* ── Кнопка-ссылка ─────────────────────────────────────────────────────────
   Берётся под переход, который сам является частью инструкции: «дальше
   читайте вот это». Под упоминание по ходу текста не берётся — пять таких
   кнопок подряд превращают статью в оглавление.

   Поля из CMS: заголовок, необязательное описание, необязательная иконка,
   адрес */
.kb-linkbutton {
  display: grid;
  grid-template-columns: minmax(0, 1fr) var(--size-button-icon-m);
  align-items: center;
  gap: var(--spacing-m);
  padding: var(--spacing-m);
  border: var(--size-stroke-m) solid var(--color-border);
  border-radius: var(--radius-xl);
  corner-shape: var(--corner-smooth);
  background: var(--color-surface);
  color: inherit;
  text-decoration: none;
  transition: background-color var(--duration-fast) var(--ease-brand);
}
@media (hover: hover) {
  .kb-linkbutton:hover { background: var(--color-surface-hover); }
}
.kb-linkbutton:active { background: var(--color-surface-pressed); }
.kb-linkbutton__title {
  display: block;   /* span сам по себе строчный, а заголовок и описание
                       обязаны стоять друг под другом */
  font-size: var(--text-p1);
  line-height: var(--text-p1--line-height);
  font-weight: var(--font-weight-body-medium);
  color: var(--color-text-primary);
}
.kb-linkbutton__text {
  display: block;
  margin-top: var(--spacing-xs);
  font-size: var(--text-p2);
  line-height: var(--text-p2--line-height);
  /* Regular, а не наследуемый Medium заголовка (1171:8824): описание —
     пояснение под подписью, и весом с ней спорить ему незачем */
  font-weight: var(--font-weight-body);
  color: var(--color-text-secondary);
}
/* Иконки в кнопке-ссылке чёрные, а не синие: ссылку красит текст, а
   стрелка и значок раздела — метки, а не часть подписи */
.kb-linkbutton .icon { color: var(--color-text-primary); }
/* Стрелка стоит в правом верхнем углу, а не по центру карточки (1171:8824):
   поля у неё те же 16, что у всей кнопки, — их держит её padding */
.kb-linkbutton__arrow {
  flex: none;
  align-self: start;
}
/* С иконкой слева: третья колонка встаёт первой, поле слева не трогаем —
   его добирает сама иконка со своим зазором */
.kb-linkbutton--icon {
  grid-template-columns: var(--size-button-icon-m) minmax(0, 1fr) var(--size-button-icon-m);
}

/* ── Вопрос-ответ ──────────────────────────────────────────────────────────
   На <details>: работает без скрипта, содержимое индексируется поисковиком
   даже свёрнутым, клавиатура работает сама. Скрипт нужен только для
   плавной высоты.

   Поля из CMS: вопрос и richtext ответа */
.kb-faq {
  border: var(--size-stroke-m) solid var(--color-border);
  border-radius: var(--radius-xl);
  corner-shape: var(--corner-smooth);
  background: var(--color-surface);
  transition: background-color var(--duration-fast) var(--ease-brand);
}
/* Подсветка только под курсором: в макете (1171:8850) свёрнутый вопрос
   такой же белый, как развёрнутый */
@media (hover: hover) {
  .kb-faq:not([open]):hover { background: var(--color-surface-hover); }
}
.kb-faq > summary {
  display: grid;
  grid-template-columns: minmax(0, 1fr) var(--size-button-icon-m);
  align-items: center;
  gap: var(--spacing-m);
  padding: var(--spacing-m);
  font-size: var(--text-p1);
  line-height: var(--text-p1--line-height);
  font-weight: var(--font-weight-body-medium);
  cursor: pointer;
  list-style: none;   /* убираем родной треугольник */
}
.kb-faq > summary::-webkit-details-marker { display: none; }
.kb-faq__chevron {
  transform: rotate(90deg);   /* шеврон нарисован вправо, здесь смотрит вниз */
  transition: transform var(--duration-fast) var(--ease-brand);
}
.kb-faq[open] .kb-faq__chevron { transform: rotate(-90deg); }
.kb-faq__body {
  padding: 0 var(--spacing-m) var(--spacing-m);
}

/* ── Плитки главной ────────────────────────────────────────────────────────
   Сетка ссылок под заголовком главной (1171:10757). Три в ряд там, где
   колонка позволяет; сколько влезет — там, где нет. Ступеней под это не
   заведено намеренно: колонка статьи всего 800, и своих ширин у сетки,
   кроме минимума плитки, не бывает.

   Знак у плитки — иконка на серой подложке или эмодзи вместо неё:
   подложка одна и та же, меняется только начинка. Первая плитка живёт
   по своим правилам (1227:18916): синяя рамка, голубой градиент и эмодзи
   без подложки. Это вход для новичка, а не такая же ссылка, как соседние.

   Поля из CMS: список плиток — знак (иконка или эмодзи), подпись, адрес;
   у первой отдельным признаком «выделенная» */
.kb-tiles {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--spacing-l);
  margin-block: var(--spacing-s);
}
.kb-tile {
  display: flex;
  flex-direction: column;
  gap: var(--spacing-m);
  padding: var(--spacing-l);
  border: var(--size-stroke-m) solid var(--color-border);
  border-radius: var(--radius-xl);
  corner-shape: var(--corner-smooth);
  background: var(--color-surface);
  color: inherit;
  text-decoration: none;
  transition: background-color var(--duration-fast) var(--ease-brand),
              border-color var(--duration-fast) var(--ease-brand);
}
@media (hover: hover) {
  .kb-tile:hover {
    background: var(--color-surface-hover);
    border-color: var(--color-border-hover);
  }
}
.kb-tile:active {
  background: var(--color-surface-pressed);
  border-color: var(--color-border-hover);
}

/* Подпись прижата к низу: плитки в ряду одной высоты, знак стоит сверху,
   и пустота копится между ними, а не под подписью (1227:18903) */
.kb-tile__title {
  margin-top: auto;
  /* Подпись рвём по буквам только тогда, когда слово не влезает целиком.
     У статьи стоит overflow-wrap: anywhere ради длинных идентификаторов,
     но он учитывается ещё и в расчёте ширины — в узкой плитке на телефоне
     это рвало обычное «подключить» посреди слова при запасе в пиксель */
  overflow-wrap: break-word;
  font-size: var(--text-h3);
  line-height: var(--text-h3--line-height);
  letter-spacing: var(--text-h3--letter-spacing);
  font-weight: var(--font-weight-heading);
  color: var(--color-text-primary);
}

/* Подложка знака — та же, что у вторичной кнопки. Внутри чёрная иконка
   и ничего кроме: эмодзи на этой подложке не ставится, у него своё
   место — плитка со стикером */
.kb-tile__icon {
  display: flex;
  flex: none;
  align-items: center;
  justify-content: center;
  width: var(--size-button-l);
  height: var(--size-button-l);
  border-radius: var(--radius-l);
  corner-shape: var(--corner-smooth);
  background: var(--color-button-secondary);
}
.kb-tile__icon .icon { color: var(--color-text-primary); }

/* Выделенная плитка. Градиент обрывается на 45.6% — так он стоит в
   макете: верхняя половина белая, цвет набирается только к низу */
.kb-tile--start {
  border-color: var(--color-info);
  background: linear-gradient(to bottom,
    var(--color-surface) 45.6%, var(--color-info-05));
}
@media (hover: hover) {
  .kb-tile--start:hover {
    border-color: var(--color-info-120);
    background: linear-gradient(to bottom,
      var(--color-info-01) 45.6%, var(--color-info-11));
  }
}
.kb-tile--start:active {
  border-color: var(--color-info-120);
  background: linear-gradient(to bottom,
    var(--color-info-01) 45.6%, var(--color-info-11));
}

/* Знак-стикер: ролик Lottie без подложки, ростом с неё. Пока ролик не
   приехал, внутри стоит обычный эмодзи — им же страница обходится, если
   скрипт не отработал вовсе. Кегль взят на ступень ниже размера бокса:
   глиф рисуется крупнее своего кегля */
.kb-tile__sticker {
  flex: none;
  align-self: start;   /* иначе тянется во всю ширину плитки */
  width: var(--size-button-l);
  height: var(--size-button-l);
  font-size: var(--size-button-icon-xl);
  line-height: var(--size-button-l);
}
.kb-tile__sticker svg { display: block; }

/* ══ ПОДВАЛ СТАТЬИ ════════════════════════════════════════════════════════ */

.kb-article-footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--spacing-l);
  margin-top: var(--spacing-xl);
  padding: var(--spacing-xl) var(--spacing-l) 0;
  border-top: var(--size-stroke-m) solid var(--color-divider);
  /* Оценка стоит рядом с автором на любой ширине — она не переносится
     строкой ниже. Когда места перестаёт хватать, уходит подпись, а кнопки
     остаются: спрашивать «была ли страница полезна» словами приятнее, но
     палец вверх и палец вниз понятны и без вопроса.

     Порог считает контейнерный запрос, а не медиа: подвал живёт и в колонке
     800, и в колонке 358, а ширина окна об этом ничего не говорит */
  container-type: inline-size;
}
/* 560 — замеренная сумма: автор с аватаром 186, подпись с двумя пальцами
   348, зазор между ними 24. Ниже этого подпись не просто жмётся, а начинает
   ломать имя автора на две строки — тогда она и уходит */
@container (max-width: 559px) {
  .kb-rate__label { display: none; }
}
.kb-author {
  display: flex;
  align-items: center;
  gap: var(--spacing-m);
}
.kb-author img {
  width: var(--size-ava-l);
  height: var(--size-ava-l);
  border-radius: var(--radius-full);
  object-fit: cover;
}
.kb-author__name {
  font-size: var(--text-p1);
  line-height: var(--text-p1--line-height);
  font-weight: var(--font-weight-body-medium);
}
.kb-author__role {
  font-size: var(--text-p3);
  line-height: var(--text-p3--line-height);
  color: var(--color-text-secondary);
}

/* ── Оценка страницы ───────────────────────────────────────────────────────
   Отправки нет: эндпоинта не существует. Место под один fetch помечено
   в kb.js */
.kb-rate {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--spacing-m);
}
.kb-rate__label {
  font-size: var(--text-p1);
  line-height: var(--text-p1--line-height);
  font-weight: var(--font-weight-body-bold);
}
.kb-rate__btn {
  display: grid;
  place-items: center;
  width: var(--size-button-m);
  height: var(--size-button-m);
  border: 0;
  border-radius: var(--radius-l);
  background: var(--color-button-ghost);
  color: var(--color-text-primary);
  cursor: pointer;
  transition: background-color var(--duration-fast) var(--ease-brand);
}
/* В покое палец чёрный, как в макете (1171:8901): приглушать нечего —
   выбор читается цветом, а наведение подложкой */
@media (hover: hover) {
  .kb-rate__btn:hover { background: var(--color-button-ghost-hover); }
}
/* Пока из кнопки открыт попап, она держит ту же подсветку, что под курсором:
   иначе выехавшая карточка висит сама по себе, без указания, чья она.
   Признак ставит скрипт — CSS про открытый попап не знает */
.kb-rate__btn[data-open] { background: var(--color-button-ghost-hover); }
.kb-rate__btn--up[aria-pressed="true"] { color: var(--color-success); }
.kb-rate__btn--down[aria-pressed="true"] { color: var(--color-error); }

/* Попап с вопросом «как улучшить». Открывается только у пальца вниз:
   у довольного читателя спрашивать нечего */
.kb-rate__popup {
  position: absolute;
  z-index: 30;
  top: calc(100% + var(--spacing-xs));
  right: 0;
  width: var(--kb-popup-width);
  display: flex;
  flex-direction: column;
  /* Подпись отбита от поля на 8, кнопка от поля на 16 (1171:9039) */
  gap: var(--spacing-xs);
  padding: var(--spacing-m);
  border: var(--size-stroke-m) solid var(--color-border);
  border-radius: var(--radius-l);
  background: var(--color-surface);
  box-shadow: var(--shadow-dropdown);
}
.kb-rate__popup[hidden] { display: none; }
/* Внизу страницы попапу не хватает места — тогда он раскрывается вверх */
.kb-rate__popup--up {
  top: auto;
  bottom: calc(100% + var(--spacing-xs));
}
.kb-rate__popup label { font-weight: var(--font-weight-body-medium); }
.kb-rate__popup .btn { margin-top: var(--spacing-xs); }
.kb-rate__popup textarea {
  min-height: var(--kb-textarea-height);
  /* Растёт под текст сама, скрипт считает высоту. Тянуть её за уголок
     нельзя: попап узкий, и растянутое поле вылезает за него */
  max-height: calc(var(--kb-textarea-height) * 2);
  overflow-y: auto;
  resize: none;
  padding: var(--spacing-xs);
  border: var(--size-stroke-m) solid var(--color-border);
  border-radius: var(--radius-l);
  background: transparent;
  color: var(--color-text-primary);
  font: inherit;
  font-size: var(--text-p2);
  line-height: var(--text-p2--line-height);
}
.kb-rate__popup textarea:focus {
  outline: 0;
  border-color: var(--color-brand);
  box-shadow: 0 0 0 var(--focus-ring-width) var(--color-focus);
}

/* iOS увеличивает страницу, когда фокус встаёт в поле мельче 16px, и обратно
   масштаб не возвращает. Порог обходим ступенью шкалы выше него: 15 → 17.
   Правило по указателю, а не по ширине окна: втягивает масштаб именно
   сенсорный браузер. Новое поле базы знаний — добавить сюда же.

   Стоит ниже собственных правил обоих полей нарочно: специфичность у них
   одна, спор решает порядок в файле. Выше по файлу кегль 15 у textarea
   перебивал эти 17, и телефон всё равно увеличивал страницу */
@media (pointer: coarse) {
  .kb-search__field,
  .kb-rate__popup textarea {
    font-size: var(--text-p1);
    line-height: var(--text-p1--line-height);
  }
}

/* ── Карусель ──────────────────────────────────────────────────────────────
   Прокрутка работает без скрипта: scroll-snap плюс свайп пальцем. Скрипт
   только двигает стрелками и красит точки — то есть на телефоне карусель
   останется рабочей, даже если скрипт не загрузится.

   Поля из CMS: массив картинок и одна подпись на всю карусель */
.kb-gallery__frame { position: relative; }
/* Скругления у ленты нет: его держит .kb-figure своим overflow, как и у
   одиночной картинки (1171:8699). Своё скругление на ленте давало второй
   радиус поверх родительского — углы кадров карусели выглядели иначе,
   чем у соседних картинок */
.kb-gallery__track {
  display: flex;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
}
.kb-gallery__track::-webkit-scrollbar { display: none; }
.kb-gallery__track > * {
  flex: 0 0 100%;
  scroll-snap-align: center;
}
/* Кадры разной высоты: лента ростом с самый высокий, остальные встают в
   ней по центру, а пустоту сверху и снизу закрашивает размытая копия */
.kb-gallery__slide {
  position: relative;
  display: grid;
  place-items: center;
  overflow: hidden;
}
.kb-gallery__slide img { max-height: 100%; }
/* Стрелки прижаты к краям картинки. Сама кнопка описана одним правилом
   вместе с «наверх» — см. «Круглая кнопка поверх содержимого».
   На телефоне стрелок нет — там свайп */
.kb-gallery__arrow {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
}
.kb-gallery__arrow--prev { left: var(--spacing-m); }
.kb-gallery__arrow--next { right: var(--spacing-m); }
/* Иконка нарисована вправо: левой стрелке её отражаем, а не заводим
   вторую маску */
.kb-gallery__arrow--prev .icon { transform: scaleX(-1); }
.kb-gallery__arrow[disabled] { display: none; }

/* Точки под кадром (1171:8734): пилюля с размытием, внутри полоска текущего
   кадра и по точке на остальные. На широком экране их нет — там про
   листание говорят стрелки, — а на телефоне стрелок нет, и без точек кадр
   не отличить от обычной картинки */
.kb-gallery__dots {
  position: absolute;
  bottom: var(--spacing-m);
  left: 50%;
  transform: translateX(-50%);
  display: none;
  align-items: center;
  gap: var(--spacing-xs);
  padding: var(--spacing-s);
  border-radius: var(--radius-full);
  /* Подложка та же, что у круглых кнопок — см. правило выше */
  background-color: var(--color-overlay);
  background-image: linear-gradient(var(--color-button-secondary),
                                    var(--color-button-secondary));
  backdrop-filter: blur(var(--blur-s));
  -webkit-backdrop-filter: blur(var(--blur-s));
  pointer-events: none;   /* листают свайпом, а не попаданием в точку 8px */
}
.kb-gallery__dots span {
  width: var(--size-dot);
  height: var(--size-dot);
  border-radius: var(--radius-full);
  /* Белый в треть силы: на тёмной подложке пройденные кадры видно, но они
     не спорят с текущим */
  background: color-mix(in srgb, var(--color-text-on-primary) 30%, transparent);
  transition: width var(--duration-fast) var(--ease-brand);
}
/* Текущий кадр — не точка, а полоска: так видно, где ты в ленте */
.kb-gallery__dots span[data-current] {
  width: calc(var(--size-dot) * 4);
  background: var(--color-text-on-primary);
}

/* ── Просмотр картинки во весь экран ───────────────────────────────────────
   Подложка, картинка по центру, крестик справа сверху и стрелки по бокам,
   когда открыт кадр карусели (1171:8991). Всё своё: сторонний просмотрщик
   приносил свой интерфейс, которого нет в макете, и грузился из сети —
   на медленном канале клик по картинке просто не давал ничего.

   Увеличение пальцами своё, в kb.js: браузерный жест тянет за собой всё
   окно, а диалог лежит в верхнем слое — после закрытия страница осталась бы
   увеличенной.

   ⚠️ Чего нельзя делать никогда: ставить user-scalable=no или
   maximum-scale=1 в мета-теге viewport. Это ломает увеличение на всей
   странице и нарушает требования доступности */
.kb-lightbox {
  width: 100vw;
  max-width: 100vw;
  height: 100dvh;
  max-height: 100dvh;
  padding: 0;
  border: 0;
  /* Затемняет только ::backdrop: положить ту же подложку ещё и на сам
     диалог — значит наложить её дважды и получить 0.84 вместо 0.6 */
  background: transparent;
}
.kb-lightbox[open] {
  display: grid;
  place-items: center;
}
.kb-lightbox::backdrop { background: var(--color-overlay-l); }

/* Кадры лежат лентой: листает их прокрутка со scroll-snap, то есть сам
   браузер — картинка идёт за пальцем, а отпущенная доезжает с родной
   инерцией. Подменять src по свайпу нельзя: кадр менялся бы рывком.

   touch-action: pan-x — горизонтальный жест отдаём браузеру (это и есть
   листание), всё остальное забирает скрипт: жест двумя пальцами он
   разбирает сам, иначе браузер увеличил бы страницу целиком */
.kb-lightbox__track {
  display: flex;
  width: 100vw;
  height: 100dvh;
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
  overscroll-behavior: contain;
  touch-action: pan-x;
}
.kb-lightbox__track::-webkit-scrollbar { display: none; }
/* Пока кадр увеличен, лента стоит: иначе сдвиг картинки пальцем уводил бы
   к соседней */
/* Ленту фокусируют при открытии, чтобы кольцо не встало на крестик (kb.js).
   Своего кольца ей не нужно: нажимать в ней нечего, а при свайпе вниз его
   верхняя грань выезжала из-за края экрана синей полосой поперёк картинки */
.kb-lightbox__track:focus,
.kb-lightbox__track:focus-visible { outline: 0; }
.kb-lightbox__track[data-zoom] {
  overflow-x: hidden;
  touch-action: none;
}
/* Увеличенный кадр таскают мышью — курсор об этом и говорит */
@media (hover: hover) {
  .kb-lightbox__track[data-zoom] { cursor: grab; }
  .kb-lightbox__track[data-zoom]:active { cursor: grabbing; }
}
/* Возврат ленты на место, когда свайп вниз не дотянул до порога (kb.js).
   Транзишн живёт в классе, а не в стиле элемента: во время самого жеста
   лента должна идти за пальцем без задержки */
.kb-lightbox__track.is-returning {
  transition: transform var(--duration-base) var(--ease-brand),
              opacity var(--duration-base) var(--ease-brand);
}
.kb-lightbox__slide {
  flex: 0 0 100%;
  scroll-snap-align: center;
  display: grid;
  place-items: center;
  /* Поля по краям — место под крестик и стрелки */
  padding: var(--size-header) calc(var(--size-button-l) + var(--spacing-m) * 2);
}
/* Картинка тянется до краёв кадра, а не показывается в своём размере:
   мелкий скриншот в колонке статьи растянут на все 800, и в просмотре он
   выглядел меньше, чем до нажатия. object-fit держит пропорции */
.kb-lightbox__img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
}
/* Крестик и стрелки — те же круглые кнопки тёмного стекла, что «наверх» и
   стрелки карусели: вид описан одним правилом на все четыре, здесь только
   место на экране (см. «Круглая кнопка поверх содержимого») */
.kb-lightbox__close,
.kb-lightbox__nav {
  position: absolute;
  /* Касание не должно оставлять синий след поверх тёмной подложки */
  -webkit-tap-highlight-color: transparent;
}
/* Кольцо — только под клавиатуру: пальцем по крестику попадают и так */
.kb-lightbox__close:focus,
.kb-lightbox__nav:focus { outline: 0; }
.kb-lightbox__close:focus-visible,
.kb-lightbox__nav:focus-visible {
  outline: var(--focus-ring-width) solid var(--color-brand);
  outline-offset: var(--spacing-xxs);
}
/* Крестик без подложки: он стоит на затемнённом фоне просмотра, и круг под
   ним только спорил с картинкой за внимание */
.kb-lightbox__close {
  top: var(--spacing-xs);
  right: var(--spacing-xs);
  display: grid;
  place-items: center;
  width: var(--size-button-l);
  height: var(--size-button-l);
  border: 0;
  background: none;
  color: var(--color-text-on-primary);
  cursor: pointer;
}
.kb-lightbox__close .icon {
  width: var(--size-button-icon-m);
  height: var(--size-button-icon-m);
}
.kb-lightbox__nav {
  top: 50%;
  transform: translateY(-50%);
}
.kb-lightbox__nav--prev { left: var(--spacing-m); }
.kb-lightbox__nav--next { right: var(--spacing-m); }
/* Иконка нарисована вправо: левой стрелке её отражаем */
.kb-lightbox__nav--prev .icon { transform: scaleX(-1); }
/* Телефон (1171:9008): подложка сплошная чёрная, картинка растянута по
   большей стороне до края экрана, стрелок нет — соседний кадр листают
   свайпом. Сверху остаётся только крестик */
@media (max-width: 767px) {
  .kb-lightbox::backdrop { background: var(--kb-lightbox-bg); }
  /* Стрелок нет, крестик лежит поверх — кадр занимает весь экран */
  .kb-lightbox__slide { padding: 0; }
}

/* ── Подсветка синтаксиса ──────────────────────────────────────────────────
   В макете подсветки нет, в ките палитры для неё тоже нет — проверены все
   шесть коллекций переменных. Палитра собрана из утилитарных шкал
   дизайн-системы.

   Пять ролей, а не четыре: в справке по API основной контент — JSON, и
   ключ со строковым значением обязаны различаться, иначе объект читается
   как каша. Ровно на этом спотыкается Solarized, где строки и числа
   одного цвета.

   Взяты ступени 140, а не привычные 100. Текст 15px обязан давать 4.5:1
   к фону; на сером фоне блока кода ступени 100 дают 2.7–2.8 — код был бы
   блёклым ровно там, ради чего подсветку и делают. Ступени 140 проходят:
   синий 6.52, зелёный 4.72, красный 4.90.

   Скобки и запятые не крашены — они цвета обычного текста. Так же сделано
   в теме Prism по умолчанию и во всех аккуратных светлых темах: цветной
   пунктуацией код только рябит.

   Цвет не единственный признак: комментарий набран курсивом, ключ —
   средним весом. У строки и числа контраст между собой всего 1.04, то есть
   при дальтонизме они сольются, и один цвет их бы не развёл.

   Классы совместимы с Prism. Разработчик подключает любой подсветчик —
   палитра встаёт сама. При переходе на Shiki имена ролей те же */
.kb-code .token.keyword,
.kb-code .token.boolean,
.kb-code .token.null,
.kb-code .token.constant,
.kb-code .token.builtin { color: var(--color-info-140); }

.kb-code .token.string,
.kb-code .token.char,
.kb-code .token.attr-value,
.kb-code .token.url { color: var(--color-success-140); }

.kb-code .token.number,
.kb-code .token.symbol { color: var(--color-error-140); }

.kb-code .token.property,
.kb-code .token.attr-name,
.kb-code .token.tag { font-weight: var(--font-weight-body-medium); }

.kb-code .token.comment,
.kb-code .token.prolog,
.kb-code .token.doctype,
.kb-code .token.cdata {
  color: var(--color-text-secondary-hover);
  font-style: italic;
}

.kb-code .token.punctuation,
.kb-code .token.operator { color: inherit; }

/* ── Круглая кнопка поверх содержимого ─────────────────────────────────────
   Одна кнопка на две роли: стрелки карусели и «наверх». Рисунок из кита 2.0
   (492:29240, «Message Up Arrow») — круг 46 с заливкой Button/Secondary,
   размытием фона 8 и тёмной иконкой 22.

   Одно правило на обе не для экономии строк: кнопки стоят на одной
   странице в паре экранов друг от друга, и любая расходимость сразу видна
   как две разные кнопки */
.kb-top,
.kb-gallery__arrow,
.kb-lightbox__nav {
  display: grid;
  place-items: center;
  width: var(--size-button-l);
  height: var(--size-button-l);
  border: 0;
  border-radius: var(--radius-full);
  /* Два слоя, как в ките: тёмная подложка Basic/Overlay/M и поверх неё
     заливка Button/Secondary. Одним цветом это не собрать — кнопка стоит
     то на картинке, то на тексте, и держит контраст именно тёмный слой,
     а серый сверху даёт кнопке объём */
  background-color: var(--color-overlay);
  background-image: linear-gradient(var(--color-button-secondary),
                                    var(--color-button-secondary));
  backdrop-filter: blur(var(--blur-s));
  -webkit-backdrop-filter: blur(var(--blur-s));
  color: var(--color-text-on-primary);
  cursor: pointer;
  transition: background-color var(--duration-fast) var(--ease-brand);
}
@media (hover: hover) {
  .kb-top:hover,
  .kb-gallery__arrow:hover,
  .kb-lightbox__nav:hover {
    background-image: linear-gradient(var(--color-button-secondary-hover),
                                      var(--color-button-secondary-hover));
  }
}
.kb-top:active,
.kb-gallery__arrow:active,
.kb-lightbox__nav:active {
  background-image: linear-gradient(var(--color-button-secondary-pressed),
                                    var(--color-button-secondary-pressed));
}
.kb-top .icon,
.kb-gallery__arrow .icon,
.kb-lightbox__nav .icon {
  width: var(--size-button-icon-m);
  height: var(--size-button-icon-m);
}

/* Стрелки просмотра прячутся здесь, а не при своих правилах выше: вид кнопки
   описан общим правилом, и скрытие обязано стоять после него — иначе
   display: grid перебивает его порядком в файле.
   Одиночная картинка — листать нечего; телефон — там листают свайпом */
.kb-lightbox__nav[hidden] { display: none; }
@media (max-width: 767px) {
  .kb-lightbox__nav { display: none; }
}

/* «Наверх» появляется там, где оглавление уже не помещается: колонки со
   списком разделов нет, и вернуться к началу статьи нечем. Прокрутил
   больше экрана — кнопка появилась.

   Кнопку ставит скрипт: без него она не нажимается, и в разметке от неё
   был бы один мусор */
.kb-top {
  position: fixed;
  z-index: 40;
  right: var(--spacing-m);
  bottom: var(--spacing-m);
  display: none;
  /* Появляется и уходит плавно, всплывая снизу. Прячет её не display, а
     прозрачность: display не анимируется, и кнопка вспыхивала на месте.
     visibility в переходе — чтобы скрытая кнопка не ловила клик и не
     попадала под Tab, но гасла вместе с остальным, а не мгновенно */
  opacity: 0;
  visibility: hidden;
  transform: translateY(var(--spacing-s));
  transition:
    opacity var(--duration-base) var(--ease-brand),
    transform var(--duration-base) var(--ease-brand),
    visibility var(--duration-base) var(--ease-brand),
    background-color var(--duration-fast) var(--ease-brand);
}
.kb-top[data-visible] {
  opacity: 1;
  visibility: visible;
  transform: none;
}
@media (prefers-reduced-motion: reduce) {
  .kb-top { transform: none; transition: none; }
}
/* Иконка нарисована вправо — здесь смотрит вверх */
.kb-top .icon { transform: rotate(-90deg); }

/* ══ АДАПТИВ ══════════════════════════════════════════════════════════════
   Мобильного макета нет, поэтому раскладка ниже — реконструкция по логике
   лендинга. Ступени взяты у него же, новых не выдумано. Кегли заголовков
   не подобраны на глаз: у коллекции Typography в ките есть режим mobile,
   значения оттуда.

   ── 1279: оглавление уходит из сетки ─────────────────────────────────── */
@media (max-width: 1279px) {
  .kb-main {
    grid-template-columns: minmax(0, var(--kb-column));
    grid-template-areas:
      "head"
      "body";
  }
  /* Картинка не занимает столько же, сколько на компе: 600 на планшете —
     это больше половины экрана, а в карусели по этой высоте равняются все
     кадры, и широкий кадр получает поля в треть своего роста */
  :root,
  .kb-layout { --kb-image-max-height: 480px; }

  /* Оглавление уходит совсем: держать его блоком над статьёй значит
     отдать первый экран списку ссылок. Возврат к началу берёт на себя
     кнопка «наверх» — она и появляется ровно здесь */
  .article-toc:not([data-static]) { display: none; }
  .kb-top { display: grid; }

  /* Маркеры списка возвращаются внутрь колонки: слева от неё поля больше
     нет, и вынесенные наружу номера ложились на сайдбар. Раз выносить
     нечего — и поле под вынос колонке статьи не нужно */
  .payload-richtext > :where(ol, ul) {
    margin-left: 0;
    padding-left: var(--kb-marker-col);
  }
  .kb-main { padding-inline: var(--page-padding-desktop); }

}

/* Кнопка разделов (1212:17388). Нужна только там, где колонка с разделами
   не помещается. Скрытие объявлено до медиазапроса намеренно: и здесь, и
   там один класс, поэтому спор решает порядок в файле, а не специфичность.
   Поставить это правило ниже — и кнопка не покажется никогда.

   Стоит у нижнего края с тем же полем, что «наверх», но у другого края:
   разделы выезжают слева, возврат к началу живёт справа, и две
   закреплённые кнопки не спорят за одно место */
.kb-sidebar-toggle {
  display: none;
  position: fixed;
  z-index: 40;
  left: var(--spacing-m);
  bottom: var(--spacing-m);
  /* Поля по 16, а не 32 от .btn--l: в макете кнопка 105 в ширину, и
     считается она от иконки на 16-м пикселе. Широкие поля размера L
     набраны под кнопку без иконки */
  padding-inline: var(--spacing-m);
}

/* ── Drawer снизу ──────────────────────────────────────────────────────────
   Компонент Dropdown кита 2.0 (3203:38180) — панель у нижнего края с
   ручкой над ней. В таком виде на узком экране открываются разделы
   (1213:18090) и вопрос «как улучшить страницу» (1212:16815).

   Ручка и подложка появляются только там, где включён drawer: признаком
   служит --kb-drawer у самой панели, по нему же скрипт понимает, тянуть
   её пальцем или оставить обычным попапом. Скрытие объявлено до
   медиазапросов — как и у кнопки разделов, спор решает порядок в файле */
.kb-drawer-handle,
.kb-drawer-veil { display: none; }

/* Ручка висит над панелью, на затемнении: в макете она вне белого блока.
   Это кнопка, а не украшение — с клавиатуры drawer тоже надо чем-то
   закрывать, крестика в макете нет */
.kb-drawer-handle {
  position: absolute;
  /* Отсчёт идёт от поля панели, а обводка лежит выше него: без поправки
     ручка садилась бы на панель на толщину обводки */
  bottom: calc(100% + var(--size-stroke-m));
  left: 0;
  right: 0;
  align-items: flex-end;
  height: var(--kb-drawer-handle);
  padding: 0 0 var(--spacing-xs);
  border: 0;
  background: none;
  cursor: pointer;
  /* Жест по ручке ведёт панель, а не прокручивает страницу под ней */
  touch-action: none;
}
.kb-drawer-handle::before {
  content: '';
  display: block;
  width: var(--kb-drawer-line);
  height: var(--kb-drawer-line-h);
  margin-inline: auto;
  border-radius: var(--radius-s);
  background: var(--color-border);
}

/* Затемнение под панелью: сама панель непрозрачная, а страница за ней
   гасится Overlay/M */
.kb-drawer-veil {
  position: fixed;
  z-index: 45;
  /* Низ уходит за край окна: с открытой клавиатурой iOS рисует страницу и
     под своей полупрозрачной панелью, ниже границы окна, — там она
     просвечивала незатемнённой */
  inset: 0 0 -100vh;
  /* Жест по затемнению не должен прокручивать страницу под ним: на iOS
     overflow: hidden у body сам по себе прокрутку не держит */
  touch-action: none;
  background: var(--color-overlay);
}
.kb-drawer-veil[hidden] { display: none; }

/* Открытие: панель выезжает снизу. У разделов переход держит transform,
   а попап оценки до открытия ещё display: none, и переходу не за что
   зацепиться — ему нужна анимация */
@keyframes kb-drawer-in {
  from { transform: translateY(100%); }
}

/* ── 983: сайдбар прячется под кнопку ─────────────────────────────────── */
@media (max-width: 983px) {
  .kb-layout { grid-template-columns: minmax(0, 1fr); }

  /* Разделы выезжают снизу (1213:18090). Выше шапки панель не поднимается:
     над ней видно затемнённую страницу, и читатель понимает, что никуда
     со статьи не ушёл. Пока панель закрыта — не только не видна, но и
     недостижима с клавиатуры: иначе Tab уводил бы фокус в невидимый список */
  .kb-sidebar {
    --kb-drawer: 1;
    position: fixed;
    z-index: 50;
    /* Высота по содержимому: короткий список не растягивается на весь
       экран. Потолок — до шапки, дальше список прокручивается внутри */
    inset: auto 0 0;
    max-height: calc(100dvh - var(--size-header));
    /* Замер делает скрипт на открытии. Пока читатель набирает запрос,
       панель стоит на месте: прыгать вслед за числом находок она не должна */
    height: var(--kb-drawer-height, auto);
    display: flex;
    flex-direction: column;
    width: auto;
    max-width: none;
    padding: 0;
    /* Панель лежит поверх затемнённой страницы: там фон белый, а не цвет
       колонки. Поле поиска и затухание красятся от той же переменной */
    --kb-sidebar-bg: var(--color-bg-primary);
    background: var(--kb-sidebar-bg);
    border: var(--size-stroke-m) solid var(--color-border);
    border-bottom: 0;
    border-radius: var(--radius-xl) var(--radius-xl) 0 0;
    /* Закрытая панель уходит за нижний край вместе с ручкой — она висит
       над панелью и на свою высоту за край не уезжает */
    transform: translateY(calc(100% + var(--kb-drawer-handle)));
    transition: transform var(--duration-base) var(--ease-brand);
  }
  /* Открытая стоит на месте, пока её не тянут: сдвиг за пальцем скрипт
     кладёт в --kb-drawer-drag */
  .kb-sidebar[data-open] { transform: translateY(var(--kb-drawer-drag, 0px)); }
  .kb-sidebar.is-dragging { transition: none; }
  /* Пока панель открыта, страница под ней стоит: иначе жест по списку
     уводил вниз статью, а не разделы. Прокрутка остаётся у списка внутри,
     и до страницы не доходит — этим занят overscroll-behavior */
  html[data-drawer-open],
  html[data-drawer-open] body { overflow: hidden; }
  .kb-sidebar:not([data-open]) { visibility: hidden; }
  /* Список прокручивается внутри панели, а не тянет её вниз: у панели
     фиксированная высота в окно, и залипание ей ни к чему */
  .kb-sidebar .kb-sidebar__inner {
    --kb-list-pad: var(--spacing-m);
    position: static;
    max-height: none;
    flex: 1;
    min-height: 0;
    padding: var(--kb-list-pad);
    /* Нижняя строка списка не уходит под домашний индикатор iPhone */
    padding-bottom: calc(var(--spacing-m) + env(safe-area-inset-bottom, 0px));
    margin: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
  }

  .kb-sidebar .kb-drawer-handle { display: flex; }
  .kb-sidebar + .kb-drawer-veil:not([hidden]) { display: block; }

  .kb-sidebar-toggle { display: inline-flex; }
}

/* ── 767: мобильная раскладка ─────────────────────────────────────────── */
@media (max-width: 767px) {
  .kb-main { padding-inline: var(--page-padding-mobile); }

  /* На телефоне потолок ещё ниже: 360 — это меньше половины экрана, и под
     картинкой видно, что инструкция продолжается */
  :root,
  .kb-layout { --kb-image-max-height: 360px; }

  /* Кегли из режима mobile коллекции Typography (Kit 2.0).
     Основной текст не уменьшается: в справке 17px — это минимум, ниже
     инструкцию читать на телефоне уже тяжело */
  .article-title {
    font-size: var(--text-h1-mobile);
    line-height: var(--text-h1-mobile--line-height);
    padding-block: var(--spacing-m) var(--spacing-s);
  }
  .payload-richtext h2 {
    font-size: var(--text-h2-mobile);
    line-height: var(--text-h2-mobile--line-height);
  }

  /* Полосы блоков ужимаются: на узком экране поле 24 съедает восьмую
     часть ширины. Видео в списке нет: у него кадр и вкладки идут вплотную
     к рамке на любой ширине (1171:8839) */
  .kb-card,
  .payload-richtext > blockquote,
  .kb-quote,
  .kb-code,
  .kb-linkbutton,
  .kb-faq > summary,
  .kb-figure figcaption,
  .payload-richtext figcaption { padding: var(--spacing-m); }
  /* У ответа поле сверху не берётся: его уже дал вопрос своим полем снизу,
     и вдвоём они давали 32 вместо 16 */
  .kb-faq__body { padding: 0 var(--spacing-m) var(--spacing-m); }

  /* Плитки на телефоне встают по две: по одной на строку — это шесть
     экранов прокрутки до первого текста. Ровно две не задаём — считает
     сетка по --kb-tile-min: на 360 и шире помещаются две, на 320 честно
     остаётся одна. Зазор здесь 12, а не 24: на 360 разница между «две
     плитки» и «одна» — те самые 12 пикселей */
  .kb-tiles {
    grid-template-columns: repeat(auto-fill, minmax(var(--kb-tile-min), 1fr));
    gap: var(--spacing-s);
  }
  /* Поле у плитки на телефоне 12, а не 16, как у остальных блоков: на 360
     разница между «две плитки в ряд» и «одна» — ровно эти четыре пикселя
     с каждой стороны */
  .kb-tile { padding: var(--spacing-s); }

  .kb-table th { padding: var(--spacing-m) var(--spacing-m) var(--spacing-xs); }
  .kb-table td { padding: var(--spacing-xs) var(--spacing-m); }

  /* Стрелки карусели прячутся — на телефоне листают пальцем, а стрелки
     закрывают собой треть кадра. Вместо них точки */
  .kb-gallery__arrow { display: none; }
  .kb-gallery__dots { display: flex; }

  /* Поля по бокам подвал не берёт: колонка и так узкая */
  .kb-article-footer { padding-inline: 0; }

  /* Вопрос «как улучшить» спрашиваем drawer'ом снизу (1212:16815).
     Попапу у пальца на телефоне негде развернуться: он шире, чем остаток
     строки, и всё равно упирается в край экрана */
  .kb-rate__popup {
    --kb-drawer: 1;
    position: fixed;
    z-index: 50;
    inset: auto 0 0;
    width: auto;
    border-color: var(--color-border);
    border-bottom: 0;
    border-radius: var(--radius-xl) var(--radius-xl) 0 0;
    /* Тень попапа тут ни к чему: панель отделена от страницы затемнением */
    box-shadow: none;
    padding-bottom: calc(var(--spacing-m) + env(safe-area-inset-bottom, 0px));
    transform: translateY(var(--kb-drawer-drag, 0px));
    transition: transform var(--duration-base) var(--ease-brand);
    animation: kb-drawer-in var(--duration-base) var(--ease-brand);
  }
  .kb-rate__popup.is-dragging { transition: none; }
  /* Белое поле панели продолжается за нижний край окна: с открытой
     клавиатурой iOS показывает под своей панелью полосу страницы ниже
     границы окна, и между «Отправить» и клавиатурой просвечивал подвал */
  .kb-rate__popup::after {
    content: '';
    position: absolute;
    top: 100%;
    left: calc(-1 * var(--size-stroke-m));
    right: calc(-1 * var(--size-stroke-m));
    height: 100vh;
    background: var(--color-surface);
  }
  /* Разворот вверх — забота попапа у кнопки. Drawer и так у нижнего края */
  .kb-rate__popup--up { top: auto; bottom: 0; }
  /* Кнопка во всю ширину панели, как в макете */
  .kb-rate__popup .btn { width: 100%; }

  .kb-rate__popup .kb-drawer-handle { display: flex; }
  .kb-rate__popup + .kb-drawer-veil:not([hidden]) { display: block; }
}

