/* ==========================================================================
   01-base.css — «голые» элементы: reset, html/body, типографика, скроллбар.
   Второй в порядке подключения: задаёт умолчания, которые компоненты и
   страницы затем уточняют. Здесь НЕТ ни одного класса-компонента — только
   универсальный селектор, html/body, псевдоэлементы скроллбара и h1…h6.

   Заголовки h1…h6 в blog.css физически лежали в разделе «3. LAYOUT»,
   НИЖЕ .container/.main-grid. Здесь они подняты выше — это единственный
   перенос из данного модуля, и он безопасен: селекторы h1…h6/.h1….h6 и
   .container/.main-grid/.main-content/.main-sidebar не пересекаются ни по
   одному элементу, спорить им не за что.
   ========================================================================== */

/* 2. BASE & TYPOGRAPHY
/* ========================================================================== */

* {
  border-color: var(--border);
  box-sizing: border-box;
}

/* --------------------------------------------------------------------------
   RESET, ВОССТАНОВЛЕННЫЙ ПОСЛЕ СНЯТИЯ BOOTSTRAP 3.4.1 (этап 10.4).

   В начало bootstrap.min.css был вшит normalize.css v3.0.3 — в плане этапа
   про него не было ни слова, и вместе с Bootstrap со страницы ушёл он.
   Браузерные умолчания молча вернулись на место.

   Переносим НЕ весь normalize, а только то, на что вёрстка реально опирается.
   Проверялось не на глаз: слепки страниц до снятия Bootstrap (дерево 74e1bf0)
   и после сравнивались по вычисленным стилям в headless Chrome, в список ниже
   попало только то, что там разошлось.

   СОЗНАТЕЛЬНО НЕ ПЕРЕНЕСЕНО (проверено, расхождений нет или правило вредно):
     • display:block для article/aside/section/… , [hidden], img{border:0},
       svg:not(:root){overflow:hidden}, figure{margin}, input[type=checkbox]
       {box-sizing} — заплатки под IE8–9; современные движки делают это сами;
     • a:hover,a:active{outline:0} — снимает фокусную обводку, это регресс
       доступности; у нас есть свои :focus-visible;
     • button{cursor:pointer} — все <button> проекта уже приходят с классом,
       который объявляет cursor (.btn, .btn-flat, .theme-toggle-btn,
       .mobile-menu-toggle, .search-button, .view-toggle-btn,
       .user-dropdown-toggle); в замерах курсор нигде не разошёлся;
     • b,strong{font-weight:700} — UA-умолчание `bolder` в нашей вёрстке
       разрешается в те же 700 (родитель везде 400), расхождения нет;
     • code,kbd,pre,samp{font-family;font-size:1em} — прозаичный код и так
       красится нашими .post-content-правилами в var(--font-mono), а
       font-size:1em для голого <pre> сделал бы его КРУПНЕЕ, чем было при
       Bootstrap (там pre{font-size:13px}) — это не восстановление;
     • table{border-collapse:collapse} и td,th{padding:0} — таблиц нет ни на
       одной из 23 снятых страниц; если появятся, правило заводить вместе с
       остальным оформлением таблиц в 04-prose.css.
   -------------------------------------------------------------------------- */

/* Поля вокруг всего сайта. Без этого браузер даёт body 8px margin: шапка и
   подвал перестают прижиматься к краям вьюпорта, а по периметру страницы
   идёт полоса цвета --background. Затронута каждая страница. */
body {
  margin: 0;
}

/* Элементы форм не наследуют шрифт от body — это давнее умолчание браузеров,
   normalize его гасил. Без правила <button class="btn"> рисовался системным
   Arial 13.33px/normal, а стоящая рядом <a class="btn"> — шрифтом Inter 16px:
   один класс давал две разные кнопки. Через .form-control закрыта была только
   часть полей, голые button/select/textarea — нет.
   `font: inherit` (а не одна font-family) возвращает заодно размер и
   межстрочный интервал; `color: inherit` — цвет (у .mobile-menu-toggle он
   уезжал в чёрный вместо --foreground); `margin: 0` снимает системные поля
   вокруг полей ввода. */
button,
input,
optgroup,
select,
textarea {
  font: inherit;
  color: inherit;
  margin: 0;
}

/* Блок с преформатированным текстом должен прокручиваться сам, а не растягивать
   родителя. Для кода внутри статей это уточняется в 04-prose.css; здесь —
   страховка для любого <pre> вне .post-content. Правило прямо связано с
   обрезкой контента на узких экранах: без него длинная строка кода расширяет
   min-content своего предка. */
pre {
  overflow: auto;
}

