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

/* ── Сетка карточек ────────────────────────────────────────────────────────
   auto-fill с minmax, а не фиксированное число колонок с медиазапросами.
   Так задано в дизайн-системе, и причина практическая: число карточек в
   ряду подстраивается под ЛЮБУЮ ширину контейнера, включая те, о которых
   мы не думали — сайдбар с фильтрами, узкая колонка «похожих товаров».
   Медиазапросы пришлось бы писать под каждый такой случай отдельно. */
/* Минимальная высота сетки. Без нее при отборе, когда товаров стало меньше,
   страница схлопывается, браузер подтягивает прокрутку вверх — и человек
   теряет место, где стоял. Высоты одной карточки достаточно: дальше сетка
   растет сама. */
.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: var(--grid-gutter-desktop);
  min-height: 320px;
}

/* На мобильных 220px не помещаются в две колонки (2 × 220 + 12 + 2 × 24 =
   500 при ширине экрана 360). Порог опущен до 150px — это ровно та карточка,
   что получается из мобильной сетки: (360 − 2 × 24 − 12) / 2. */
@media (max-width: 640px) {
  .card-grid {
    grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
    gap: var(--grid-gutter-mobile);
  }
}

/* ── Карточка ──────────────────────────────────────────────────────────────
   Порядок элементов снят с боевой страницы категории: скидка, фото, цена,
   старая цена, название, кнопка. Цена ПЕРЕД названием — так на сайте, и это
   осмысленно для каталога запчастей, где выбирают в первую очередь по цене
   при одинаковой детали. */
/* Все вертикальные отступы внутри карточки задаются ОДНИМ свойством — gap.
   До этого каждый элемент нес свой margin, их было пять штук, и они спорили:
   бейдж Пэй отбивался на 4px вместо 8, у названия min-height добавлял еще до
   4px снизу, а между рейтингом и кнопкой не было отступа вообще — margin-top:
   auto дает МИНИМУМ НОЛЬ, а не расстояние. Кнопка вставала вплотную к тексту.

   Теперь ритм один: var(--gap-inner) между всем. Исключение одно и оно ниже. */
.card {
  display: flex;
  flex-direction: column;
  gap: var(--gap-inner);
  min-width: 0;
}

/* Перед кнопкой — 16px вместо 8. Селектор указывает на элемент, стоящий
   предпоследним, то есть прямо перед кнопкой: это либо рейтинг, либо название,
   если оценок нет. Так отступ не зависит от того, какие блоки карточка
   показывает, и его не нужно дублировать на каждом из них.
   margin-bottom складывается с gap: 8 + 8 = 16. */
.card > :nth-last-child(2) { margin-bottom: var(--gap-inner); }

.card__media {
  position: relative;
  aspect-ratio: 1 / 1;
  border-radius: 8px;
  overflow: hidden;
}

/* Ссылка вложена в контейнер, а не совпадает с ним: поверх снимка лежат
   кружки-индикаторы, а сам контейнер ловит наведение для переключения. */
.card__media-link { display: block; width: 100%; height: 100%; }

/* Второе размещение избранного — поверх снимка, справа вверху. Без обводки и
   подложки, только знак: так просил заказчик, для сравнения с кнопкой у
   «В корзину».
   Отступ 8px от краев — тот же шаг, что у кружков галереи снизу: для всего,
   что лежит на снимке, сетка отсчитывается от его края.
   Кнопка стоит РЯДОМ со ссылкой, а не внутри нее: кнопка внутри ссылки
   недопустима. */
.card__fav--over {
  position: absolute;
  top: var(--gap-inner);
  right: var(--gap-inner);
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  /* Область кнопки равна знаку: пустого поля внутри нет. Иначе знак стоял бы
     в 14px от края снимка (8px отступа плюс 6px поля), хотя задано 8.
     Исключение из правила о размере цели: 28px вместо 44. Допустимо —
     минимум WCAG 2.5.8 это 24px, и он выдержан. */
  width: 24px;
  height: 24px;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  color: var(--color-text-secondary);
  transition: color .15s ease;
}
/* 24px — на ступень меньше основного размера иконки интерфейса. Сердце лежит
   поверх снимка и не должно спорить с ним за внимание.
   Оба знака, контурный и залитый, рисуются одинаково: их окна подрезаны так,
   что больший габарит у каждого равен 20px из области 24. */
.card__fav--over svg { width: 24px; height: 24px; display: block; }

/* Правила показа контура и заливки нужны отдельно: те, что написаны для
   .card__fav, сюда не достают — у этой кнопки другой класс. Без них оба
   знака рисовались друг на друге. */
.card__fav--over .card__fav-on { display: none; }
.card__fav--over .card__fav-off { display: block; }

/* В избранном — заливка брендовым. */
.card__fav--over[aria-pressed="true"] { color: var(--color-brand-strong); }
.card__fav--over[aria-pressed="true"] .card__fav-on { display: block; }
.card__fav--over[aria-pressed="true"] .card__fav-off { display: none; }

@media (hover: hover) and (pointer: fine) {
  /* При наведении контур сменяется заливкой светло-серым: подсказка, что
     знак нажимается, без обещания, что товар уже отложен. Только для
     ненажатого состояния — у нажатого свой цвет, и подменять его серым
     значило бы сообщать обратное. */
  .card__fav--over:hover:not([aria-pressed="true"]) { color: var(--color-placeholder); }
  .card__fav--over:hover:not([aria-pressed="true"]) .card__fav-on { display: block; }
  .card__fav--over:hover:not([aria-pressed="true"]) .card__fav-off { display: none; }
}

