/* kb.css — слой страницы базы знаний
 *
 * Разбор системы — `docs/design-system.md`. Здесь только то, чего нет на
 * главной: голова статьи, оглавление, длинная типографика, таблицы, код,
 * врезки, блок платформы, соседи и пред/след.
 *
 * Порядок подключения обязателен:
 *   fonts.css → tokens.css → base.css → kb.css
 */

/* ------------------------------------------------------------ раскладка */

.kb-head { padding-block: clamp(40px, 6vw, 88px) clamp(24px, 3vw, 40px); }

.kb-crumbs {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2);
  font: 400 var(--fs-label)/1.6 var(--font-sans);
  letter-spacing: 0.04em; text-transform: uppercase;
  color: var(--dim);
}
.kb-crumbs a { color: var(--dim); }
.kb-crumbs a:hover { color: var(--text); }
.kb-crumbs li + li::before { content: "/"; margin-right: var(--space-2); color: var(--line); }
.kb-crumbs li { display: inline-flex; align-items: center; }

.kb-head h1 {
  margin-top: var(--space-6);
  font-size: clamp(30px, 4.4vw, 46px);
  line-height: 1.14;
  letter-spacing: -0.03em;
  font-weight: 600;
  text-wrap: balance;
  max-width: 21ch;
}
.kb-head .lead { margin-top: var(--space-6); max-width: 62ch; }

.kb-meta {
  margin-top: var(--space-8);
  display: flex; flex-wrap: wrap; gap: var(--space-6);
  padding-top: var(--space-4);
  border-top: var(--hairline) solid var(--line);
  font: 400 var(--fs-label)/1.6 var(--font-sans);
  letter-spacing: 0.04em; text-transform: uppercase;
  color: var(--dim);
}
.kb-meta b { color: var(--muted); font-weight: 400; }

/* Колонка оглавления слева, текст справа. Оглавление уходит первым: на
   узком экране список разделов перед статьёй полезнее, чем сбоку. */
.kb-layout {
  display: grid;
  grid-template-columns: 216px minmax(0, 1fr);
  gap: clamp(32px, 5vw, 72px);
  padding-bottom: clamp(56px, 8vw, 112px);
}

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

.kb-toc { position: sticky; top: 84px; align-self: start; }
.kb-toc-label {
  font: 400 var(--fs-label)/1 var(--font-sans);
  letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--dim);
  padding-bottom: var(--space-3);
  border-bottom: var(--hairline) solid var(--line);
}
.kb-toc ol { margin-top: var(--space-3); counter-reset: toc; }
.kb-toc li { counter-increment: toc; }
.kb-toc a {
  display: block;
  padding: var(--space-2) 0 var(--space-2) var(--space-4);
  border-left: 2px solid transparent;
  font-size: var(--fs-sm);
  line-height: 1.45;
  color: var(--dim);
  transition: color var(--t-ui), border-color var(--t-ui);
}
.kb-toc a::before {
  content: counter(toc, decimal-leading-zero);
  font: 400 10px var(--font-sans); color: var(--line);
  margin-right: var(--space-2);
}
.kb-toc a:hover { color: var(--text); border-left-color: var(--line); }

/* Пройденное и текущее — без скрипта. Раздел объявляет именованную
   прокруточную шкалу, ссылка на неё подписывается; `timeline-scope` нужен
   потому, что ссылка не потомок раздела.

   Два подводных камня, оба стоили по прогону:
   1. Шорткат `animation:` сбрасывает `animation-range` в `normal`, и общее
      правило с меньшей специфичностью его уже не вернёт. Только длинные
      свойства.
   2. «Подсветить ровно текущий раздел» на шкалах не выражается: в кадре
      почти всегда два-три раздела разом, и три горящих пункта читались как
      сбой. Поэтому смысл другой — след прочитанного: раздел в кадре горит
      акцентом, пройденный остаётся приглушённо отмеченным, непрочитанный
      тусклый. Пересечение соседних при таком чтении не ошибка.

   Где браузер шкал не знает, оглавление просто без подсветки. */