html {
  scroll-behavior: smooth;
  text-size-adjust: 100%;
  -webkit-text-size-adjust: 100%;
  /* The page background is driven solely by the theme's --background token.
     data-theme is set on :root synchronously in <head> before first paint,
     and the core stylesheets are render-blocking <link>s, so var(--background) already
     resolves to the correct theme color by the time the canvas is painted —
     no white->black flash on navigation. Do NOT also set an inline
     background-color on <html>/<body> from JS: an inline value outranks this
     rule, and keeping the two in sync by hand is exactly what caused the
     "background stuck in the old theme while everything else switched" bug. */
  background-color: var(--background);
}

body {
  background-color: var(--background);
  color: var(--foreground);
  font-family: var(--font-sans);
  font-size: 1rem; /* resolves against the :root 16px scale (see :root font-size) */
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

/* Guard against horizontal scroll: any wide element (oversized image, table,
   sticky-positioned decoration, box-shadow bleed) would otherwise force the
   page wider than the viewport on phones. The specific overflow sources
   (header-actions, post-content media) are handled separately below; this is
   the safety net that keeps sub-pixel/rounding artefacts from producing a
   sliver of horizontal scroll. */
html,
body {
  overflow-x: hidden;
}

/* Custom Scrollbar */
::-webkit-scrollbar {
  width: 8px;
}
::-webkit-scrollbar-track {
  background: var(--background);
}
::-webkit-scrollbar-thumb {
  background: oklch(0.3 0 0);
  border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
  background: oklch(0.4 0 0);
}
body[data-theme="light"] ::-webkit-scrollbar-thumb {
    background: oklch(0.8 0 0);
}
body[data-theme="light"] ::-webkit-scrollbar-thumb:hover {
    background: oklch(0.7 0 0);
}
/* Typography — modular scale (1.25 ratio) on the :root 16px base.
   Tighter than the previous 2.8rem h1 so headings sit in proportion with
   the now 16px body copy instead of overwhelming it. */
h1, .h1 { font-size: 2rem; font-weight: 700; margin-bottom: 1rem; line-height: 1.2; }
h2, .h2 { font-size: 1.75rem; font-weight: 600; margin-bottom: 0.875rem; line-height: 1.25; }
h3, .h3 { font-size: 1.5rem; font-weight: 600; margin-bottom: 0.75rem; line-height: 1.3; }
h4, .h4 { font-size: 1.25rem; font-weight: 600; margin-bottom: 0.625rem; line-height: 1.35; }
h5, .h5 { font-size: 1.125rem; font-weight: 500; margin-bottom: 0.5rem; line-height: 1.4; }
h6, .h6 { font-size: 1rem; font-weight: 500; margin-bottom: 0.5rem; line-height: 1.5; }

/* Slightly step down the two largest headings on small screens so a long
   article title never wraps into an oversized block on phones. */
@media (max-width: 767px) {
  h1, .h1 { font-size: 1.75rem; }
  h2, .h2 { font-size: 1.5rem; }
}

/* Базовый цвет ссылки.
   До этапа 10.4 его давал Bootstrap: `a { color: #337ab7; text-decoration: none }`
   внутри bootstrap.min.css. Вместе с фреймворком правило ушло, а собственного
   в проекте не было — и ссылки провалились в браузерный дефолт #0000EE
   с подчёркиванием. Это хуже, чем просто некрасиво: #0000EE зашит в UA, про
   токены не знает и в тёмной теме не меняется, то есть на тёмном фоне даёт
   гарантированный провал контраста.
   Правило намеренно живёт в базовом слое и держится на одном теге: компоненты
   (.button, .btn, .post-card, навигация, вкладки) объявляют свой цвет явно
   и перебивают его специфичностью. */
a {
    color: var(--primary);
    text-decoration: none;
}
a:hover {
    text-decoration: underline;
}

/* ==========================================================================
   ПОЛЬЗОВАТЕЛЬСКИЕ ПРЕДПОЧТЕНИЯ: ДВИЖЕНИЕ И КОНТРАСТ (этап 10.6)

   Правила намеренно живут в базовом слое и бьют по универсальному селектору:
   движение в проекте объявляется в 115+ местах (02-layout, 03-components,
   pages/*), и перечислять их поимённо — гарантированно отстать от следующей
   правки. Точечные исключения, где мало «сделать быстро» и надо «не делать
   вовсе», описаны рядом со своими компонентами (02-layout.css, 04-prose.css).
   ========================================================================== */

/* --------------------------------------------------------------------------
   prefers-reduced-motion: reduce.

   Системный флаг «меньше движения» (macOS: Reduce Motion, Windows: Show
   animations off, GNOME: Enable animations off). Его включают не для красоты:
   параллакс, разлёт и разгон вызывают тошноту и головокружение при
   вестибулярных нарушениях (WCAG 2.3.3 Animation from Interactions, уровень
   AAA; плюс 2.2.2 Pause/Stop/Hide для того, что крутится само).

   ПОЧЕМУ НЕ `animation: none` И НЕ `transition: none`.
   Полное выключение убивает события animationend/transitionend: скрипт,
   который ждёт конца анимации, чтобы снять класс или показать блок, при
   `none` не дождётся ничего и оставит интерфейс в промежуточном состоянии.
   Канонический приём — свести длительность к 0.01ms: событие всё равно
   приходит (на следующем кадре), а движения человек не видит.
   Проверено грепом по static/js/ на 2026-07: слушателей animationend /
   transitionend / getAnimations() в проекте сейчас НЕТ ни одного, то есть
   сегодня выбор безразличен. Приём оставлен именно каноническим,
   чтобы следующий скрипт с ожиданием анимации не сломался молча.

   ПОЧЕМУ ДЛИТЕЛЬНОСТЬ, А НЕ ТОЛЬКО ЗАДЕРЖКА: `*-delay` сбрасывается тоже.
   Задержка сама по себе не движение, но если переход мгновенный, а задержка
   осталась 0.5s, состояние меняется рывком спустя полсекунды после действия —
   это читается как поломка, а не как «без анимации».

   `animation-iteration-count: 1` — про бесконечные циклы (пиксельные коты в
   03-components.css: pc-wag/pc-float/pc-tilt/pc-bob, все `infinite`).
   Без этой строки они бы не остановились, а закрутились с частотой кадра.
   Все четыре keyframes заканчиваются нейтральной позой (rotate(0)/translateY(0)),
   поэтому одна мгновенная итерация оставляет кота ровно в покое.

   `scroll-behavior: auto` снимает `html { scroll-behavior: smooth }` выше в
   этом же файле — плавная прокрутка к якорю это как раз «экран поехал сам».
   Универсальный селектор покрывает и html: `*` матчит корневой элемент,
   а !important перебивает объявление без него.
   ВАЖНО: CSS не отменяет ЯВНЫЙ `scrollIntoView({behavior: 'smooth'})` из JS —
   опция объекта сильнее CSS-свойства. Такой вызов в проекте есть
   (static/js/series-toc.js), и лечится он только в JS.
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-delay: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    transition-delay: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* --------------------------------------------------------------------------
   prefers-contrast: more.

   Системный флаг «увеличить контраст» (macOS: Increase Contrast, Windows:
   Contrast themes). Для AA не обязателен — он нужен тем, кому мало и AA.
   Делаем ровно одно, зато сквозное: подменяем ТОКЕН границы, а не
   border-color у элементов. Компоненты пишут `border: 1px solid var(--border)`
   со специфичностью выше `*`, поэтому переопределять универсальным селектором
   бессмысленно — перекрасились бы только те редкие места, что берут цвет
   из `* { border-color }` в начале файла.

   Замер (sRGB после конверсии из oklch, обе темы, к трём поверхностям
   --background / --card / --muted):
     светлая: --border #D8DFE4 → 1.25 / 1.35 / 1.16 : 1
              --border-interactive #7B8186 → 3.64 / 3.94 / 3.41 : 1
     тёмная:  --border #161616 → 1.15 / 1.12 / 1.09 : 1
              --border-interactive #636363 → 3.46 / 3.38 / 3.28 : 1
   То есть граница переходит порог 3:1 из SC 1.4.11 (Non-text Contrast) в
   обеих темах, вместо нынешних «едва заметно». Обычный режим не трогаем
   намеренно: там граница — деликатный разделитель, а не элемент управления.

   Список селекторов повторяет 00-tokens.css дословно (`:root`,
   `:root[data-theme="dark"]`, `body[data-theme="dark"]`, то же для light).
   Иначе не сработает: у `:root[data-theme="dark"]` специфичность (0,2,0)
   против (0,1,0) у голого `:root`, и одного `:root` не хватило бы, чтобы
   перебить тёмную ветку. 01-base.css подключается ПОСЛЕ 00-tokens.css, при
   равной специфичности выигрывает наше правило.
   -------------------------------------------------------------------------- */
@media (prefers-contrast: more) {
  :root,
  :root[data-theme="light"],
  body[data-theme="light"],
  :root[data-theme="dark"],
  body[data-theme="dark"] {
    --border: var(--border-interactive);
  }
}

/* --------------------------------------------------------------------------
   .sr-only / .sr-only-focusable — текст только для скринридера (этап 10.6).

   ЗАЧЕМ. Часть смысла интерфейса живёт в иконках и в порядке блоков: кнопка
   лайка = сердечко + число, у страницы профиля вообще нет видимого заголовка.
   Скринридеру это нечем озвучить. .sr-only даёт положить туда обычный текст,
   не меняя ни одного пикселя.

   ПОЧЕМУ НЕ display:none И НЕ visibility:hidden. Оба убирают элемент не
   только с экрана, но и из дерева доступности — скринридер их не читает
   вовсе. То же у hidden, [aria-hidden] и width/height:0. Нужен элемент,
   который отрисован (значит, доступен), но не виден. Отсюда весь набор:

     position: absolute   — вынимает из потока, чтобы 1px не сдвигал соседей
                            (в частности во flex-контейнерах: абсолютный
                            потомок не становится flex-элементом и не
                            получает gap);
     width/height: 1px    — не 0: элемент нулевого размера часть движков
                            считает несуществующим и выбрасывает из дерева;
     margin: -1px         — компенсирует этот 1px, чтобы он не влиял даже на
                            границы прокрутки родителя;
     overflow: hidden     — текст длиннее 1px не вылезает наружу;
     clip / clip-path     — обрезают саму отрисовку; clip устарел, но всё ещё
                            единственный вариант для старых WebKit, а
                            clip-path: inset(50%) — современная замена.
                            Держим оба: они не конфликтуют;
     white-space: nowrap  — в коробке шириной 1px строка иначе переносится
                            по одной букве, и часть скринридеров читает её
                            по буквам;
     padding: 0;
     border-width: 0      — унаследованные отступ или рамка вернули бы
                            элементу видимые пиксели мимо clip.

   .sr-only-focusable — тот же текст, но проявляется, когда попадает в фокус
   (сам элемент или что-то внутри него). Нужен ссылке «Перейти к содержимому»:
   она обязана быть невидимой до Tab и видимой после. Объявлена здесь, рядом
   с парной утилитой, чтобы обе лежали в одном месте; :focus-within — на
   случай, когда фокус получает не сам элемент, а вложенный в него.
   Собственного оформления (фон, отступы, цвет) утилита не задаёт намеренно:
   это дело того компонента, который её применит, — иначе она диктовала бы
   внешний вид и перестала быть утилитой.
   -------------------------------------------------------------------------- */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border-width: 0;
}