/* ── Нет в наличии ─────────────────────────────────────────────────────────
   Снимок гасится прозрачностью, поверх него плашка с надписью. Два признака
   вместо одного: приглушенное фото само по себе можно принять за неудачный
   снимок, а надпись без гашения теряется среди прочих подписей карточки.

   Прозрачность 0.35 — сильная намеренно: товар должен читаться как
   недоступный с первого взгляда, при беглом просмотре сетки. */
.card--out .card__media img,
.card--out .card__media--empty .card__media-link > svg { opacity: .35; }

/* Кружки галереи гасятся вместе со снимком: листать нечего, товара нет. */
.card--out .card__dots { opacity: .35; }

.card__stock {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  /* Плашка не перехватывает указатель: под ней ссылка на товар, и карточка
     должна открываться нажатием в любое место снимка. Товара нет в наличии,
     но страница его существует. */
  pointer-events: none;
  display: inline-flex;
  align-items: center;
  min-height: 32px;
  padding: 0 var(--gap-block);
  border-radius: 8px;
  /* Заливка непрозрачная, хотя снимок под ней приглушен: полупрозрачная
     плашка на приглушенном фоне дает мутный текст. */
  background: var(--color-white);
  color: var(--color-text);
  font-size: var(--text-ui);
  line-height: var(--lh-ui);
  font-weight: var(--fw-h2);
  white-space: nowrap;
}

/* Кружки — ИНДИКАТОР, а не кнопки. Переключение висит на самой фотографии:
   она делится по ширине на равные полосы, наведение на полосу показывает
   соответствующий снимок. Поэтому кружкам не нужен размер цели в 24px, и они
   стоят вплотную — при семи снимках ряд занимает 66px вместо 192px, которые
   получались из кнопок 24x24.

   pointer-events:none обязателен: иначе кружки перехватывали бы наведение у
   фотографии под ними, и над нижним краем снимок переставал бы меняться.

   Выводятся только когда снимков больше одного: единственный кружок ничего не
   сообщает. */
.card__dots {
  position: absolute;
  left: 0;
  right: 0;
  /* Отступ от нижнего края снимка — --gap-inner, шаг шкалы. При --gap-tight
     кружки липли к границе, а следующая ступень подняла бы их на 16px и они
     начали бы перекрывать деталь. Отсчет идет от фотографии: для кружков
     сетка — это ее край, а не край карточки. */
  bottom: var(--gap-inner);
  display: flex;
  justify-content: center;
  gap: var(--gap-tight);
  pointer-events: none;
}

.card__dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--color-placeholder);
  transition: background-color .15s ease, transform .15s ease;
}
/* Активный помечен цветом И размером: цвет как единственный признак не
   работает для тех, кто его не различает. */
.card__dot[data-current="true"] {
  background: var(--color-text);
  transform: scale(1.34);
}

/* Заглушка, когда фотографии нет. Знак камеры с подписью «Нет фотографии»:
   пустой серый квадрат человек читает как незагрузившуюся картинку, а не как
   отсутствие снимка.

   SVG, а не файл-картинка: масштабируется без потерь на экранах любой
   плотности, не требует отдельного запроса и берет цвет от currentColor.

   Цвет --color-text-on-plate: подпись это текст, норма для него 4.5:1 и здесь
   выходит 5.82. В присланном образце стоял #B9B9B9 — 1.8:1, то есть подпись
   почти не читалась. */
.card__media--empty { background: var(--color-gray-light); }

/* Центрируется и красится ССЫЛКА, а не контейнер. Две причины, обе неочевидны:
   ссылка растянута на весь квадрат, поэтому центрирование контейнера ничего не
   дает — знак прижимается к началу ссылки. И у ссылки собственный цвет, синий
   или фиолетовый после посещения; унаследованный от контейнера он
   перекрывает, и знак красился в цвет ссылки. */
/* Центрируется ССЫЛКА, а не контейнер: ссылка растянута на весь квадрат, и
   центрирование контейнера ничего не дает — знак прижимается к ее началу. */
.card__media--empty .card__media-link {
  display: flex;
  align-items: center;
  justify-content: center;
}
/* Цвета заданы в самом знаке токенами, а не через currentColor: у ссылки
   собственный цвет, синий и фиолетовый после посещения, и он перекрыл бы
   унаследованный. */
/* Правило касается ТОЛЬКО знака «нет фотографии» — того, что лежит внутри
   ссылки. Раньше селектор бил по всем svg внутри пустого слота, и под него
   попадало сердце избранного: вместо 24px оно получало 62% от слота по
   ширине и рисовалось 15px — мельче, чем в карточке со снимком. */
.card__media--empty .card__media-link > svg { width: 62%; height: auto; }

/* cover: фото занимает квадрат целиком. contain оставлял поля по бокам, и
   карточки в ряду выглядели разъезженными — у каждого снимка свои пропорции,
   и пустота была разной. Квадрат одинаковый у всех, содержимое подрезается. */
.card__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* Скидка стоит в строке цены, сразу за перечеркнутой. Три числа читаются
   одним движением: сколько платить, сколько было, насколько дешевле. На фото
   она была оторвана от того, к чему относится.
   Высоту ряда это не ломает, потому что строка цены есть у всех карточек, а
   у нее задан min-height — со скидкой и без скидки высота одинаковая.
   Красный текстом, а не заливкой: #C41A1A на белом дает 5.98:1. Фирменный
   #F02525 не проходит — 4.2:1 при норме 4.5. */