@keyframes toc-read {
  0%,  99% { color: var(--text); border-left-color: var(--accent); }
  100%     { color: var(--muted); border-left-color: var(--line); }
}

@supports (animation-timeline: view()) {
  .kb-layout { timeline-scope: --s1, --s2, --s3, --s4, --s5, --s6, --s7, --s8, --s9, --s10; }
  .kb-body > section:nth-of-type(1)  { view-timeline-name: --s1; }
  .kb-body > section:nth-of-type(2)  { view-timeline-name: --s2; }
  .kb-body > section:nth-of-type(3)  { view-timeline-name: --s3; }
  .kb-body > section:nth-of-type(4)  { view-timeline-name: --s4; }
  .kb-body > section:nth-of-type(5)  { view-timeline-name: --s5; }
  .kb-body > section:nth-of-type(6)  { view-timeline-name: --s6; }
  .kb-body > section:nth-of-type(7)  { view-timeline-name: --s7; }
  .kb-body > section:nth-of-type(8)  { view-timeline-name: --s8; }
  .kb-body > section:nth-of-type(9)  { view-timeline-name: --s9; }
  .kb-body > section:nth-of-type(10) { view-timeline-name: --s10; }

  .kb-toc a {
    animation-timing-function: linear;
    animation-fill-mode: forwards;
    animation-range: entry 88% exit 8%;
  }
  .kb-toc li:nth-child(1)  a { animation-name: toc-read; animation-timeline: --s1; }
  .kb-toc li:nth-child(2)  a { animation-name: toc-read; animation-timeline: --s2; }
  .kb-toc li:nth-child(3)  a { animation-name: toc-read; animation-timeline: --s3; }
  .kb-toc li:nth-child(4)  a { animation-name: toc-read; animation-timeline: --s4; }
  .kb-toc li:nth-child(5)  a { animation-name: toc-read; animation-timeline: --s5; }
  .kb-toc li:nth-child(6)  a { animation-name: toc-read; animation-timeline: --s6; }
  .kb-toc li:nth-child(7)  a { animation-name: toc-read; animation-timeline: --s7; }
  .kb-toc li:nth-child(8)  a { animation-name: toc-read; animation-timeline: --s8; }
  .kb-toc li:nth-child(9)  a { animation-name: toc-read; animation-timeline: --s9; }
  .kb-toc li:nth-child(10) a { animation-name: toc-read; animation-timeline: --s10; }
}

/* ----------------------------------------------------- длинная типографика */

/* якорь ведёт на заголовок, а не на секцию: `verify` сверяет оглавление
   именно с `<h2 id>` */
.kb-body h2 { scroll-margin-top: 96px; }
.kb-body > section + section { margin-top: clamp(48px, 6vw, 80px); }

.kb-body h2 {
  font-size: clamp(23px, 2.6vw, 28px);
  line-height: 1.25;
  letter-spacing: -0.02em;
  font-weight: 400;
  max-width: 26ch;
  text-wrap: balance;
}
/* Только подзаголовок прозы. Без ограничения правило доставало <h3> внутри
   <summary> в FAQ и отбивало его на 40px — строка вопроса разъезжалась,
   а маркер уезжал на строку выше. */
.kb-prose h3 {
  margin-top: var(--space-10);
  font-family: var(--font-sans);
  font-size: var(--fs-md, 17px);
  line-height: 1.4;
  letter-spacing: var(--ls-card);
  font-weight: 400;
}

/* Отбивка от заголовка секции — общим правилом, а не у каждого блока.
   На пилоте её потерял ровно тот блок, у которого своей не было (.faq):
   у прозы, схем, таблиц, кода и врезок отступ свой, у FAQ его не оказалось,
   и коробка вопросов прилипла к «Questions». */
.kb-body > section > h2 + * { margin-top: var(--space-6); }