.sr-only-focusable:focus,
.sr-only-focusable:focus-within {
  position: static;
  width: auto;
  height: auto;
  margin: 0;
  overflow: visible;
  clip: auto;
  clip-path: none;
  white-space: normal;
}

/* --------------------------------------------------------------------------
   .icon — единая система иконок (этап 10.8, план 10-design-system).

   Заменила две прежние системы: FontAwesome 6 (CDN, размер через font-size
   на <i>) и россыпь инлайн-SVG с захардкоженными width/height. Теперь все
   иконки — <svg class="icon"><use href="icons.svg#id"/></svg> из шаблонного
   тега {% icon %} (blog/templatetags/blog_tags.py), и их параметры держит
   этот блок, а не атрибуты в разметке.

   Размер — 1em по умолчанию: иконка наследует font-size родителя, как глиф
   шрифта у FontAwesome. Это сохраняет прежнюю оптику в местах, где размер
   задавался через font-size (<i class="fas …"> в кнопках, мета карточек).
   Три именованных модификатора покрывают три размера, сложившихся в проекте
   де-факто (см. plan/artifacts/icons-inventory.md §3.4):
     .icon--sm  0.875rem (14px) — мета в карточках постов/серий;
     .icon--lg  1.25rem  (20px) — заголовки секций сайдбара/футера;
     .icon--xl  3rem     (48px) — пустые состояния (поиск), замена fa-2x.
   flex-shrink:0 — иконка в flex-кнопке/строке не сжимается под давлением
   текста; vertical-align выравнивает по baseline строчного текста.
   color наследуется — currentColor в <symbol> спрайта берёт его, поэтому
   иконка всегда окрашена как соседний текст (включая :hover родителя).
   -------------------------------------------------------------------------- */
.icon {
  display: inline-block;
  width: 1em;
  height: 1em;
  flex-shrink: 0;
  vertical-align: -0.125em;
  fill: none;
  stroke: currentColor;
}

.icon--sm {
  width: 0.875rem;
  height: 0.875rem;
}

.icon--lg {
  width: 1.25rem;
  height: 1.25rem;
}

.icon--xl {
  width: 3rem;
  height: 3rem;
}