.card__discount {
  display: inline-flex;
  align-items: center;
  min-height: 24px;
  padding: 0 var(--gap-tight);
  border-radius: 4px;
  /* Светлая подложка с темно-красным текстом, а не сплошная заливка. Сплошная
     плашка спорила с кнопкой «В корзину»: два одинаково насыщенных красных
     пятна в одной карточке, и глаз не понимает, что из них действие.
     Светлая читается как пометка к цене, рядом с которой стоит. */
  background: var(--color-discount-bg);
  color: var(--color-brand-strong);
  font-size: var(--text-caption);
  line-height: var(--lh-caption);
  font-weight: var(--fw-h2);
  font-feature-settings: "tnum";
  white-space: nowrap;
}

/* Роль «Цена в сетке» — кегль Body, вес Price. Это ступень шкалы, внесенная
   в дизайн-систему отдельно, а не отступление от нее: роль Price это 24px, и
   в карточке шириной 220px такая цена занимает четверть ширины и перевешивает
   название. Вес остается 700 — цена обязана читаться первой. */
/* Цена и перечеркнутая — в одну строку, старая справа от новой. Так место
   под строкой остается свободным: туда встает виджет оплаты (Яндекс Пэй).
   Если положить старую цену под новой, виджету придется уезжать ниже и
   карточка вырастет на строку. */
.card__prices {
  display: flex;
  /* center, а не baseline: у скидки появилась плашка с фоном, и по базовой
     линии она вставала бы ниже цены — выравнивается текст, а видно
     прямоугольник. По центру плашка и цифры стоят на одной оси. */
  align-items: center;
  flex-wrap: wrap;
  gap: var(--gap-inner);
  min-height: var(--lh-price-card);
}

/* Ступень «цена в сетке»: 20/24 при весе Price.
   Проверены все три варианта. 16px (кегль Body) отличался от названия и
   рейтинга на два пикселя — цена не выделялась. 24px (полная роль Price) —
   масштаб страницы товара, где цена главный элемент экрана; в мини-карточке
   она начинает спорить с фотографией. 20px дает отрыв в шесть пикселей от
   текста и остается в пределах карточки. */
.card__price {
  font-size: var(--text-price-card);
  line-height: var(--lh-price-card);
  font-weight: var(--fw-price);
  color: var(--color-text);
  font-feature-settings: "tnum";
}

.card__price-old {
  font-size: var(--text-ui);
  line-height: var(--lh-ui);
  font-weight: var(--fw-ui);
  color: var(--color-text-secondary);
  font-feature-settings: "tnum";
}

/* Слот под бейдж Сплита. Высота РЕЗЕРВИРУЕТСЯ: 16px, ровно как у самого
   бейджа. Бейдж есть не у всех товаров — он скрывается, когда сумма выходит
   за лимит Сплита, — и без резерва карточка без него становилась на 24px
   короче соседок. Внутри ряда это скрадывал margin-top: auto у кнопок, но
   между РЯДАМИ он не работает: ряд без бейджей вставал ниже соседнего.
   Прежде здесь стояло display: none у пустого слота, и обосновано это было
   тем, что зарезервированное место осталось бы дырой. Дыра в 16px ровнее,
   чем пляска рядов. */
.card__pay { min-height: 16px; }

/* Значок оплаты частями. Высота фиксирована, ширина считается из пропорций;
   max-width не дает ему вылезти за карточку на узких экранах, где 200px
   шире самой карточки. */
.card__pay-badge {
  display: block;
  height: 16px;
  width: 133px;
  max-width: 100%;
}

/* Название в две строки с обрезкой. В каталоге запчастей названия длинные —
   «Щиток с тормозным механизмом задний левый в сборе для Рено Логан 2,
   Сандеро 2, Лада Х Рей (оригинал)» это 100 символов. Без ограничения одна
   карточка растянула бы весь ряд. */
/* Название обрезается ДВУМЯ строками. В каталоге запчастей названия длинные —
   «Щиток с тормозным механизмом задний левый в сборе для Рено Логан 2,
   Сандеро 2, Лада Х Рей (оригинал)» это сто символов. Без ограничения одна
   карточка растягивает весь ряд по высоте.

   Плата известна и принята: в названиях запчастей применимость стоит в
   конце, и на двух строках список машин обрезается. В карточке 236px видно
   66 знаков из ста — до «Рено Логан 2», остальные модели уходят под
   многоточие. Лечится это не версткой, а данными: коротким названием для
   витрины и отдельным полем применимости.

   Высота цели добирается min-height, а НЕ набивкой. Набивка снизу ломает
   обрезку: браузер отсчитывает три строки от края содержимого, и в набивке
   проглядывает начало четвертой. min-height растит бокс, не трогая границу
   обрезки. Заодно отпадает необходимость вычитать набивку из соседних
   отступов — они снова простые числа из шкалы. */
/* Заводские данные. В разметке есть всегда, на экране — только в виде
   списком: в сетке карточка и так плотная, лишние строки вытеснили бы
   название. Скрыты через display:none, а не visibility: место они занимать не
   должны. */
.card__specs { display: none; }

/* Шильдик происхождения товара. Два состояния, и они противопоставлены:
   товар либо оригинальный, либо аналог, третьего нет.
   Оформлены как значок скидки: светлая подложка с темным текстом того же
   тона. Сплошная заливка спорила с кнопкой покупки и с ценой — два-три
   насыщенных пятна в карточке, и глаз не понимает, что из них главное.
   Оригинал зеленый, аналог синий. Пары замерены: 5.45:1 и 5.53:1 при норме
   4.5 (значок скидки для сравнения дает 5.06:1).
   Цвет не единственный признак: в каждом шильдике стоит слово.

   Компонент лежит ЗДЕСЬ, а не в стилях страницы товара: шильдик стоит и в
   карточках выдачи, а те собираются общим генератором. Пока правила жили на
   странице товара, в листинге шильдик выходил простым текстом без плашки. */