.kb-prose { margin-top: var(--space-6); color: var(--muted); max-width: 68ch; }
.kb-prose > * + * { margin-top: var(--space-6); }
.kb-prose p { line-height: var(--lh-body); }
.kb-prose strong { color: var(--text); font-weight: 500; }
.kb-prose a { color: var(--text); border-bottom: 1px solid var(--line); }
.kb-prose a:hover { color: var(--accent-hover); border-bottom-color: var(--accent-hover); }
.kb-prose code {
  font: 400 0.92em var(--font-sans);
  color: var(--text);
  background: var(--panel);
  border: var(--hairline) solid var(--line);
  border-radius: 3px;
  padding: 1px 5px;
}
/* Абзац, открывающийся жирной врезкой: в исходниках это несущая форма —
   утверждение, потом его развёртка. */
.kb-prose .kb-point { position: relative; padding-left: var(--space-6); }
.kb-prose .kb-point::before {
  content: ""; position: absolute; left: 0; top: 0.62em;
  width: 10px; height: 1px; background: var(--accent);
}

.kb-prose ul, .kb-prose ol { display: grid; gap: var(--space-3); }
.kb-prose li { position: relative; padding-left: var(--space-6); line-height: var(--lh-body); }
.kb-prose ul li::before {
  content: ""; position: absolute; left: 2px; top: 0.68em;
  width: 6px; height: 1px; background: var(--dim);
}
.kb-prose ol { counter-reset: n; }
.kb-prose ol li { counter-increment: n; }
.kb-prose ol li::before {
  content: counter(n, decimal-leading-zero);
  position: absolute; left: 0; top: 0.1em;
  font: 400 11px var(--font-sans); color: var(--dim);
}

/* ------------------------------------------------------------- врезка */

.kb-callout {
  margin-top: var(--space-8);
  padding: var(--space-6);
  background: var(--bg-raise);
  border: var(--hairline) solid var(--line);
  box-shadow: inset 3px 0 0 var(--mode-monitor);
  border-radius: var(--radius);
  max-width: 68ch;
}
.kb-callout-label {
  font: 400 var(--fs-label)/1 var(--font-sans);
  letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--mode-monitor);
}
.kb-callout p { margin-top: var(--space-3); color: var(--muted); font-size: var(--fs-sm); line-height: 1.6; }

/* --------------------------------------------------------- быстрые факты */

.kb-facts {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--hairline);
  background: var(--line);
  border: var(--hairline) solid var(--line);
  margin-top: var(--space-8);
}
.kb-facts > div { background: var(--bg); padding: var(--space-6); }
/* Пятая карточка в сетке из двух колонок остаётся одна в ряду. Там, где
   пятая — ответ на вопрос с оговоркой («останавливает ли WAF»), ряд
   отдаётся ей целиком: оговорка длиннее ячейки. */
.kb-facts > div.kb-facts-wide { grid-column: 1 / -1; }
.kb-facts dt {
  font: 400 var(--fs-label)/1 var(--font-sans);
  letter-spacing: 0.06em; text-transform: uppercase; color: var(--dim);
}
.kb-facts dd { margin-top: var(--space-3); font-size: var(--fs-sm); line-height: 1.5; color: var(--text); }

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

.kb-table-wrap { margin-top: var(--space-8); overflow-x: auto; }
.kb-table { width: 100%; border-collapse: collapse; min-width: 520px; }
.kb-table th, .kb-table td {
  padding: var(--space-4);
  border: var(--hairline) solid var(--line);
  text-align: left;
  vertical-align: top;
  font-size: var(--fs-sm);
  line-height: 1.5;
}
.kb-table th {
  font: 400 var(--fs-label)/1.4 var(--font-sans);
  letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--dim);
  background: var(--panel);
}
.kb-table td { color: var(--muted); }
.kb-table td:first-child { font-family: var(--font-sans); color: var(--text); white-space: nowrap; }

/* ----------------------------------------------------------------- код */