.product-mark {
  display: inline-flex;
  align-items: center;
  min-height: 24px;
  margin-bottom: var(--gap-block);
  padding: 0 var(--gap-inner);
  border-radius: 4px;
  font-size: var(--text-caption);
  line-height: var(--lh-caption);
  font-weight: var(--fw-h2);
}
.product-mark--original {
  background: var(--color-mark-original-bg);
  color: var(--color-mark-original);
}
.product-mark--analog {
  background: var(--color-mark-analog-bg);
  color: var(--color-mark-analog);
}
/* «Товар куплен в этом магазине» — не происхождение, а подтверждение факта,
   и оформлено иначе: сплошная зеленая заливка, белым по ней 4.53:1. Светлая
   подложка тут сравняла бы его с шильдиками происхождения, а это утверждение
   другого рода — на нем держится доверие к отзыву. */
.product-mark--verified {
  background: var(--color-status-ok);
  color: var(--color-white);
}
/* Происхождение не указано: место занято, шильдика не видно — ряд без
   шильдиков не должен вставать ниже соседнего. */
.product-mark--empty { visibility: hidden; }

/* Шильдик в карточке. Отступ снизу снят: расстояние до названия задает
   промежуток самой карточки, иначе они сложились бы.
   align-self обязателен: карточка — колонка, и по умолчанию элемент
   растягивается на всю ее ширину. Плашка расползалась от края до края и
   читалась полосой, а не шильдиком. */
.card .product-mark { margin-bottom: 0; align-self: start; }

.card__name {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;

  /* min-height в ДВЕ строки. Раньше его здесь не было, и я это обосновывал —
   обоснование оказалось неверным.

   Без него короткое название занимает 20px вместо 40, кнопка остается внизу
   (ее держит margin-top:auto), а цена с рейтингом поднимаются на строку
   относительно соседей по ряду. Ряд получается рваным: у одной карточки
   рейтинг выше, у другой ниже.

   Побочно это решает и размер цели: ссылка в одну строку давала 20px при
   минимуме 24, и приходилось ссылаться на исключение «Equivalent». Теперь
   40px без всяких оговорок. */
  min-height: calc(var(--lh-ui) * 2);

  font-size: var(--text-ui);
  line-height: var(--lh-ui);
  /* Кегль Interface, начертание Body. Interface — роль подписей интерфейса:
     кнопок, фильтров, пунктов меню, где 500 добавляет плотности коротким
     ярлыкам. Название товара — две строки читаемого текста, по природе это
     Body, а у Body начертание 400.
     Побочно растет отрыв от цены: 400 против 700 читается яснее, чем
     500 против 700. */
  font-weight: var(--fw-body);
  color: var(--color-text);
  text-decoration: none;
}

/* Рейтинг. Показывается ТОЛЬКО когда оценки есть: «0.0 (0)» отталкивает
   сильнее, чем отсутствие строки. Пустой блок в разметку не выводится, место
   под него не резервируется — кнопку к низу прижимает margin-top:auto, и ряд
   все равно ровный.
   Звезда помечена aria-hidden: смысл несет число, а «звезда четыре точка
   девять» скринридер прочитал бы как мусор. Само значение озвучивается
   фразой в aria-label ссылки-рейтинга. */
/* Рейтинг набран основной ступенью интерфейса (14/20), а не подписью (12/16):
   это не сноска к названию, а довод при выборе — по нему сравнивают товары
   в ряду. Звезда 24px — размер иконки интерфейса из дизайн-системы. */
.card__rating {
  display: flex;
  align-items: center;
  /* Высота строки закреплена: она равна знаку звезды. Нужна для состояния
     «оценок нет» — пустая строка без нее схлопнулась бы в ноль. */
  min-height: 24px;
  gap: var(--gap-tight);
  font-size: var(--text-ui);
  line-height: var(--lh-ui);
  font-weight: var(--fw-ui);
  color: var(--color-text);
  text-decoration: none;
}
.card__rating svg {
  width: 24px;
  height: 24px;
  flex-shrink: 0;
  color: var(--color-rating);
}

.card__rating-value { font-weight: var(--fw-h2); font-feature-settings: "tnum"; }

/* Состояние «оценок нет». Строка остается в потоке и держит свои 24px, но не
   видна. Убирать ее совсем нельзя: у карточки без отзывов пропадала бы целая
   строка, и в ряду с соседями она вставала бы выше или ниже них.
   visibility, а не opacity: невидимый элемент заодно уходит из обхода по Tab
   и из дерева для скринридера — читать пустую ссылку на отзывы незачем. */
.card__rating--empty { visibility: hidden; }

/* Рейтинг — ссылка на отзывы, и отзывается она ТОЛЬКО надписью про
   количество. Оценка и звезда остаются как были: оценка — значение, звезда —
   опознаватель, и краснеть им незачем. Нажимается при этом вся строка
   целиком, включая звезду и число.
   Прежде краснела вся строка: подсветка значения читалась так, будто меняется
   сама оценка. */
@media (hover: hover) and (pointer: fine) {
  .card__rating:hover .card__rating-count { color: var(--color-brand); }
}

/* Количество отзывов и перечеркнутая цена — одна ступень: Interface вторичным
   цветом. Это второстепенные числа одного порядка, оба уточняют главное
   значение рядом с собой. */
.card__rating-count {
  /* Начертание Body, а не Interface. Расходится с перечеркнутой ценой, и это
     осознанно: там голые цифры, здесь цифры со словом — по природе обычный
     текст, а у текста начертание 400. */
  font-weight: var(--fw-body);
  color: var(--color-text-secondary);
  font-feature-settings: "tnum";
}

/* Разделитель — фигура, а не знак: символ «·» в разных шрифтах разного
   размера и стоит на разной высоте.
   vertical-align задан числом, а не словом middle: middle ставит точку на
   середину строчных букв, а рядом стоят цифры — их центр выше. По замеру
   точка сидела на 1.5px ниже цифр. 0.26em поднимает ее ровно на эту
   величину: низ на 3.6px над базовой линией, центр на 5.14px. */
.card__rating-count::before {
  content: '';
  display: inline-block;
  width: 0.22em;
  height: 0.22em;
  margin-right: var(--gap-tight);
  vertical-align: 0.26em;
  border-radius: 50%;
  background: var(--color-text-secondary);
}

/* margin-top:auto прижимает кнопку к низу карточки. Без этого в ряду с
   названиями разной длины кнопки вставали бы на разной высоте — глаз читает
   такой ряд как сломанный. */
/* Кнопки в один ряд: «В корзину» тянется, избранное — квадрат 44x44.
   Избранное не подписано словом намеренно: подпись отняла бы половину ширины
   у главного действия, а сердце узнается без пояснений. Название для
   скринридера задается aria-label и меняется вместе с состоянием. */
.card__actions {
  display: flex;
  gap: var(--gap-inner);
  /* auto прижимает ряд кнопок к низу: в ряду карточек с названиями разной
     длины кнопки встают на одной линии. Минимальный отступ дает не auto, а
     правило :nth-last-child(2) выше — auto его не заменяет. */
  margin-top: auto;
}

/* Обводка здесь по правилу «только когда несет смысл»: без нее кнопку без
   заливки не отличить от иконки, лежащей просто так. */
.card__fav {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  flex-shrink: 0;
  padding: 0;
  /* Контур кнопки без заливки: по нему ее и отличают от текста рядом.
     Нетекстовый элемент, несущий смысл, — норма 3:1, отсюда separator. */
  border: 1px solid var(--color-separator);
  border-radius: 8px;
  background: var(--color-white);
  color: var(--color-text-secondary);
  cursor: pointer;
  transition: color .15s ease, border-color .15s ease;
}
.card__fav svg { width: 20px; height: 20px; display: block; }

/* Состояние «в избранном» помечено не только цветом: сердце меняется с
   контурного на закрашенное. Цвет как единственный признак не работает для
   тех, кто его не различает — это требование WCAG 1.4.1. */
.card__fav[aria-pressed="true"] {
  color: var(--color-brand-strong);
  border-color: var(--color-brand-strong);
}
.card__fav .card__fav-on { display: none; }
.card__fav[aria-pressed="true"] .card__fav-on { display: block; }
.card__fav[aria-pressed="true"] .card__fav-off { display: none; }

.card__cart {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 1;
  min-width: 0;
  min-height: 44px;
  /* auto прижимает кнопку к низу, когда над ней остается место: в ряду с
     названиями разной длины кнопки встают на одной линии. Минимальный отступ
     дает не auto, а правило :nth-last-child(2) выше. */
  margin-top: auto;
  padding: var(--gap-inner) var(--gap-block);
  border: 0;
  border-radius: 8px;
  cursor: pointer;
  /* Обводка объявлена сразу и прозрачной. В состоянии «уже в корзине» она
     становится видимой, и если бы ее не было изначально, кнопка при
     переключении подпрыгивала бы на два пикселя. */
  border: 1px solid transparent;
  background: var(--color-cta);
  color: var(--color-white);
  font-size: var(--text-ui);
  line-height: var(--lh-ui);
  font-weight: var(--fw-h2);
  transition: background-color .15s ease, color .15s ease, border-color .15s ease;
}

/* Товар уже в корзине: кнопка выворачивается — белая с красной обводкой — и
   перестает добавлять, а ведет в корзину. Повторное нажатие раньше клало
   товар второй раз, чего человек не ожидает: он нажимает, потому что не
   уверен, что первое нажатие сработало.
   Надпись остается прежней, меняется вид и назначение. Для скринридера этого
   мало, поэтому назначение задается отдельно через aria-label. */
/* Кнопка недоступного товара — «Подобрать аналог». Не заливка: заливку в
   карточке получает покупка, а здесь ее нет. Контур говорит, что нажать
   можно, но это не то же самое, что «купить». */
.card__cart--alt {
  background: var(--color-white);
  border-color: var(--color-separator);
  color: var(--color-text);
  text-decoration: none;
}

/* ── Окно с аналогами ──────────────────────────────────────────────────────
   Нативный <dialog>: он сам запирает фокус внутри, закрывается по Esc и
   возвращает фокус на кнопку. Своя реализация всего этого — полсотни строк,
   которые легко написать с ошибкой. */