.kb-code {
  margin-top: var(--space-8);
  border: var(--hairline) solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
}
.kb-code-label {
  padding: var(--space-3) var(--space-4);
  border-bottom: var(--hairline) solid var(--line);
  background: var(--panel);
  font: 400 var(--fs-label)/1 var(--font-sans);
  letter-spacing: 0.06em; text-transform: uppercase; color: var(--dim);
}
.kb-code pre { margin: 0; padding: var(--space-4); overflow-x: auto; background: var(--bg); }
.kb-code code { font: 400 13px/1.75 var(--font-mono); color: var(--muted); }

/* ------------------------------------------------- где стоит платформа */

/* Повторяется на 15 страницах: три режима одним блоком и переход к продукту.
   Формулировки — из исходника страницы, не из шаблона. */
.kb-platform {
  margin-top: clamp(48px, 6vw, 80px);
  border: var(--hairline) solid var(--line);
  border-radius: var(--radius);
  background: var(--bg-raise);
  overflow: hidden;
}
.kb-platform-head { padding: var(--space-6); border-bottom: var(--hairline) solid var(--line); }
.kb-platform-head .eyebrow { display: block; margin-bottom: var(--space-3); }
.kb-modes { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--hairline); background: var(--line); }
.kb-modes > div { background: var(--bg); padding: var(--space-6); }
.kb-modes h3 {
  margin: 0; display: flex; align-items: center; gap: var(--space-2);
  font: 400 var(--fs-card)/1.3 var(--font-sans); letter-spacing: var(--ls-card);
}
.kb-modes h3::before { content: ""; width: 8px; height: 8px; border-radius: 50%; flex: none; }
.kb-modes > div:nth-child(1) h3::before { background: var(--mode-off); }
.kb-modes > div:nth-child(2) h3::before { background: var(--mode-monitor); }
.kb-modes > div:nth-child(3) h3::before { background: var(--mode-block); }
.kb-modes p { margin-top: var(--space-3); font-size: var(--fs-sm); line-height: 1.5; color: var(--muted); }
.kb-platform-cta { padding: var(--space-6); display: flex; flex-wrap: wrap;
  align-items: center; gap: var(--space-3); }
.kb-platform-cta p { flex: 1 1 22ch; font-size: var(--fs-sm); line-height: 1.6; color: var(--muted); }

/* --------------------------------------------------------------- соседи */

.kb-related { margin-top: clamp(48px, 6vw, 80px); }
.kb-related-label {
  font: 400 var(--fs-label)/1 var(--font-sans);
  letter-spacing: 0.06em; text-transform: uppercase; color: var(--dim);
  margin-bottom: var(--space-4);
}
.kb-related-grid {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--hairline); background: var(--line);
  border: var(--hairline) solid var(--line);
}
.kb-related-grid a { background: var(--bg); padding: var(--space-6); display: block;
  position: relative; transition: background var(--t-ui); }
.kb-related-grid a:hover { background: var(--panel); color: inherit; }
.kb-related-grid h3 { font-family: var(--font-sans); font-size: 16px; line-height: 1.4;
  letter-spacing: var(--ls-card); font-weight: 400; }
.kb-related-grid h3::after { content: "\00a0\2192"; color: var(--dim); display: inline-block;
  transition: transform var(--t-ui), color var(--t-ui); }
.kb-related-grid a:hover h3::after { transform: translateX(4px); color: var(--accent-hover); }
.kb-related-grid p { margin-top: var(--space-3); font-size: var(--fs-sm);
  line-height: 1.5; color: var(--muted); }

/* ------------------------------------------------------------- пред/след */

.kb-nav {
  margin-top: clamp(48px, 6vw, 80px);
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--hairline); background: var(--line);
  border: var(--hairline) solid var(--line);
}
.kb-nav a { background: var(--bg); padding: var(--space-6); display: block;
  transition: background var(--t-ui); }
.kb-nav a:hover { background: var(--panel); color: inherit; }
.kb-nav span { display: block; font: 400 var(--fs-label)/1 var(--font-sans);
  letter-spacing: 0.06em; text-transform: uppercase; color: var(--dim); }