.analogs::backdrop { background: rgba(0, 0, 0, .5); }
.analogs {
  /* Ширина под шесть карточек в ряд: 6 x 176 + 5 промежутков по 24 + набивка
     64 = 1240. Показываем не больше шести аналогов, значит все помещаются
     одной строкой и окно не превращается в сетку с прокруткой. */
  width: min(calc(1176px + 2 * var(--pad-box-xl)), calc(100vw - var(--gap-section)));
  max-height: calc(100vh - var(--gap-section));
  /* margin: auto обязателен: без него окно прижимается к верху. Браузер
     центрирует <dialog> только при значении auto по обеим осям. */
  margin: auto;
  /* Набивка окна — --pad-box-xl, как у остальных окон (решение 17.09.2026).
     Ширина: шесть карточек 6 x 176 + пять промежутков 5 x 24 = 1176 плюс две
     набивки. Прежний комментарий обещал набивку 64 на две стороны, а стояло
     16 — теперь число и описание совпадают. */
  padding: var(--pad-box-xl);
  border: 0;
  border-radius: 8px;
  background: var(--color-white);
  color: var(--color-text);
  overflow-y: auto;
}
.analogs__head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--gap-block);
  margin-bottom: var(--gap-component);
}
.analogs__title {
  font-size: var(--text-h2);
  line-height: var(--lh-h2);
  font-weight: var(--fw-h2);
}
.analogs__subtitle {
  margin-top: var(--gap-tight);
  font-size: var(--text-ui);
  line-height: var(--lh-ui);
  font-weight: var(--fw-body);
  color: var(--color-text-secondary);
}
.analogs__close {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  flex-shrink: 0;
  padding: 0;
  border: 0;
  border-radius: 8px;
  cursor: pointer;
  background: var(--color-gray-light);
  color: var(--color-text);
}
.analogs__close svg { width: 20px; height: 20px; }
.analogs__grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: var(--gap-component);
}
/* На телефоне набивка прежняя, 16px: мобильная версия — отдельная задача. */
@media (max-width: 640px) {
  .analogs { padding: var(--pad-box); }
}
.analogs__empty {
  font-size: var(--text-ui);
  line-height: var(--lh-ui);
  font-weight: var(--fw-body);
  color: var(--color-text-secondary);
}

@media (hover: hover) and (pointer: fine) {
  .analogs__close:hover { background: var(--color-border); }
}

.card__cart[data-in-cart="true"] {
  background: var(--color-white);
  border-color: var(--color-cta);
  color: var(--color-cta);
}




@media (hover: hover) and (pointer: fine) {
  .card__name:hover { color: var(--color-brand); }
  .card__cart:hover { background: var(--color-cta-hover); }
  /* Строго ПОСЛЕ правила выше: у обоих одинаковый вес, и при обратном порядке
     кнопка подбора аналога краснела бы от общего правила покупки. Красная
     заливка принадлежит только покупке. */
  .card__cart--alt:hover { background: var(--color-gray-light); }
  .card__cart[data-in-cart="true"]:hover {
    background: var(--color-white);
    border-color: var(--color-cta-hover);
    color: var(--color-cta-hover);
  }
}

/* ── Полоса прокрутки ──────────────────────────────────────────────────────
   Рисуется своим элементом, а не средствами CSS. Причина не в прихоти:
   современный Chrome, увидев стандартные scrollbar-width и scrollbar-color,
   ИГНОРИРУЕТ правила ::-webkit-scrollbar. А стандартные свойства не умеют ни
   ширину в 8px, ни скругление дорожки — получается системная тонкая полоса,
   не похожая на ту, что в панели каталога.

   Величины взяты оттуда же: ширина 8, скругление 4, светлая дорожка,
   ползунок на ступень темнее. */
.scrollbox { position: relative; }

/* Прокрутки по умолчанию НЕТ. Это важнее, чем кажется: элемент с
   overflow-y:auto становится областью прокрутки, даже когда прокручивать
   нечего, и колесо мыши уходит в него, а не на страницу. Вместе с
   overscroll-behavior:contain это полностью съедает прокрутку под курсором —
   страница стоит, хотя списку двигаться некуда.
   Прокрутка включается только вместе с ограничением высоты — см. .is-open в
   стилях листинга. */
.scrollbox__view {
  overflow: visible;
  /* Родную полосу убираем в обоих семействах браузеров. */
  scrollbar-width: none;
}
.scrollbox__view::-webkit-scrollbar { display: none; width: 0; height: 0; }

/* Дорожка появляется, только когда есть что прокручивать: полоса на коротком
   списке сообщала бы о прокрутке, которой нет. */
.scrollbox__bar {
  position: absolute;
  top: 0;
  right: 0;
  width: 8px;
  border-radius: 4px;
  background: var(--color-gray-light);
  opacity: 0;
  pointer-events: none;
  transition: opacity .15s ease;
}
.scrollbox.is-scrollable .scrollbox__bar { opacity: 1; pointer-events: auto; }

.scrollbox__thumb {
  position: absolute;
  left: 0;
  width: 8px;
  border-radius: 4px;
  background: var(--color-scrollbar-track);
  cursor: grab;
}
.scrollbox__thumb:active { cursor: grabbing; }

/* ── Вы недавно смотрели ───────────────────────────────────────────────────
   Сквозной блок: листинг, страница товара, главная. Разметки в странице нет,
   она пустая — блок собирает скрипт из истории просмотров.

   Почему не отдается с сервера: содержимое у каждого свое. Отдай его в HTML —
   поисковик проиндексирует случайную чужую подборку как часть страницы.

   Появляется, только если история не пуста. Пустой блок с заголовком хуже
   отсутствующего: он обещает содержимое и не дает его. */
/* Сверху 64px, роль «между блоками страницы в конце содержимого». Снизу
   ничего: отступ до подвала держит набивка рабочей области, иначе он
   складывался бы с ней вдвое. */
.recent { margin-top: var(--gap-page); }
.recent[hidden] { display: none; }

/* Отступ 24px, как у заголовков разделов на странице товара: роль «между
   компонентами». Стояло 16 — расстояние смысловых кусков внутри компонента,
   и заголовок читался частью сетки, а не ее названием. */
.recent__title {
  margin-bottom: var(--gap-component);
  font-size: var(--text-h2);
  line-height: var(--lh-h2);
  font-weight: var(--fw-h2);
  color: var(--color-text);
}

/* Сетка ровно на ПЯТЬ колонок, а не auto-fill, как в выдаче. В выдаче число
   колонок подбирается по ширине, потому что карточек там сотня; здесь их
   пять, и второй ряд из одной-двух штук выглядел бы обрывком.
   min-height у сетки выдачи держит высоту при отборе — здесь отбирать нечего,
   и запас высоты дал бы пустоту под блоком. */
.recent__grid {
  grid-template-columns: repeat(5, minmax(0, 1fr));
  min-height: 0;
}

@media (max-width: 1024px) {
  /* Пять карточек по 220px требуют 1196px вместе с промежутками. Ниже этого
     колонки подбираются по ширине, как в выдаче. */
  .recent__grid { grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); }
}
@media (max-width: 640px) {
  .recent__grid { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); }
}

/* ── Карточка в строке ─────────────────────────────────────────────────────
   Второй вид раскладки того же компонента. Разметка карточки НЕ меняется —
   меняется только признак у родителя.

   Включается двумя способами: классом на сетке (так вид показывает витрина
   компонентов) и признаком data-catalog-view на html, который ставит ранний
   скрипт по сохраненному выбору. Второй нужен, чтобы вид применился ДО
   отрисовки: класс на сетке скрипт может повесить только после разбора
   разметки, и страница успевает мигнуть плиткой.

   Живет в компоненте, а не в стилях листинга: раскладка принадлежит карточке.
   Пока правила лежали в стилях страницы, витрина рисовала строки плиткой.

   Ниже 768px раскладки строкой нет вовсе: три зоны в одну строку там не
   помещаются, и переключатель на таких экранах скрыт. */
@media (min-width: 768px) {

  /* Карточки разделены линией, а не промежутком: страница белая, и белый блок
     на белом ничего не отделяет. В образцах, где карточки — белые блоки, фон
     страницы серый; менять фон всей страницы ради списка нельзя. */
  :is(.card-grid--list, [data-catalog-view="list"] .card-grid) {
    grid-template-columns: minmax(0, 1fr);
    gap: 0;
  }

  :is(.card-grid--list, [data-catalog-view="list"] .card-grid) .card {
    display: grid;
    /* Снимок 200px: при 160 он мельче высоты текста рядом и выглядит
       приставленным. Правая колонка 200px — под цену и кнопку. */
    grid-template-columns: 200px minmax(0, 1fr) 200px;
    /* Пятая строка пустая, и она обязательна. Снимок растянут на все строки и
       высок: без нее лишняя высота распределяется МЕЖДУ строками, и между
       ценой, значком оплаты и кнопкой появляются провалы. */
    grid-template-rows: auto auto auto auto 1fr;
    column-gap: var(--gap-component);
    row-gap: var(--gap-inner);
    align-items: start;
    padding: var(--gap-block) 0;
    border-bottom: 1px solid var(--color-divider-strong);
  }
  /* У последней линии нет: под ней уже пагинация со своим отступом. */
  :is(.card-grid--list, [data-catalog-view="list"] .card-grid) .card:last-child { border-bottom: 0; }

  :is(.card-grid--list, [data-catalog-view="list"] .card-grid) .card__media   { grid-column: 1; grid-row: 1 / -1; }

  /* В строке места больше — название показывается тремя строками вместо двух и
     без минимальной высоты: выравнивать нечего, соседних карточек в ряду нет. */
  /* Номер строки НЕ задан — элементы средней колонки встают подряд. Пока он
     стоял, у карточки без рейтинга оставалась пустая строка: место под него
     резервировалось, даже когда рейтинга нет. В строке резервировать незачем —
     соседних карточек в ряду нет, и выравнивать нечего. */
  :is(.card-grid--list, [data-catalog-view="list"] .card-grid) .card__name {
    grid-column: 2;
    min-height: 0;
    -webkit-line-clamp: 3;
    line-clamp: 3;
  }

  /* Рейтинг сразу под названием, до заводских данных: он помогает выбрать, а
     артикул и применимость — проверить уже выбранное. */
  :is(.card-grid--list, [data-catalog-view="list"] .card-grid) .card__rating { grid-column: 2; align-self: start; }

  /* Заводские данные: бренд, артикул, марка и модель. В сетке их нет — места
     не хватает, а в строке они уместны: по артикулу и применимости подбирают
     запчасть.
     display: grid ОБЯЗАТЕЛЕН: в компоненте они скрыты через display:none, и
     без явного значения здесь остались бы скрытыми. */
  :is(.card-grid--list, [data-catalog-view="list"] .card-grid) .card__specs {
    display: grid;
    grid-column: 2;
    gap: var(--gap-tight);
    margin-top: var(--gap-tight);
    /* Роль Body: пары «название — значение» это читаемый текст, тот же, что в
       таблице характеристик на странице товара, и набор у них обязан
       совпадать. */
    font-size: var(--text-body);
    line-height: var(--lh-body);
    font-weight: var(--fw-body);
  }
  :is(.card-grid--list, [data-catalog-view="list"] .card-grid) .card__specs div { display: flex; gap: var(--gap-inner); }
  :is(.card-grid--list, [data-catalog-view="list"] .card-grid) .card__specs dt {
    flex-shrink: 0;
    min-width: 72px;
    color: var(--color-text-secondary);
  }
  :is(.card-grid--list, [data-catalog-view="list"] .card-grid) .card__specs dd {
    margin: 0;
    color: var(--color-text);
    font-feature-settings: "tnum";
  }

  :is(.card-grid--list, [data-catalog-view="list"] .card-grid) .card__prices  { grid-column: 3; grid-row: 1; }
  :is(.card-grid--list, [data-catalog-view="list"] .card-grid) .card__pay     { grid-column: 3; grid-row: 2; }
  :is(.card-grid--list, [data-catalog-view="list"] .card-grid) .card__actions { grid-column: 3; grid-row: 3; }

  /* Правило «перед кнопкой 16px» из плитки здесь не нужно: в строке кнопка
     стоит в своей колонке, а не под текстом. */
  :is(.card-grid--list, [data-catalog-view="list"] .card-grid) .card > :nth-last-child(2) { margin-bottom: 0; }
}

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

   Только прозрачность, без сдвига. Мгновенная подмена читается как рывок:
   глазу не за что зацепиться, содержимое просто стало другим. Проявления
   достаточно, чтобы снять рывок, а подъем добавляет к нему собственное
   движение — и вместе они дают не плавность, а суету.

   Для тех, у кого в системе выключены анимации, смена мгновенная. Это не
   украшение, а требование WCAG 2.3.3: движение на экране вызывает у части
   людей физическое недомогание. */