.kb-nav b { display: block; margin-top: var(--space-3); font-weight: 400;
  font-family: var(--font-sans); font-size: 16px; letter-spacing: var(--ls-card); }
.kb-nav a:last-child { text-align: right; }

/* ------------------------------------------------------------ узкий экран */

@media (max-width: 1000px) {
  .kb-layout { grid-template-columns: minmax(0, 1fr); }
  .kb-toc { position: static; }
  .kb-toc ol { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0; }
}
@media (max-width: 700px) {
  .kb-facts, .kb-related-grid, .kb-modes { grid-template-columns: minmax(0, 1fr); }
  .kb-toc ol { grid-template-columns: minmax(0, 1fr); }
  .kb-nav { grid-template-columns: minmax(0, 1fr); }
  .kb-nav a:last-child { text-align: left; }
}

@media print {
  .kb-toc, .kb-platform-cta, .kb-nav { display: none; }
  .kb-layout { grid-template-columns: minmax(0, 1fr); }
}

/* ============================================ СЛОВАРЬ СХЕМ СТАТЬИ

   Примитивы, из которых собираются схемы внутренних страниц. Геометрия
   задаётся переменными в разметке (`style="--travel:480px"`), поэтому один
   набор keyframes обслуживает схемы разной ширины и не плодит копии.

   Правила те же, что на главной (`docs/design-system.md` §7): рисунок
   читается статикой, `role="img"` описывает происходящее, `data-loop`
   обязателен — скрипт гасит анимацию за пределами кадра, `transform-box`
   иначе считает начало координат от вьюпорта.
   ------------------------------------------------------------------------- */

.faq summary h3 { margin: 0; }

.kb-viz {
  margin-top: var(--space-8);
  padding-top: var(--space-6);
  border-top: var(--hairline) solid var(--line);
}
.kb-viz svg { width: 100%; height: auto; overflow: visible; }
.kb-viz figcaption {
  margin-top: var(--space-4);
  font: 400 var(--fs-label)/1.6 var(--font-sans);
  color: var(--dim);
}
.kb-viz svg *, .kb-viz svg { transform-box: fill-box; }