@keyframes besfo-appear {
  from { opacity: 0; }
  to   { opacity: 1; }
}

.appear-in { animation: besfo-appear .18s ease both; }

/* Волна по первым трем, дальше общая задержка: последовательная на двенадцати
   элементах дала бы заметное ожидание в конце. */
.appear-in:nth-child(2) { animation-delay: .03s; }
.appear-in:nth-child(3) { animation-delay: .06s; }
.appear-in:nth-child(n+4) { animation-delay: .09s; }

/* Окна тоже проступают. Исключение одно — шторка сортировки: она выезжает
   снизу, и это не украшение, а указание, откуда она взялась и куда уйдет. */
dialog[open] { animation: besfo-appear .2s ease both; }
.sort-sheet[open] { animation: besfo-sheet .22s ease both; }
@keyframes besfo-sheet {
  from { opacity: 0; transform: translateY(24px); }
  to   { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  .appear-in,
  dialog[open],
  .sort-sheet[open] { animation: none; }
}

/* ── Боковое меню разделов ─────────────────────────────────────────────────
   Один блок на две страницы: подкатегории в листинге и разделы личного
   кабинета. Поэтому стили живут здесь, а не в стилях листинга (перенесено
   17.09.2026 из page-catalog.css без изменений).

   В кабинете используется та же разметка: заголовок .subcats__title, список
   .subcats__list, пункты .subcats__item, текущий — span с aria-current="page".
   Обертка прокрутки и кнопка «Показать все» нужны длинному списку категорий,
   у кабинета их нет: разделов восемь. */
.subcats { margin-bottom: var(--gap-component); }
/* Высота 40px и текст по центру — как у вариантов сортировки справа. Боксы
   обеих колонок и так стоят на одном уровне, но краска расходилась на 10px:
   заголовок в боксе 20px рисуется с самого верха, а подпись кнопки в боксе
   40px — по центру. Выравнивать надо то, что видно, а не то, что в коде. */
/* Высота ЖЕСТКАЯ, не минимальная: строка сортировки справа имеет ту же
   высоту, и первые строки обеих колонок должны стоять на одном уровне.
   С min-height высота зависела от содержимого и не совпадала. */
.subcats__title {
  display: flex;
  align-items: center;
  height: 40px;
  margin-bottom: var(--gap-inner);
  font-size: var(--text-ui);
  line-height: var(--lh-ui);
  font-weight: var(--fw-h2);
  color: var(--color-text);
}

/* Ни отрицательных полей, ни набивки по бокам: список должен помещаться в
   свою колонку целиком. Иначе он вылезает вправо и съедает промежуток до
   выдачи. */
.subcats__list {
  display: flex;
  flex-direction: column;
}
.subcats__list.is-open {
  max-height: 384px;
  overflow-y: auto;
  overflow-x: hidden;
  overscroll-behavior: contain;
}

.subcats__item {
  display: flex;
  align-items: center;
  gap: var(--gap-inner);
  min-height: 32px;
  padding: var(--gap-tight) 0;
  border-radius: 4px;
  /* Без этого пункты СЖИМАЮТСЯ: в колоночном флексе с ограниченной высотой
     элементы ужимаются ниже своего содержимого, и двухстрочные названия
     наезжают друг на друга. */
  flex-shrink: 0;
  color: var(--color-text);
  text-decoration: none;
  font-size: var(--text-ui);
  line-height: var(--lh-ui);
  font-weight: var(--fw-body);
}
/* Текущий раздел выделен весом и ссылкой не является — вести на саму себя
   незачем. */
.subcats__item[aria-current="page"] { font-weight: var(--fw-h2); }
.subcats__item[hidden] { display: none; }

@media (hover: hover) and (pointer: fine) {
  /* Меняется цвет текста, а не подложка: в списке из семнадцати строк плашка
     превращает наведение в мигание крупным пятном. */
  .subcats__item:not([aria-current="page"]):hover { color: var(--color-brand); }
}