@media (max-width: 720px) {
  /* Схема шире экрана и листается вбок. Замер на 390px: контейнер 338px,
     SVG отрисован в 560px (это `min-width` ниже), то есть видно 60%, а 40%
     уходит за край. Ширина `viewBox` на это НЕ влияет — 640 и 560
     отрисовываются одинаково, в 560px, и разница только в масштабе.
     Об уехавшей части ничто не сообщало, и обрыв читался как обрезка.
     Затухание у конечного края — приём, уже принятый в сети для полосы
     ссылок в шапке (`site.css`, B-46). Отступ равен ширине затухания:
     между 560 и 720px схема помещается целиком, и градиент ложится на
     пустоту, ничего не пряча. */
  .kb-viz {
    overflow-x: auto;
    padding-bottom: var(--space-3);
    padding-inline-end: 1.5rem;
    scrollbar-width: none;
    -webkit-mask-image: linear-gradient(to right, #000 0,
                        #000 calc(100% - 1.5rem), transparent 100%);
    mask-image: linear-gradient(to right, #000 0,
                #000 calc(100% - 1.5rem), transparent 100%);
  }
  .kb-viz::-webkit-scrollbar { display: none; }
  .kb-viz svg { min-width: 560px; }
}

/* запрос едет по дорожке; длину пути задаёт --travel */
@keyframes kb-ride {
  0%   { transform: translateX(0); opacity: 0; }
  6%   { opacity: 1; }
  94%  { opacity: 1; }
  100% { transform: translateX(var(--travel, 480px)); opacity: 0; }
}
/* запрос доезжает до рубежа и гаснет; --stop-at — где именно */
@keyframes kb-ride-stop {
  0%   { transform: translateX(0); opacity: 0; }
  12%  { opacity: 1; }
  46%  { transform: translateX(var(--stop-at, 240px)); opacity: 1; }
  56%  { transform: translateX(var(--stop-at, 240px)) scale(2.4); opacity: 0; }
  100% { transform: translateX(var(--stop-at, 240px)) scale(2.4); opacity: 0; }
}
/* копия уходит вниз в журнал */
@keyframes kb-drop {
  0%, 42% { opacity: 0; transform: translate(var(--at, 240px), 0); }
  50%     { opacity: 1; }
  84%     { opacity: 1; transform: translate(var(--at, 240px), var(--fall, 96px)); }
  100%    { opacity: 0; transform: translate(var(--at, 240px), var(--fall, 96px)); }
}
/* вспышка на рубеже в момент прохода */
@keyframes kb-pulse { 0%, 100% { opacity: 0.3; } 46%, 54% { opacity: 1; } }
/* запись с датой пересмотра гаснет; запись без даты остаётся навсегда */
@keyframes kb-expire { 0% { opacity: 0; } 16%, 66% { opacity: 1; } 82%, 100% { opacity: 0; } }
@keyframes kb-accrue { 0% { opacity: 0; } 16%, 100% { opacity: 1; } }
/* заполнение счётчика; конечную долю задаёт --to в разметке */
/* Заполнение занимает первую треть цикла, дальше счётчик СТОИТ на конечном
   значении. Иначе случайный кадр — а именно им страница и живёт на снимке,
   на паузе и в чужом восприятии — показывает половину, и схема утверждает
   не то, что написано рядом. */
@keyframes kb-fill { 0%, 8% { transform: scaleX(0); } 38%, 100% { transform: scaleX(var(--to, 1)); } }
/* последовательная подсветка шагов или столбцов */
@keyframes kb-lit { 0%, 100% { opacity: 0.55; } 14%, 30% { opacity: 1; } }

.kb-ride  { animation: kb-ride var(--cycle, 4s) linear infinite; }
.kb-ride-stop { animation: kb-ride-stop var(--cycle, 4s) linear infinite; }
.kb-copy  { animation: kb-drop var(--cycle, 4s) linear infinite; }
.kb-pulse { animation: kb-pulse var(--cycle, 4s) var(--ease) infinite; }
.kb-lit   { animation: kb-lit var(--cycle, 4s) linear infinite; }
.kb-fill  { transform-origin: left center; animation: kb-fill var(--cycle, 4s) var(--ease) infinite; }
.kb-expire { animation: kb-expire var(--cycle, 4s) linear infinite; }
.kb-accrue { animation: kb-accrue var(--cycle, 4s) linear infinite; }

.kb-d1 { animation-delay: calc(var(--cycle, 4s) * -0.2); }
.kb-d2 { animation-delay: calc(var(--cycle, 4s) * -0.4); }
.kb-d3 { animation-delay: calc(var(--cycle, 4s) * -0.6); }
.kb-d4 { animation-delay: calc(var(--cycle, 4s) * -0.8); }

/* Заливка задаётся КЛАССОМ, не атрибутом `fill=`: правило `.viz-label { fill }`
   из base.css перебивает презентационный атрибут, и цветные метки молча
   становятся серыми. */
.viz-ok   { fill: var(--accent-secondary); }
.viz-warn { fill: var(--mode-monitor); }
.viz-stop { fill: var(--mode-block); }
.viz-cell        { fill: var(--bg); stroke: var(--viz-line); stroke-width: 1; }
.viz-dot         { fill: var(--viz-line); }
.viz-dot-flag    { fill: var(--mode-monitor); }
.viz-dot-ok      { fill: var(--accent-secondary); }
.viz-dot-warn    { fill: var(--mode-monitor); }
.viz-dot-stop    { fill: var(--mode-block); }
/* Кольцо вокруг помеченной точки: сигнал сработал, а запрос всё равно свой.
   Зелёное — цвет «прошло», не цвет режима: режимные цвета заняты. */
.viz-ring        { fill: none; stroke: var(--accent-secondary); stroke-width: 1.5; }
.viz-meter       { fill: var(--panel-2); stroke: var(--viz-line); stroke-width: 1; }
.viz-meter-fill  { fill: var(--accent); opacity: 0.7; }
.viz-meter-over  { fill: var(--mode-block); opacity: 0.8; }
.viz-plot        { fill: none; stroke: var(--muted); stroke-width: 1.5; }
.viz-plot-alt    { fill: none; stroke: var(--dim); stroke-width: 1.5; stroke-dasharray: 6 4; }
.viz-band        { fill: var(--accent-soft); stroke: none; }
.viz-limit       { stroke: var(--mode-monitor); stroke-width: 1; stroke-dasharray: 5 4; fill: none; }
/* Пунктир цвета отказа. Отдельным классом, а не атрибутом stroke=: правило
   класса перебивает презентационный атрибут, и линия молча остаётся чужого
   цвета. Та же ловушка, что с fill у подписей. */
.viz-limit-stop  { stroke: var(--mode-block); stroke-width: 1.5; stroke-dasharray: 5 4; fill: none; }
.viz-stroke-stop { stroke: var(--mode-block); stroke-width: 1.5; fill: none; }
.viz-stroke-ok   { stroke: var(--accent); stroke-width: 1; fill: none; }
.viz-cell-marked { fill: var(--warn-soft); stroke: var(--mode-monitor); stroke-width: 1; }
/* «Наблюдает / входит в область» — НЕ янтарным. Янтарный, серый и красный
   заняты тремя режимами продукта, и красить ими что-то другое значит
   размывать единственную цветовую семантику, которая на сайте что-то
   значит. Для утверждений вида «это видно» — акцент. */
.viz-cell-on     { fill: var(--accent-soft); stroke: var(--accent); stroke-width: 1; }
.viz-cell-stop   { fill: var(--stop-soft); stroke: var(--mode-block); stroke-width: 1; }
/* Жирная метка на схеме. Аналог .path-tick с главной, но живёт здесь:
   `home.css` внутренние страницы не подключают, и класс оттуда молча не
   применяется — линия остаётся без цвета и без толщины. */
.viz-tick-ok     { stroke: var(--accent); stroke-width: 3; fill: none; }
.viz-tick-warn   { stroke: var(--mode-monitor); stroke-width: 3; fill: none; }
.viz-tick-stop   { stroke: var(--mode-block); stroke-width: 3; fill: none; }

.kb-pkt-plain { fill: var(--dim); }
.kb-pkt-flag  { fill: var(--mode-monitor); }
.kb-pkt-stop  { fill: var(--mode-block); }
.kb-gate      { fill: none; stroke-dasharray: 4 5; stroke-width: 1; stroke: var(--viz-line); }
.kb-gate-monitor { stroke: var(--mode-monitor); }
.kb-gate-block   { stroke: var(--mode-block); }

@media (prefers-reduced-motion: reduce) {
  .kb-ride, .kb-ride-stop, .kb-copy, .kb-pulse, .kb-lit, .kb-fill,
  .kb-expire, .kb-accrue { animation: none !important; }
  .kb-expire, .kb-accrue { opacity: 1; }
  .kb-fill { transform: scaleX(var(--to, 1)); }
  .kb-copy, .kb-lit { opacity: 1; }
  .kb-ride-stop { opacity: 0.5; }
}

/* ===================== ИЛЛЮСТРАЦИИ ИЗ СЕТЕВОЙ БИБЛИОТЕКИ (media-assets) §16
   Кадр строго без наложений: поверх арта не размещается ничего — ни текст,
   ни градиенты (условие владельца, 2026-09-02). Тема — через data-theme:
   CSS-фон грузит только вариант активной темы; aspect-ratio держит место
   (нет CLS). Исходники: bridge-cdn-index/media-assets/features/ (SHA — в
   README библиотеки); производные images/art/ по пайплайну design-system §8
   (Pillow, avif q52 / webp q72 / jpg q78, ширина 1100, без апскейла). */
.kb-art { margin-top: var(--space-8); }
.kb-art-frame {
  aspect-ratio: 1100 / 601;
  border: var(--hairline) solid var(--line);
  border-radius: var(--radius);
  background: center / cover no-repeat;
}

.kb-art-ddos .kb-art-frame { background-image: url("/images/art/ddos-protection-dark-1100.jpg");
  background-image: image-set(url("/images/art/ddos-protection-dark-1100.avif") type("image/avif"), url("/images/art/ddos-protection-dark-1100.webp") type("image/webp")); }
:root[data-theme="proof-light"] .kb-art-ddos .kb-art-frame { background-image: url("/images/art/ddos-protection-light-1100.jpg");
  background-image: image-set(url("/images/art/ddos-protection-light-1100.avif") type("image/avif"), url("/images/art/ddos-protection-light-1100.webp") type("image/webp")); }

.kb-art-shield .kb-art-frame { background-image: url("/images/art/origin-shield-dark-1100.jpg");
  background-image: image-set(url("/images/art/origin-shield-dark-1100.avif") type("image/avif"), url("/images/art/origin-shield-dark-1100.webp") type("image/webp")); }
:root[data-theme="proof-light"] .kb-art-shield .kb-art-frame { background-image: url("/images/art/origin-shield-light-1100.jpg");
  background-image: image-set(url("/images/art/origin-shield-light-1100.avif") type("image/avif"), url("/images/art/origin-shield-light-1100.webp") type("image/webp")); }

.kb-art-detect .kb-art-frame { background-image: url("/images/art/bot-analytics-dark-1100.jpg");
  background-image: image-set(url("/images/art/bot-analytics-dark-1100.avif") type("image/avif"), url("/images/art/bot-analytics-dark-1100.webp") type("image/webp")); }
:root[data-theme="proof-light"] .kb-art-detect .kb-art-frame { background-image: url("/images/art/bot-analytics-light-1100.jpg");
  background-image: image-set(url("/images/art/bot-analytics-light-1100.avif") type("image/avif"), url("/images/art/bot-analytics-light-1100.webp") type("image/webp")); }

.kb-art-log .kb-art-frame { background-image: url("/images/art/visitor-analytics-dark-1100.jpg");
  background-image: image-set(url("/images/art/visitor-analytics-dark-1100.avif") type("image/avif"), url("/images/art/visitor-analytics-dark-1100.webp") type("image/webp")); }
:root[data-theme="proof-light"] .kb-art-log .kb-art-frame { background-image: url("/images/art/visitor-analytics-light-1100.jpg");
  background-image: image-set(url("/images/art/visitor-analytics-light-1100.avif") type("image/avif"), url("/images/art/visitor-analytics-light-1100.webp") type("image/webp")); }

/* 404: разведённый мост из библиотеки (not-found/bascule-gap) */
.kb-art-404 .kb-art-frame { aspect-ratio: 1100 / 619; background-image: url("/images/art/bascule-gap-dark-1100.jpg");
  background-image: image-set(url("/images/art/bascule-gap-dark-1100.avif") type("image/avif"), url("/images/art/bascule-gap-dark-1100.webp") type("image/webp")); }
:root[data-theme="proof-light"] .kb-art-404 .kb-art-frame { background-image: url("/images/art/bascule-gap-light-1100.jpg");
  background-image: image-set(url("/images/art/bascule-gap-light-1100.avif") type("image/avif"), url("/images/art/bascule-gap-light-1100.webp") type("image/webp")); }
.nf-wrap { max-width: 720px; margin: 0 auto; padding: var(--space-16) var(--gutter); text-align: left; }
.nf-wrap h1 { font-size: var(--fs-h2); line-height: var(--lh-h2); margin: var(--space-8) 0 var(--space-4); }
.nf-wrap p { color: var(--muted); line-height: var(--lh-body); margin: 0 0 var(--space-6); }
.nf-code { font: 500 var(--fs-label)/1 var(--font-mono); letter-spacing: 0.06em; color: var(--dim); text-transform: uppercase; }
