/* ============================================================
   «Витаминный рай» — витрина
   Токены взяты из макетов: Prata + Onest, тёмно-синий #0e2c50,
   оранжевый акцент #ef7f1a, зелёный #5f9e46.
   ============================================================ */

:root {
    /* Два шрифта на две роли: --font — весь текст, который читают
       (меню, описания, цены, кнопки), --font-display — витринные
       заголовки.

       Названы переменными, а не вписаны в каждое правило, потому что
       раньше было именно так: смена шрифта означала правку в пятнадцати
       местах, и пропущенное место молча остаётся на старом шрифте —
       заметно это только на той странице, куда редко заходят. Теперь
       заголовки переводятся на основной шрифт одной строкой:
       --font-display: var(--font). */
    --font:         'Onest', -apple-system, 'Segoe UI', sans-serif;
    --font-display: 'Prata', Georgia, serif;

    --navy:        #0e2c50;
    --ink:         #122e4f;
    --orange:      #ef7f1a;
    --orange-dark: #d96f0e;
    --orange-soft: #fdf1e3;
    --green:       #5f9e46;
    --green-dark:  #4f8a38;
    --green-ink:   #3c6e28;
    --green-soft:  #f1f7ec;
    --blue:        #2f6cb3;
    --red:         #d94b4b;
    --red-soft:    #fdf0f0;

    /*
     * Лестница приглушённого текста. Пять ступеней одного тона (H 214°,
     * S 20%), различаются только светлотой.
     *
     * Она была светлее, и это оказалось не оттенком вкуса, а поломкой.
     * Замер на белом фоне:
     *
     *     --muted   #5c6f87   5.15:1   проходил
     *     --muted-2 #7d8da3   3.38:1   не проходил
     *     --muted-3 #8b9bb0   2.90:1   не проходил
     *     --muted-4 #98a7ba   2.45:1   не проходил
     *
     * AA просит 4.5:1 для обычного текста. То есть три ступени из четырёх
     * были ловушкой: любой, кто брал «второй серый», получал нечитаемую
     * строку — и получал молча, потому что на хорошем мониторе в тёмной
     * комнате разница видна. На телефоне на улице — нет.
     *
     * Держали они не подписи к иконкам, а текст, который читают:
     * хлебные крошки, характеристики товара, текст под заголовком
     * входа в кабинет, подсказки в корзине, старую цену. Владелец
     * заметил это на промокоде («Применить не читается»), но там
     * причина была другая — столкновение классов, — и, разбираясь с
     * ним, вылезла эта.
     *
     * Новая лестница: тот же тон, светлота 36/40/44/47%, шаг ~7%.
     * Ступени по-прежнему различимы рядом, а нижняя держит 4.72:1 —
     * то есть промахнуться теперь нельзя, какую бы ни взяли.
     */
    --muted:       #49596e;  /* 7.15:1 на белом */
    --muted-2:     #52637a;  /* 6.13:1 */
    --muted-3:     #5a6d87;  /* 5.20:1 */
    --muted-4:     #607590;  /* 4.72:1 — нижняя граница AA с запасом */
    /* Пятая ступень — не текст: ею покрашен крестик «убрать из корзины»
       и подобная мелкая графика. Для неё норма другая (3:1 как для
       нетекстового элемента), но прежние #c3cfdd давали 1.70:1 — кнопка
       удаления была почти невидима на белом. */
    --muted-5:     #7e91a9;  /* 3.23:1 */

    --line:        #e6ebf2;
    --line-soft:   #eef2f7;
    --line-dash:   #d4dde8;
    --surface:     #f4f7fb;
    --surface-2:   #f3f6fa;

    --on-navy:     #c9d9ec;
    --on-navy-dim: #9db4d0;
    --on-navy-mute:#8fa9c7;

    --container: 1280px;
    --gutter: 32px;
    --shadow-card: 0 12px 28px rgba(14, 44, 80, .12);
    --shadow-pop:  0 8px 24px rgba(14, 44, 80, .18);
}

*, *::before, *::after { box-sizing: border-box; }

/*
 * Атрибут hidden сильнее любой раскладки.
 *
 * Браузер прячет [hidden] правилом display:none из своей таблицы стилей —
 * а она слабее авторской, поэтому любой класс с собственным display её
 * молча отменяет. Так и вышло: у .stepper стоит display:flex, и счётчик
 * «− 0 +» висел рядом с кнопкой «В корзину» на каждой карточке витрины,
 * хотя разметка честно писала hidden. Скрипт, переключающий .hidden,
 * тоже не работал — ни в карточке товара, ни в каталоге.
 */
[hidden] { display: none !important; }

html, body {
    margin: 0;
    padding: 0;
    background: #fff;
    font-family: var(--font);
    color: var(--ink);
    -webkit-font-smoothing: antialiased;
    /* Мобильные браузеры сами раздувают шрифт в длинных абзацах при
       повороте экрана — вёрстка при этом расползается. Размеры задаём мы. */
    -webkit-text-size-adjust: 100%;
            text-size-adjust: 100%;
}

body { display: flex; flex-direction: column; min-height: 100vh; }
main { flex: 1 0 auto; }

a { color: var(--ink); text-decoration: none; transition: color .15s; }
a:hover { color: var(--orange); }

img { max-width: 100%; }

/* <picture> обязан исчезнуть из вёрстки. Он строчный, высоты у него нет,
   и height:100% у картинки внутри резолвить не от чего: в карточке с
   aspect-ratio 4/3 фото при этом вылезает из рамки. display:contents
   убирает саму коробку, и правила вида «.product-card__media img»
   продолжают работать так, будто обёртки нет — а она появляется только
   там, где для фото готов WebP-вариант. */
picture { display: contents; }

/* Обратная сторона display:contents: дети <picture> становятся детьми
   её родителя, и <source> — тоже. Сам по себе он не рисуется, но коробку
   имеет, и в сетке или флексе занимает место как полноценный элемент:
   в блоке «О магазине» пустой <source> вставал в первую колонку, фото
   уезжало во вторую, а текст — на строку ниже. В строчной вёрстке этого
   не видно, поэтому правило и появилось только вместе с первым фото в
   сетке. Убираем коробку совсем: браузер и без неё читает srcset. */
picture > source { display: none; }

input, button, textarea, select { font-family: inherit; font-size: inherit; }

/* Видимый фокус нужен тем, кто ходит по сайту с клавиатуры.
   :focus-visible не трогает мышь — рамка не мешает обычному клику. */
:focus-visible { outline: 3px solid var(--orange); outline-offset: 2px; border-radius: 4px; }

/* Ссылка «к содержимому» прячется, пока на неё не встал фокус:
   первым Tab читалка и клавиатура пропускают меню целиком. */
.skip-link {
    position: absolute; left: -9999px; top: 0; z-index: 100;
    background: var(--navy); color: #fff; padding: 12px 20px;
    border-radius: 0 0 10px 0; font-weight: 700;
}
.skip-link:focus { left: 0; color: #fff; }

/* Уважаем системную просьбу «поменьше движения». */
@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: .01ms !important; animation-iteration-count: 1 !important;
        transition-duration: .01ms !important; scroll-behavior: auto !important;
    }
}

/*
 * Появление блоков при подходе к ним.
 *
 * Правило ровно одно, и оно НЕ прячет ничего заранее. Обычный способ —
 * написать .reveal { opacity: 0 } и снимать это скриптом — ломается
 * тихо и целиком: не догрузился app.js, старый браузер, ошибка строкой
 * выше в файле — и посетитель видит пустую страницу вместо магазина.
 * Причём поисковому роботу она достанется такой же.
 *
 * Здесь наоборот: блок виден всегда, а класс лишь запускает готовую
 * анимацию «из прозрачности снизу вверх». Не сработал скрипт — не
 * случилось анимации, и это единственное последствие.
 *
 * backwards, и именно backwards, а не both.
 *
 * backwards обязателен: пока идёт задержка из stagger(), кадр «from»
 * должен уже действовать. Без него карточка сначала показывается как
 * обычно, а в момент старта анимации прыгает в прозрачность — мигание
 * тем заметнее, чем дальше карточка в ряду.
 *
 * forwards здесь стоял и оказался вредным. Он держит конечный кадр
 * после конца анимации, а значения из анимаций перебивают в каскаде
 * обычные объявления автора. Конечный кадр — transform: none, и он
 * начинал перебивать .product-card:hover { transform: translateY(-3px) }.
 * Замер на /catalog, карточка после анимации:
 *
 *     с forwards  — matrix(1, 0, 0, 1, 0,  0)   наведение не видно
 *     без него    — matrix(1, 0, 0, 1, 0, -3)   карточка приподнялась
 *
 * То есть подъезд карточек молча отключал подъём при наведении, и сразу
 * на всех трёх видах плиток. Держать конечный кадр тут и не нужно: «to»
 * совпадает с обычным состоянием блока (он и так непрозрачен и не
 * сдвинут), поэтому по окончании анимации сбрасывать нечего.
 *
 * Кто именно и когда получает класс — в app.js, revealOnScroll();
 * там же объяснено, почему видимым сразу блокам его не дают вовсе и
 * почему отыгравший класс всё равно снимают.
 */
@keyframes reveal-in {
    from { opacity: 0; transform: translateY(18px); }
    to   { opacity: 1; transform: none; }
}

.is-revealing { animation: reveal-in .55s cubic-bezier(.22, .61, .36, 1) backwards; }

/*
 * Страховка от слова, которое шире экрана.
 *
 * По-русски такие слова обычные, и одного хватает, чтобы увести вбок всю
 * страницу: заголовок «Политика конфиденциальности» на телефоне выходил
 * за край на 16px, и вместе с ним ехали шапка, подвал и нижняя панель —
 * страница начинала болтаться вбок целиком. Заголовок здесь не единственный
 * подозреваемый и даже не главный: названия товаров приезжают из МойСклад,
 * где их пишет владелец, а карточка на телефоне — 161px шириной, в неё не
 * помещается и «Мультивитаминный».
 *
 * break-word, а не anywhere: первый переносит слово, только если строка
 * ему всё равно мала, и не трогает ни расчёт минимальной ширины колонок,
 * ни текст, который и так помещается.
 */
body { overflow-wrap: break-word; }

h1, h2, h3 { font-family: var(--font-display); font-weight: 400; color: var(--navy); margin: 0; }
h1 { font-size: 36px; }
h2 { font-size: 32px; }
h3 { font-size: 22px; }

.container {
    max-width: var(--container);
    width: 100%;
    margin: 0 auto;
    padding: 0 var(--gutter);
}

.section { padding-top: 64px; }
.section--tight { padding-top: 40px; }
/* Узкая колонка для блоков-текстов вроде «частых вопросов». Раньше эти
   размеры стояли инлайном в шаблоне — в классе их можно переопределить
   в адаптиве и не держать style-src 'unsafe-inline' ради вёрстки. */
.section--narrow { max-width: 900px; margin: 0 auto; }
.section__title-center { text-align: center; margin-bottom: 24px; }
.stack { display: flex; flex-direction: column; }
.muted { color: var(--muted-3); }
.nowrap { white-space: nowrap; }
.visually-hidden {
    position: absolute; width: 1px; height: 1px; overflow: hidden;
    clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}

.eyebrow {
    font-size: 13px; font-weight: 800; letter-spacing: 2px;
    text-transform: uppercase; color: var(--green);
}

.section-head {
    display: flex; align-items: baseline; justify-content: space-between;
    gap: 20px; margin-bottom: 24px; flex-wrap: wrap;
}
.section-head__link { font-weight: 800; font-size: 14.5px; color: var(--orange); }

/* ------------------------------------------------------------ шапка */

.topbar { background: var(--navy); color: var(--on-navy); font-size: 13px; }
.topbar__inner {
    display: flex; align-items: center; gap: 24px;
    padding-top: 8px; padding-bottom: 8px; flex-wrap: wrap;
}
.topbar a { color: var(--on-navy); }
.topbar a:hover { color: #fff; }
.topbar__side { margin-left: auto; display: flex; align-items: center; gap: 18px; flex-wrap: wrap; }
.topbar__hours { color: var(--on-navy-mute); }
.topbar__phone { color: #fff; font-weight: 700; }

.masthead {
    display: grid; grid-template-columns: 1fr auto 1fr;
    align-items: center; gap: 24px;
    padding-top: 20px; padding-bottom: 20px;
}
/*
 * width:auto обязателен. У <img> есть атрибуты width/height, и браузер
 * считает их указанием ширины — задав в стилях одну высоту, логотип
 * получаешь растянутым до атрибутной ширины. Так он и жил: 1420×899
 * показывались как 320×96, то есть вдвое сплюснутыми по горизонтали.
 */
.masthead__logo img { height: 96px; width: auto; display: block; }

.search {
    display: flex; align-items: center; gap: 10px; max-width: 360px;
    border: 1.5px solid var(--line); border-radius: 12px;
    padding: 11px 16px; color: var(--muted-3);
    transition: border-color .15s;
}
.search:focus-within { border-color: var(--orange); }
.search input {
    border: none; outline: none; font-size: 14px; width: 100%;
    color: var(--ink); background: transparent;
}
.search--wide { max-width: 420px; padding: 12px 16px; }
.search--wide input { font-size: 14.5px; }
.search__submit { border: none; background: none; padding: 0; cursor: pointer; display: flex; }

.search { position: relative; }
.suggest {
    position: absolute; top: calc(100% + 8px); left: 0; right: 0; z-index: 30;
    background: #fff; border: 1.5px solid var(--line); border-radius: 12px;
    box-shadow: var(--shadow-pop); overflow: hidden; max-height: 340px; overflow-y: auto;
}
.suggest a {
    display: flex; justify-content: space-between; gap: 14px; align-items: center;
    padding: 11px 16px; font-size: 14px; font-weight: 600; border-bottom: 1px solid var(--line-soft);
}
.suggest a:last-child { border-bottom: none; }
.suggest a:hover { background: var(--surface); }
.suggest b { white-space: nowrap; font-weight: 800; }

.is-busy { opacity: .6; pointer-events: none; }

.masthead__actions { display: flex; align-items: center; gap: 26px; justify-self: end; }
.icon-link {
    position: relative; display: flex; flex-direction: column; align-items: center;
    gap: 4px; font-size: 12px; font-weight: 600; color: var(--ink);
}
.icon-link:hover { color: var(--orange); }
.icon-link__badge {
    position: absolute; top: -7px; right: 2px;
    background: var(--orange); color: #fff; font-size: 11px; font-weight: 800;
    min-width: 18px; height: 18px; border-radius: 999px;
    display: flex; align-items: center; justify-content: center; padding: 0 4px;
}

/*
 * Толчок счётчика корзины при изменении.
 *
 * Единственная анимация здесь, которая не украшение, а ответ. Кнопка
 * «в корзину» отправляет форму через fetch и страницу не перезагружает,
 * так что видимого подтверждения нет вовсе: число в шапке меняется
 * молча. В каталоге кнопка находится за сотни пикселей от счётчика, и
 * на широком экране взгляд в момент клика на него не направлен —
 * человек жмёт второй раз, считая, что не попал.
 *
 * Отсюда и требования: заметно, но коротко (.32s), и без сдвига
 * раскладки — только scale, который на позиционированном значке
 * ничего вокруг не двигает.
 *
 * Ни forwards, ни both: конечный кадр совпадает с обычным состоянием,
 * а держать его — значит перебить в каскаде чужие transform, как это
 * уже случилось с появлением блоков (см. .is-revealing). Здесь есть и
 * вторая причина: класс снимают, чтобы толчок можно было повторить,
 * а с удержанным кадром между снятием класса и следующим толчком
 * значок дёргался бы обратно.
 *
 * Про уменьшение движения отдельно думать не нужно: общее правило
 * prefers-reduced-motion ужимает длительность, а задержки здесь нет —
 * той ловушки, из-за которой выход первого экрана пришлось прятать в
 * no-preference, тут не возникает.
 */
@keyframes cart-bump {
    0%   { transform: scale(1); }
    35%  { transform: scale(1.32); }
    100% { transform: scale(1); }
}

.icon-link__badge.is-bumping,
.bottomnav__badge.is-bumping { animation: cart-bump .32s cubic-bezier(.34, 1.56, .64, 1); }

/*
 * Меню прилипает к верхнему краю при прокрутке.
 *
 * Прилипает именно оно, а не вся шапка. Верхняя строка с адресом и
 * логотип в 96px — это 210px, треть высоты ноутбучного экрана: закрепив
 * их, мы бы отняли у каталога ту самую полосу, ради которой человек и
 * листает. Верх уезжает, меню остаётся.
 *
 * При этом высота самого меню не меняется ни на пиксель — ни отступы, ни
 * шрифт. Это не лень, а единственный способ обойтись без скачка: элемент
 * с position:sticky остаётся в потоке, и всё, что ниже, привязано к его
 * высоте. Ужать его на 8px, пока он висит наверху, — значит подтянуть
 * страницу под ним на те же 8px, ровно в момент прокрутки. Поэтому
 * прилипание видно только по тени и по кнопке корзины справа.
 */
.mainnav {
    border-top: 1px solid var(--line); border-bottom: 1px solid var(--line);
    position: sticky; top: 0; z-index: 30; background: #fff;
    transition: box-shadow .2s ease, border-color .2s ease;
}
.mainnav.is-stuck {
    box-shadow: 0 6px 18px -8px rgba(18, 46, 79, .22);
    border-bottom-color: transparent;
}

/* Метка нулевой высоты перед меню. Отрицательный margin убирает её из
   раскладки целиком — иначе она добавила бы пиксель между шапкой и
   меню, и граница шапки читалась бы двойной. */
.sticky-probe { height: 1px; margin-bottom: -1px; }

/* Кнопка корзины в прилипшем меню. Absolute — чтобы её появление не
   раздвигало строку и не сбивало центровку пунктов: они выровнены по
   середине контейнера, и обычный элемент в потоке сдвинул бы их влево. */
.mainnav__cart {
    position: absolute; right: var(--gutter); top: 50%; transform: translateY(-50%);
    display: inline-flex; align-items: center; gap: 7px;
    font-size: 13.5px; font-weight: 700; color: var(--navy);
    background: var(--surface); border-radius: 999px; padding: 8px 15px;
    animation: nav-cart-in .22s ease both;
}
.mainnav__cart:hover { background: var(--orange-soft); color: var(--orange-dark); }
@keyframes nav-cart-in {
    from { opacity: 0; transform: translateY(-50%) translateX(8px); }
    to   { opacity: 1; transform: translateY(-50%) translateX(0); }
}

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

   На узких экранах overflow-x возвращается медиазапросом: там меню
   снова полоса прокрутки, а выпадающие списки выключены. */
.mainnav__inner {
    display: flex; justify-content: center; flex-wrap: wrap; gap: 0 34px;
    font-size: 14.5px; font-weight: 700;
    position: relative; /* якорь для .mainnav__cart */
}
.mainnav__link {
    display: inline-flex; align-items: center; gap: 5px;
    padding: 14px 2px; border-bottom: 3px solid transparent;
    margin-bottom: -1px; white-space: nowrap;
}
/* Пункт «Покупателям» — кнопка: страницы с таким адресом нет, а фокус
   нужен. Кнопка приносит с собой рамку, фон и шрифт браузера, поэтому
   их снимаем — выглядеть она обязана как соседние ссылки. */
.mainnav__link--btn {
    background: none; border: none; border-bottom: 3px solid transparent;
    font: inherit; color: inherit; cursor: pointer;
}
.mainnav__link.is-active { border-bottom-color: var(--orange); }
.mainnav__caret { flex: none; opacity: .45; transition: transform .15s, opacity .15s; }

/* ---------------------------------------------- выпадающие списки меню

   Ни строчки JS: CSP магазина запрещает встроенные обработчики, а пара
   :hover + :focus-within закрывает мышь, клавиатуру и касание разом. */

.mainnav__item { position: relative; }

.mainnav__drop {
    position: absolute; top: 100%; left: -14px; z-index: 30;
    min-width: 234px; padding: 8px; margin-top: 1px;
    background: #fff; border: 1px solid var(--line); border-radius: 14px;
    box-shadow: 0 18px 40px rgba(23, 38, 56, .13);
    display: flex; flex-direction: column;
    font-size: 14px; font-weight: 600;

    /* visibility, а не display: display:none выключает переход, а один
       лишь opacity:0 оставляет список кликабельным — курсор ловил бы
       невидимую панель, накрывшую первый ряд карточек товара. */
    visibility: hidden; opacity: 0; transform: translateY(-6px);
    transition: opacity .15s, transform .15s, visibility .15s;
}
.mainnav__item:hover .mainnav__drop,
.mainnav__item:focus-within .mainnav__drop {
    visibility: visible; opacity: 1; transform: none;
}
.mainnav__item:hover .mainnav__caret,
.mainnav__item:focus-within .mainnav__caret { opacity: 1; transform: rotate(180deg); }

.mainnav__drop a {
    display: flex; align-items: center; justify-content: space-between; gap: 14px;
    padding: 9px 12px; border-radius: 9px;
    border-bottom: none; margin-bottom: 0; white-space: nowrap;
}
.mainnav__drop a:hover,
.mainnav__drop a:focus-visible { background: var(--surface); color: var(--orange-dark); }

/* Счётчик товаров — подсказка, а не часть названия: мельче и серым. */
.mainnav__drop i {
    font-style: normal; font-weight: 700; font-size: 12px;
    color: var(--muted-3); flex: none;
}

/* «Все товары раздела» — тот же адрес, что и у пункта наверху. Дубль
   намеренный: список раскрывается наведением, и попасть по самому
   пункту, не задев подпункт, выходит не всегда. */
.mainnav__drop-all {
    margin-top: 4px; padding-top: 12px;
    border-top: 1px solid var(--line-soft); border-radius: 0 0 9px 9px;
    color: var(--orange-dark);
}

/* ---------------------------------------------------------- кнопки */

.btn {
    display: inline-flex; align-items: center; justify-content: center; gap: 8px;
    border: none; border-radius: 12px; cursor: pointer;
    font-weight: 800; font-size: 15px; padding: 14px 28px;
    transition: background-color .15s, border-color .15s, color .15s;
    text-align: center;
}
.btn--primary { background: var(--orange); color: #fff; }
.btn--primary:hover { background: var(--orange-dark); color: #fff; }
.btn--green { background: var(--green); color: #fff; }
.btn--green:hover { background: var(--green-dark); color: #fff; }
.btn--ghost { background: #fff; border: 1.5px solid var(--line-dash); color: var(--navy); }
.btn--ghost:hover { border-color: var(--orange); color: var(--orange); }
.btn--quiet { background: var(--surface); color: var(--ink); }
.btn--quiet:hover { background: #e9eff7; color: var(--ink); }
.btn--danger { background: none; color: var(--red); font-weight: 700; }
.btn--danger:hover { background: var(--red-soft); }
.btn--block { width: 100%; }
.btn--sm { padding: 10px 16px; font-size: 13.5px; border-radius: 10px; }
.btn--lg { font-size: 16px; padding: 15px 30px; }
.btn[disabled] { opacity: .55; cursor: not-allowed; }

.linkish { font-weight: 800; font-size: 15px; color: var(--orange); background: none; border: none; cursor: pointer; padding: 0; }

/* ----------------------------------------------------------- формы */

.field { display: flex; flex-direction: column; gap: 7px; font-size: 13.5px; font-weight: 700; color: var(--muted); }
/*
 * Вид поля ввода.
 *
 * `.field__control > input` в списке не для симметрии. Дочерний
 * комбинатор `.field > input` не достаёт до поля пароля: оно лежит не
 * прямо в .field, а внутри обёртки .field__control, куда его положили
 * ради кнопки «Показать». Правило не срабатывало, и поле оставалось в
 * оформлении браузера по умолчанию — тонкая серая рамка без скругления
 * и без внутренних отступов.
 *
 * Так было на каждом поле пароля магазина: вход, регистрация (два поля),
 * смена пароля по ссылке из письма (два), смена пароля в кабинете (три).
 * Рядом с полем телефона, которое лежит в .field напрямую и потому
 * оформлено, это выглядело как незагрузившиеся стили.
 *
 * Соседнее правило ниже — `.field__control input[aria-invalid]` красит
 * рамку в красный — показывает, что рамка тут подразумевалась с самого
 * начала: красить было нечего.
 */
.field > input, .field > textarea, .field > select,
.field__control > input,
.input {
    border: 1.5px solid var(--line); border-radius: 10px;
    padding: 13px 15px; font-size: 15px; outline: none;
    font-weight: 400; color: var(--ink); background: #fff;
    transition: border-color .15s; width: 100%;
}
.field > input:focus, .field > textarea:focus, .field > select:focus,
.field__control > input:focus,
.input:focus { border-color: var(--orange); }
.field > textarea, textarea.input { resize: vertical; min-height: 84px; }
.field__hint { font-weight: 400; font-size: 12.5px; color: var(--muted-4); }
.field__error { font-weight: 700; font-size: 12.5px; color: var(--red); }

/* Поле, которое сервер не принял. Рамкой, а не только текстом снизу: в
   форме на семь строк красная надпись под четвёртой теряется, и человек
   перечитывает всё подряд, ища, к чему она относится. */
.field > input[aria-invalid="true"],
.field__control input[aria-invalid="true"] { border-color: var(--red); }

/* Обёртка поля пароля: кнопка «Показать» ложится поверх правого края
   input, поэтому резервируем под неё место внутри самого поля. */
.field__control { position: relative; display: block; }
.field__control input { width: 100%; }
.field__control--revealable input { padding-right: 92px; }
.field__reveal {
    position: absolute; top: 50%; right: 6px; transform: translateY(-50%);
    border: none; background: none; cursor: pointer; padding: 8px 9px;
    font: inherit; font-size: 12.5px; font-weight: 800; color: var(--muted-3);
    border-radius: 8px;
}
.field__reveal:hover { color: var(--orange); background: var(--surface); }

.field__caps { font-weight: 700; font-size: 12.5px; color: var(--red); }

/* Требования к паролю: список с галочками, без процентов и полосок.
   Пункт, которого человек ещё не выполнил, красным не красим, пока поле
   пустое, — иначе форма встречает новичка тремя ошибками. */
.pwd-rules { list-style: none; margin: 2px 0 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }
.pwd-rules li {
    font-weight: 400; font-size: 12.5px; color: var(--muted-4);
    padding-left: 19px; position: relative; line-height: 1.45;
}
.pwd-rules li::before {
    content: '•'; position: absolute; left: 4px; top: -1px; font-size: 14px; line-height: 1.5;
}
.pwd-rules li.is-ok { color: var(--green-ink); }
.pwd-rules li.is-ok::before { content: '✓'; left: 2px; font-size: 12px; font-weight: 800; }
.pwd-rules li.is-bad { color: var(--muted-3); }

/* Поле-галочка: подпись справа от квадратика, а не над ним, как у
   остальных полей. Ширину квадратика фиксируем — иначе длинная подпись
   (а у согласия на обработку данных она длинная) сжимает его флексбоксом
   до полоски, и нажать становится некуда. */
.field--check { flex-direction: row; align-items: flex-start; gap: 10px; font-weight: 400; }
.field--check > input[type="checkbox"],
.field--check > input[type="radio"] {
    flex: 0 0 auto; width: 18px; height: 18px; margin-top: 2px;
    accent-color: var(--orange); cursor: pointer;
}
.field--check span { font-size: 13px; line-height: 1.5; color: var(--muted); }
/* Ссылка на документ внутри подписи — обычного размера, а не крупная
   кнопочная: .linkish задан под ссылки-действия и в мелком тексте
   выглядел бы вторым заголовком.

   display и min-height сбрасываем не «на всякий случай»: на узких
   экранах .linkish превращается в inline-flex высотой 44px, чтобы по
   ссылке-действию можно было попасть пальцем. Здесь ссылка стоит
   посреди фразы, а inline-flex — цельная коробка, которую перенос
   строки не разрывает: длинное название документа уезжало на отдельную
   строку целиком, а открывающая кавычка оставалась висеть в конце
   предыдущей. Палец по этой ссылке и не целится — целятся по галочке,
   она рядом и своей высоты. Специфичность у селектора выше, поэтому
   правило перебивает медиазапрос независимо от порядка. */
.field--check .linkish {
    font-size: inherit; font-weight: 700; text-decoration: underline;
    display: inline; min-height: 0;
}

.choice {
    display: flex; align-items: flex-start; gap: 12px;
    border: 1.5px solid var(--line); border-radius: 12px; padding: 14px 16px;
    cursor: pointer; font-size: 14.5px; font-weight: 700; color: var(--ink);
    transition: border-color .15s, background-color .15s;
}
.choice:hover { border-color: var(--line-dash); }
.choice input { margin: 2px 0 0; accent-color: var(--orange); }
.choice__text { display: flex; flex-direction: column; gap: 3px; }
.choice__note { font-weight: 400; font-size: 13px; color: var(--muted-2); }
.choice:has(input:checked) { border-color: var(--orange); background: var(--orange-soft); }

/*
 * Строчка «что мы собираем» под формой (partial/consent-note).
 *
 * Нарочно негромкая: это справка, а не предупреждение. Красная плашка на
 * форме адреса читается как «вы что-то сделали не так» и пугает ровно там,
 * где человек ничего плохого не делает.
 *
 * Цвет --muted-2 (6.13:1) вместо более тихого --muted-3: текст мелкий, а
 * для мелкого AA просит те же 4.5:1, и запас тут не лишний — читать её
 * будут на телефоне при дневном свете.
 *
 * align-items:start, чтобы значок держался у первой строки: при переносе
 * на три строки центрирование уводит его в середину абзаца, и он перестаёт
 * читаться как метка всей строки.
 */
.consent-note {
    display: flex; align-items: start; gap: 8px;
    margin: 12px 0 0; padding: 10px 12px;
    background: var(--surface-2); border-radius: 12px;
    font-size: 12.5px; line-height: 1.5; color: var(--muted-2);
}
.consent-note__icon { flex: none; margin-top: 2px; opacity: .75; }

/*
 * Полоса про cookie (partial/cookie-note).
 *
 * Внизу, а не поверх страницы модальным окном. Модальное окно на первом же
 * экране — это требование нажать до того, как человек увидел магазин;
 * полоса внизу сообщает и не мешает смотреть.
 *
 * left/right вместо width:100% — при width:100% плюс горизонтальных
 * отступах полоса становится шире экрана и добавляет странице боковую
 * прокрутку. На телефоне это заметно сразу: страница ездит вбок.
 *
 * z-index выше прилипшего меню (30), но полоса и меню на разных краях
 * экрана и не встречаются; число нужно против карточек товара с их
 * тенями, всплывающих при наведении.
 */
.cookie-note {
    position: fixed; left: 12px; right: 12px; bottom: 12px; z-index: 60;
    display: flex; align-items: center; gap: 18px; flex-wrap: wrap;
    max-width: 920px; margin: 0 auto;
    padding: 14px 18px;
    background: var(--navy); color: #fff;
    border-radius: 16px;
    box-shadow: 0 18px 40px -12px rgba(10, 30, 54, .45);
    animation: cookie-note-in .32s ease both;
}
.cookie-note__text { margin: 0; flex: 1 1 320px; font-size: 13px; line-height: 1.55; }
/* Ссылка внутри тёмной полосы: цвет витрины на тёмном не читается,
   поэтому здесь белый с подчёркиванием — подчёркивание и отличает её от
   остального текста, раз цветом отличить нельзя. */
.cookie-note__text a { color: #fff; text-decoration: underline; }
.cookie-note__actions { display: flex; gap: 8px; flex: none; }
/* Кнопка-призрак на тёмном фоне: рамка вместо заливки, иначе она сливается
   с полосой и перестаёт читаться как кнопка. */
.cookie-note .btn--ghost {
    border-color: rgba(255, 255, 255, .45); color: #fff; background: transparent;
}
.cookie-note .btn--ghost:hover { background: rgba(255, 255, 255, .12); border-color: #fff; }

@keyframes cookie-note-in {
    from { opacity: 0; transform: translateY(12px); }
    to   { opacity: 1; transform: translateY(0); }
}

/* --------------------------------------------------------- карточки */

.card {
    background: #fff; border: 1.5px solid var(--line);
    border-radius: 18px; padding: 26px;
    display: flex; flex-direction: column; gap: 16px;
}
.card--tight { padding: 20px; }
/*
 * Четыре плашки под героем.
 *
 * Собраны по вертикали и по центру. Раньше значок стоял слева, а текст
 * шёл строкой справа, и плашки не выравнивались ни по одной линии:
 * «Прямые поставки» умещается в строку, «Проверенное качество» переносится
 * на две, высоту в сетке всем задаёт самая длинная — и три из четырёх
 * подписей висели в верхней части своей рамки с пустотой под ними.
 * Вертикальная колонка с justify-content:center убирает это независимо от
 * длины текста, а её владелец правит из админки и заранее не знает.
 */
.tile {
    border: 1.5px solid var(--line); border-radius: 16px; padding: 24px 20px;
    display: flex; flex-direction: column; gap: 12px;
    align-items: center; justify-content: center; text-align: center;
}
.tile__title { font-weight: 800; font-size: 15px; }
.tile__text { font-size: 13px; color: var(--muted-2); line-height: 1.5; }
.tile svg { flex-shrink: 0; }

/* Кружок под значком. Оттенок — под цвет самого значка: значки заданы
   тремя разными stroke, и один общий серый фон делал бы синий конверт
   грязным, а зелёный лист — тусклым. */
.tile__icon {
    display: grid; place-items: center;
    width: 52px; height: 52px; border-radius: 50%; flex: none;
}
.tile__icon--orange { background: var(--orange-soft); }
.tile__icon--green  { background: var(--green-soft); }
.tile__icon--blue   { background: #e9f0f9; }

.soft-panel { background: var(--surface); border-radius: 18px; padding: 26px; }
.green-panel {
    background: var(--green-soft); border-radius: 16px; padding: 22px;
    display: flex; flex-direction: column; gap: 10px;
}
.green-panel__title { font-weight: 800; font-size: 15px; color: var(--green-ink); }
.green-panel__text { font-size: 13.5px; line-height: 1.6; color: var(--muted); }
/* Номер в плашке — ссылка tel:, и на телефоне по ней действительно
   звонят. Строка в 17px даёт цель высотой 22px — на два пикселя ниже
   минимума 24px из WCAG 2.5.8. Добираем высотой строки, а не отступом:
   плашка остаётся прежней. */
.green-panel__phone {
    font-weight: 800; font-size: 17px; color: var(--navy);
    display: inline-flex; align-items: center; min-height: 24px; align-self: flex-start;
}

.empty {
    border: 1.5px dashed var(--line-dash); border-radius: 20px;
    padding: 70px 40px; display: flex; flex-direction: column;
    align-items: center; gap: 16px; text-align: center;
}
.empty__title { font-weight: 800; font-size: 20px; }
.empty__text { font-size: 15px; color: var(--muted-2); max-width: 380px; }
.empty--sm { padding: 48px; font-size: 15px; color: var(--muted-3); border-radius: 16px; }

/* --------------------------------------------------- карточка товара */

.grid { display: grid; gap: 16px; }
.grid--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.grid--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.grid--4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }

/*
 * Сетка каталога считается от ширины карточки, а не от числа колонок.
 *
 * Число колонок здесь не главное: карточка каталога стоит в правом
 * столбце рядом с фильтрами шириной 260px, и «три в ряд» на ноутбуке и
 * «три в ряд» на планшете — это карточки в 294px и в 232px, то есть два
 * разных товара на вид. Раньше стояло repeat(3, 1fr) с одной подменой на
 * одну колонку в @media (max-width:560px), и между 560px и 860px
 * карточка успевала раздуться до половины экрана.
 *
 * auto-fill добирает столько колонок, сколько влезает при минимуме
 * --card, поэтому ширина карточки живёт в узком коридоре сама, без
 * лесенки медиазапросов: 4 колонки на ноутбуке и десктопе (916px под
 * товары при контейнере 1280px), 3 на планшете, 2 на телефоне.
 *
 * minmax(0, 1fr) обязателен: 1fr по умолчанию не даёт колонке сжаться
 * уже min-content, и длинное название товара без пробелов растянуло бы
 * ряд за край экрана — тот самый горизонтальный скролл на телефоне.
 */
.grid--products {
    --card: 210px;
    grid-template-columns: repeat(auto-fill, minmax(var(--card), 1fr));
}

/*
 * container-type нужен подвалу карточки: цена и кнопка встают в строку
 * или стопкой в зависимости от ширины самой карточки, а не окна. Ширина
 * тут не выводится из вьюпорта — на одном и том же экране карточка в
 * каталоге уже, чем на главной, потому что слева стоит колонка фильтров
 * (замерено на 1280px: 213px против 227px). Медиазапросом такое не
 * поймать, а @container видит ровно то, что нужно. Все карточки ряда
 * одной ширины, поэтому переключаются они одновременно — разнобоя, из-за
 * которого всё и затевалось, тут возникнуть не может.
 */
.product-card {
    background: #fff; border: 1.5px solid var(--line); border-radius: 16px;
    overflow: hidden; display: flex; flex-direction: column; height: 100%;
    transition: box-shadow .2s, transform .2s;
    position: relative;
    container-type: inline-size;
}
.product-card:hover { box-shadow: var(--shadow-card); transform: translateY(-3px); }
.product-card__media { aspect-ratio: 4 / 3; background: var(--surface-2); overflow: hidden; display: block; }
.product-card__media img { width: 100%; height: 100%; object-fit: cover; display: block; }
.product-card__body { padding: 16px 16px 18px; display: flex; flex-direction: column; gap: 6px; flex: 1; }
/*
 * Название занимает две строки всегда — не обрезается до двух, а
 * резервирует их. Обрезка прячет часть имени товара, а свободные две
 * строки лишь выравнивают цену у соседей по ряду: «Помидоры» и
 * «Помидоры черри на ветке, Азербайджан» начинают карточку одинаково.
 * Имя длиннее двух строк просто занимает три — карточки в ряду тянутся
 * по высоте вместе (height: 100%), и низ всё равно останется общим.
 */
.product-card__name {
    font-weight: 800; font-size: 15.5px; line-height: 1.3; color: var(--ink);
    min-height: 2.6em;
}
.product-card__note { font-size: 13px; color: var(--muted-2); }
/*
 * Наличие. Точка перед словами — единственное, что видно боковым
 * зрением при беглом просмотре ряда; сам текст читают уже у выбранной
 * карточки.
 */
/*
 * margin-top: auto стоит здесь, а не на подвале, и это осознанно: пустое
 * место карточки собирается в одном месте — над строкой наличия. Всё,
 * что ниже (наличие, цена, кнопка), едет вниз одним блоком и встаёт у
 * соседей по ряду на одну линию.
 *
 * Когда auto висел на подвале, выравнивались только цена и кнопка, а
 * наличие оставалось привязанным к названию и прыгало вслед за ним:
 * имя в три строки вместо двух — и точка «В наличии» уезжала ниже, чем
 * у соседа (замерено на 360px). Двух auto-отступов сразу быть не может:
 * свободное место делится между ними поровну, и между наличием и ценой
 * появляется дыра.
 */
.product-card__stock {
    margin-top: auto;
    display: flex; align-items: center; gap: 6px;
    font-size: 12.5px; font-weight: 700; color: var(--green-ink);
}
.product-card__stock::before {
    content: ''; width: 7px; height: 7px; border-radius: 50%;
    background: var(--green); flex: none;
}
.product-card__stock--order { color: var(--muted-3); }
.product-card__stock--order::before { background: var(--muted-4); }
/*
 * Цена и кнопка прижаты к низу карточки — где бы ни кончилось название,
 * покупать всегда жмут в одном и том же месте. Вниз их уводит не свой
 * отступ, а auto у строки наличия выше: см. .product-card__stock.
 */
.product-card__foot {
    padding-top: 12px; display: flex; align-items: center;
    justify-content: space-between; gap: 10px; flex-wrap: nowrap;
}
/* Кнопка не ужимается и не съезжает под цену: при переносе она встала бы
   на вторую строку только у тех карточек, где цена длиннее, и подвалы
   в ряду разъехались бы по высоте — ровно то, от чего уходим ниже.
   Длинную цену переносит внутри себя блок цены, а не подвал. */
.product-card__foot > form { flex: none; }
/*
 * Тесной карточке — стопка: цена сверху, кнопка во всю ширину под ней.
 *
 * В строку цена и кнопка помещаются только пока хватает места обеим, а
 * запас тут копеечный. Замерено на каталоге при 1280px: подвал 178.5px,
 * кнопка 98.9px, промежуток 10px — цене остаётся 69.6px. «49 ₽ 65 ₽»
 * укладывается в 68.6px, а «99 ₽ 120 ₽» требует 74px, переносится на
 * вторую строку и тянет подвал вверх: у соседей по ряду цена на 1025px,
 * у этой карточки — на 1007px. Четырёхзначная цена ломала бы строку и
 * подавно.
 *
 * 260px — это 99px кнопки, 10px промежутка, ~110px под самую длинную
 * пару «цена + старая цена» и 32px внутренних полей. Ниже этого порога
 * строка держится на честном слове, поэтому не держим её вовсе.
 *
 * Кнопка во всю ширину заодно перестаёт быть мелкой мишенью на телефоне
 * — то, чего требует проверка ширин от 320px.
 */
@container (max-width: 260px) {
    .product-card__foot { flex-direction: column; align-items: stretch; gap: 8px; }
    .product-card__foot > form { display: flex; }
    .product-card__foot .btn { flex: 1; }
    .product-card__foot .stepper { flex: 1; justify-content: space-between; }
    .product-card__foot .stepper__value { flex: 1; }
}
/*
 * Цена и старая цена — в одной строке, единица измерения — во второй.
 *
 * Раньше все три шли столбиком, и блок цены выходил то в две строки, то
 * в три: есть скидка — три, нет — две. В ряду сетки это видно сразу,
 * потому что подвал карточки прижат к низу: у товара без скидки и
 * число, и кнопка «В корзину» оказывались на 18px ниже, чем у соседей.
 * Ряд карточек читается как таблица, и колонка цен в нём шла
 * ступеньками (замерено на главной: 1872 / 1890 / 1872).
 *
 * Выхода два — держать пустую строку под зачёркнутую цену всегда или
 * убрать саму причину разнобоя. Выбран второй: строк теперь ровно две
 * при любых данных, пустого места под карточкой не остаётся, а «сейчас
 * столько, было столько» слева направо читается привычнее, чем сверху
 * вниз.
 *
 * Пара «цена + старая цена» обёрнута в .price__now, и обёртка здесь не
 * лишняя. Первым заходом строку задавали через `flex: 0 0 100%` у
 * единицы измерения — рисовалось верно, но ширина считалась неверно:
 * при вычислении max-content процентная основа неопределённа, ведёт
 * себя как auto, и браузер мерил блок так, будто все три части стоят в
 * одну строку. Блок выходил 101px вместо 69px, переставал помещаться
 * рядом с кнопкой в подвале шириной 193px — и кнопка уезжала на вторую
 * строку у карточек со скидкой. Обёртка убирает догадки: колонка из
 * двух строк меряется как максимум из них.
 *
 * baseline, а не center: зачёркнутое число мельче основного, и по
 * середине строки оно висело бы выше цифр, к которым относится.
 */
.price { display: flex; flex-direction: column; min-width: 0; }
.price__now { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 6px; }
.price__value { font-weight: 800; font-size: 17px; white-space: nowrap; }
.price__old { font-size: 13px; color: var(--muted-4); text-decoration: line-through; white-space: nowrap; }
.price__unit { font-size: 12px; color: var(--muted-4); white-space: nowrap; }

.flags { position: absolute; top: 12px; left: 12px; display: flex; flex-direction: column; gap: 6px; z-index: 1; }
.flags--inline { position: static; flex-direction: row; }
.flags--inline:empty { display: none; }
.flag {
    font-size: 11px; font-weight: 800; letter-spacing: .4px; text-transform: uppercase;
    padding: 5px 9px; border-radius: 8px; color: #fff; background: var(--orange);
}
.flag--new { background: var(--green); }
.flag--out { background: rgba(14, 44, 80, .78); }

/*
 * Сердечко избранного.
 *
 * Кнопка лежит поверх картинки в правом верхнем углу — по диагонали от
 * флагов «Хит/Новинка», чтобы они не наезжали друг на друга на узкой
 * карточке. Форма позиционируется, а не кнопка: без JS это обычный POST,
 * и у формы должен быть свой угол.
 *
 * Заливка сердца сделана классом на кнопке, а не :checked — состояние
 * приходит с сервера, и скрипт после ответа просто переключает is-on.
 */
.fav { position: absolute; top: 12px; right: 12px; z-index: 1; }
.fav__btn {
    width: 36px; height: 36px; border-radius: 50%; cursor: pointer;
    border: 1.5px solid var(--line); background: rgba(255, 255, 255, .92);
    display: flex; align-items: center; justify-content: center;
    color: var(--muted-3); font-size: 17px; line-height: 1; padding: 0;
    transition: color .15s, border-color .15s, transform .15s;
}
.fav__btn:hover { color: var(--red); border-color: var(--red); transform: scale(1.08); }
.fav__btn.is-on { color: var(--red); border-color: var(--red); background: var(--red-soft); }
.fav__btn:focus-visible { outline: 2px solid var(--orange); outline-offset: 2px; }

/* На странице товара «отложить» — обычная кнопка со словом. Отмеченное
   состояние красим тем же красным: одно действие, один цвет. */
.btn--ghost.is-on { border-color: var(--red); color: var(--red); background: var(--red-soft); }

/* счётчик количества */
.stepper {
    display: flex; align-items: center; gap: 2px;
    border: 1.5px solid var(--orange); border-radius: 10px; overflow: hidden;
}
.stepper button {
    border: none; background: #fff; color: var(--orange);
    font-size: 18px; font-weight: 800; width: 34px; height: 38px; cursor: pointer;
    transition: background-color .15s; line-height: 1;
}
.stepper button:hover { background: var(--orange-soft); }
.stepper__value { min-width: 34px; text-align: center; font-weight: 800; font-size: 15px; }
.stepper--muted { border-color: var(--line); }
.stepper--muted button { height: 36px; width: 32px; font-size: 17px; }

/* ------------------------------------------------------------ герой */

.hero {
    display: grid; grid-template-columns: 1fr 1.05fr; gap: 56px; align-items: center;
    padding-top: 56px;
}
.hero__text { display: flex; flex-direction: column; gap: 24px; align-items: flex-start; }
.hero h1 { font-size: 46px; line-height: 1.22; }
.hero__lead { margin: 0; font-size: 17px; line-height: 1.7; color: var(--muted); max-width: 460px; }
.hero__actions { display: flex; gap: 14px; flex-wrap: wrap; }
.hero__stats { display: flex; gap: 36px; margin-top: 8px; flex-wrap: wrap; }
.hero__stat { display: flex; flex-direction: column; }
.hero__stat b { font-family: var(--font-display); font-size: 26px; font-weight: 400; color: var(--navy); }
.hero__stat span { font-size: 13px; color: var(--muted-3); }
.hero__media { position: relative; }

/*
 * Выход первого экрана.
 *
 * Здесь, в отличие от .is-revealing, никакого скрипта нет — и это не
 * лень, а единственный способ. app.js подключён с defer и выполняется
 * после первой отрисовки: заголовок успевает нарисоваться, и выданный
 * скриптом класс заставляет его моргнуть в прозрачность и вернуться.
 * Именно поэтому revealOnScroll() блокам первого экрана класс не даёт
 * вовсе. Стили же приходят до первой отрисовки, так что моргать нечему.
 *
 * Обычное возражение «а если стили не догрузятся, экран останется
 * пустым» здесь не работает: без app.css не будет и правила, которое
 * прячет, — страница просто останется неоформленной. Опасен только
 * вариант «прячем в CSS, показываем скриптом», а его тут нет.
 *
 * Всё завёрнуто в no-preference, а не отдано общему правилу
 * prefers-reduced-motion выше. То правило ужимает animation-duration, но
 * не трогает animation-delay, и при просьбе «поменьше движения»
 * получилось бы худшее из двух: анимации нет, а задержка осталась — то
 * есть заголовок просто невидим первые треть секунды. Проще не начинать.
 *
 * backwards по той же причине, что и у .is-revealing: во время задержки
 * должен действовать кадр from, а держать конечный кадр не нужно —
 * он совпадает с обычным состоянием. Про то, чем оборачивается forwards,
 * подробно написано там же.
 *
 * Список делаем по nth-child, а не по классам: в разметке почти всё
 * необязательно — надстрочник, подзаголовок, кнопки и цифры выводятся
 * по условию, и порядок «кто первый на месте» надёжнее имён.
 */
@keyframes hero-in {
    from { opacity: 0; transform: translateY(14px); }
    to   { opacity: 1; transform: none; }
}

@keyframes hero-media-in {
    from { opacity: 0; transform: scale(.97); }
    to   { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: no-preference) {
    .hero__text > * { animation: hero-in .7s cubic-bezier(.22, .61, .36, 1) backwards; }

    .hero__text > :nth-child(1)   { animation-delay: .05s; }
    .hero__text > :nth-child(2)   { animation-delay: .12s; }
    .hero__text > :nth-child(3)   { animation-delay: .19s; }
    .hero__text > :nth-child(4)   { animation-delay: .26s; }
    /* Дальше не растёт: добавят пятый блок — он приедет вместе с четвёртым,
       а не заставит ждать ещё треть секунды. */
    .hero__text > :nth-child(n+5) { animation-delay: .33s; }

    .hero__media { animation: hero-media-in .8s cubic-bezier(.22, .61, .36, 1) .1s backwards; }
}

/*
 * Слайдер первого экрана.
 *
 * Высоту держит контейнер, а не картинка: слайды лежат друг на друге через
 * position:absolute, и в потоке их нет вовсе. Пока высоту задавала первая
 * фотография, при переключении на снимок другой пропорции первый экран
 * дёргался — соседние блоки уезжали вниз в момент перелистывания. Жёсткое
 * 4:3 на контейнере убирает это: кадр обрезается, вёрстка стоит.
 *
 * Без JS видно только .is-active, проставленный на сервере. То есть при
 * выключенных скриптах остаётся ровно то, что было до слайдера, — одна
 * фотография, а не стопка из четырёх.
 */
.hero-slider {
    position: relative; aspect-ratio: 4 / 3;
    border-radius: 24px; overflow: hidden;
    background: var(--surface-2);
}
.hero-slider__slide {
    position: absolute; inset: 0; width: 100%; height: 100%;
    object-fit: cover; display: block;
    opacity: 0; transition: opacity .7s ease;
}
.hero-slider__slide.is-active { opacity: 1; }

/*
 * Панель управления. Прижата к правому нижнему углу — плашка «Привоз
 * сегодня утром» стоит в левом, и на 900 px они не встречаются.
 *
 * Плотность подложки посчитана по худшему случаю, а не подобрана на глаз.
 * Фотографии сюда грузит владелец, и однажды это будет светлый кадр —
 * ягоды на белом фоне. Замер на белом:
 *
 *     подложка .5   → точка даёт 2.22:1   не проходит
 *     подложка .68  → 3.00:1              ровно по границе
 *     подложка .72  → 3.60:1              с запасом
 *
 * Точки — органы управления, для них норма 3:1 (WCAG 1.4.11). На тёмном
 * снимке запас только вырастет, так что считать нужно именно по светлому.
 */
.hero-slider__nav {
    position: absolute; right: 18px; bottom: 20px;
    display: flex; align-items: center; gap: 4px;
    padding: 4px 8px 4px 4px; border-radius: 999px;
    background: rgba(10, 30, 54, .72); backdrop-filter: blur(8px);
}
.hero-slider__dots { display: flex; align-items: center; gap: 2px; }

/*
 * Кнопки — 24×24 при девятипиксельной точке.
 *
 * Размер цели, а не размер рисунка: WCAG 2.5.8 просит от 24 px, и точка
 * в девять пикселей его не набирает. Видимую часть рисует ::before, сама
 * кнопка остаётся прозрачной — попасть пальцем можно рядом с точкой.
 */
.hero-slider__dot, .hero-slider__toggle {
    width: 24px; height: 24px; padding: 0; border: 0;
    background: none; color: #fff; cursor: pointer;
    display: grid; place-items: center; border-radius: 999px;
}
.hero-slider__dot::before {
    content: ''; display: block; width: 9px; height: 9px; border-radius: 999px;
    background: rgba(255, 255, 255, .6);
    transition: width .25s ease, background-color .25s ease;
}
.hero-slider__dot[aria-current="true"]::before { width: 20px; background: #fff; }
.hero-slider__dot:hover::before { background: rgba(255, 255, 255, .85); }
.hero-slider__toggle { opacity: .8; }
.hero-slider__toggle:hover { opacity: 1; background: rgba(255, 255, 255, .16); }
.hero-slider__dot:focus-visible, .hero-slider__toggle:focus-visible {
    outline: 2px solid #fff; outline-offset: 1px;
}

.hero__badge {
    position: absolute; left: 24px; bottom: 24px;
    background: rgba(255, 255, 255, .95); backdrop-filter: blur(6px);
    border-radius: 14px; padding: 14px 20px; display: flex; align-items: center;
    gap: 12px; box-shadow: var(--shadow-pop);
}
.hero__badge b { font-weight: 800; font-size: 14px; }
.hero__badge span { font-size: 12.5px; color: var(--muted-2); }

.cat-tile {
    position: relative; border-radius: 18px; overflow: hidden;
    display: block; aspect-ratio: 16 / 9; background: var(--surface-2);
}
.cat-tile img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform .3s; }
/* Обложка раздела приходит из category_picture(), а он оборачивает
   растровую картинку в <picture> ради WebP. Обёртка по умолчанию
   строчная и высоты не имеет — height:100% у картинки внутри неё
   считался бы от нуля, и плитка складывалась бы в полоску. Сейчас все
   обложки SVG, обёртки нет и поломки не видно; она появится в тот день,
   когда владелец загрузит первую фотографию раздела в JPEG. */
.cat-tile picture { display: block; width: 100%; height: 100%; }
.cat-tile:hover img { transform: scale(1.04); }
/* Раздел без обложки: крупная буква вместо серой заглушки — по той же
   причине, что и на плитке каталога (см. .tiles__blank). */
.cat-tile__blank {
    display: flex; align-items: center; justify-content: center;
    width: 100%; height: 100%; background: var(--surface-2);
    color: var(--muted-4); font-family: var(--font-display); font-size: 56px;
}
/* Вуаль под подписью. Прежняя (прозрачная до 40 %, .72 у самого низа)
   давала под названием раздела альфу 0.43 — и белый текст на реальной
   плитке набирал контраст 3.6:1 при норме 4.5:1 для 18px. Виноват не
   расчёт, а картинки: нижняя полоса всех обложек каталога — почти белая
   (в среднем 239,246,234), так что вуаль осветлялась ею до сине-серого.
   Здесь она набирает силу раньше: под названием альфа 0.70, под
   счётчиком 0.786. Это 5.5:1 и 7.2:1 в худшем случае — если раздел
   закроют фотографией на чисто белом фоне. */
.cat-tile__veil {
    position: absolute; inset: 0;
    background: linear-gradient(180deg,
        rgba(14, 44, 80, 0) 34%,
        rgba(14, 44, 80, .58) 68%,
        rgba(14, 44, 80, .88) 100%);
}
.cat-tile__label {
    position: absolute; left: 18px; right: 18px; bottom: 14px;
    display: flex; flex-direction: column; gap: 1px; color: #fff;
}
.cat-tile__name { font-weight: 800; font-size: 18px; }
/* Обе строки чисто белые. Приглушить счётчик прозрачностью — первое, что
   просится, и именно оно роняет контраст ниже нормы: rgba(255,255,255,.82)
   на этой вуали даёт 2.97:1. Разница между строками и так есть — кегль и
   насыщенность, — а её видно и тем, кто читает подпись при солнце на
   телефоне. */
.cat-tile__count { font-size: 12.5px; font-weight: 600; }

/*
 * Оптовый баннер на главной. Класс называется .promo-banner, а не .promo,
 * и это не вкусовщина.
 *
 * Раньше здесь стояло `.promo`, а тремястами строками ниже — второй
 * top-level `.promo` для формы промокода в корзине. Одинаковая
 * специфичность, один файл: побеждал тот, что ниже. Ломались оба блока
 * сразу, и по-разному, поэтому связь между ними никто не видел.
 *
 * Баннеру доставались `display:flex; flex-direction:column` от корзины —
 * его `grid-template-columns: 1.2fr 1fr` не работал вовсе, плюс сверху
 * приезжала чужая `border-top` и `padding-top:16px`. Форме промокода
 * доставались `background: var(--navy)` и `border-radius:24px` от
 * баннера, а `.promo__label` своего цвета не имеет и наследует тёмный
 * `--ink` — тёмный текст на тёмно-синем. Ровно это владелец описал как
 * «Промокод / Код с чека или из рассылки / Применить — не читается».
 *
 * Развели по именам, а не порядком правил: правило «этот блок должен
 * лежать выше того» живёт только в голове и переживает ровно одну
 * перестановку секций.
 */
.promo-banner {
    position: relative; overflow: hidden;
    border-radius: 24px;
    /*
     * Фотография отсюда убрана намеренно. Баннер зовёт звонить насчёт
     * оптовой цены — снимок ящика к этому ничего не добавлял, зато тянул
     * лишний запрос и на узкой колонке отжимал текст вниз на экран.
     * Фон рисуем градиентами: он не грузится, не растягивается и не
     * устаревает вместе с ассортиментом.
     */
    background:
        radial-gradient(120% 140% at 88% 8%,  rgba(239, 127, 26, .38) 0%, rgba(239, 127, 26, 0) 58%),
        radial-gradient(90%  120% at 4%  96%, rgba(95, 158, 70, .30)  0%, rgba(95, 158, 70, 0)  62%),
        linear-gradient(135deg, #123763 0%, var(--navy) 46%, #0a2140 100%);
}
/*
 * Диагональная штриховка поверх градиента — чтобы плоскость не выглядела
 * заливкой. Отдельным слоем, а не четвёртым фоном: на радиальные пятна
 * она должна ложиться с меньшей непрозрачностью, чем на края.
 */
.promo-banner::before {
    content: ''; position: absolute; inset: 0; pointer-events: none;
    background: repeating-linear-gradient(
        -45deg,
        rgba(255, 255, 255, .045) 0 2px,
        rgba(255, 255, 255, 0)    2px 14px
    );
}
/* Кольцо в правом нижнем углу. Уводим за край, чтобы оно читалось
   фрагментом фона, а не «кто-то забыл круг посреди баннера». */
.promo-banner::after {
    content: ''; position: absolute; pointer-events: none;
    right: -90px; bottom: -130px; width: 340px; height: 340px;
    border: 44px solid rgba(255, 255, 255, .05); border-radius: 50%;
}
.promo-banner__body {
    position: relative; /* поверх ::before и ::after */
    padding: 56px 60px;
    display: flex; flex-direction: column; gap: 18px; align-items: flex-start;
    /* Текст не тянется на всю ширину баннера: строка в 1100px нечитаема,
       глаз теряет начало следующей. Правый край остаётся под фон. */
    max-width: 660px;
}
.promo-banner__tag {
    background: rgba(239, 127, 26, .18); color: #ffb066; font-weight: 800; font-size: 13px;
    letter-spacing: 1.5px; text-transform: uppercase; padding: 7px 14px; border-radius: 8px;
}
/* text-wrap: balance — на 1440 заголовок ломался как «Берёте ящиками —
   считаем» / «отдельно»: одно слово во второй строке при почти полной первой.
   Балансировка делит его пополам. Руками (&nbsp; или <br>) тут не поправить:
   заголовок редактируется из админки, владелец введёт свой текст — и подгонка
   развалится. Браузер без поддержки просто перенесёт как раньше. */
.promo-banner h2 { margin: 0; font-size: 34px; line-height: 1.25; color: #fff; text-wrap: balance; }
/* Абзац был на --on-navy-dim, пока фон был ровным #0e2c50: 6.6:1, с запасом.
   Зелёное пятно в левом нижнем углу подсветило фон до ~#264e4d, и на нём тот
   же цвет даёт 4.32:1 — ниже AA ровно там, где лежит текст. --on-navy держит
   6.4:1 в самом светлом месте градиента. */
.promo-banner p { margin: 0; font-size: 16px; line-height: 1.65; color: var(--on-navy); }

.split { display: grid; grid-template-columns: 1fr 1fr; gap: 56px; align-items: center; }
.split img { width: 100%; border-radius: 24px; display: block; object-fit: cover; aspect-ratio: 4 / 3; }
.split__text { display: flex; flex-direction: column; gap: 18px; align-items: flex-start; }
.split__text p { margin: 0; font-size: 16px; line-height: 1.7; color: var(--muted); }
.checklist { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: 10px; font-size: 15px; }
.checklist li { display: flex; gap: 10px; align-items: center; }

.faq { display: flex; flex-direction: column; gap: 12px; }

/*
 * Отступ рамки отдан summary, а не details.
 *
 * Раскрывает вопрос только сам summary — поля вокруг него принадлежат
 * details и на касание не отвечают. Цель получалась 289×20: широкая, но
 * в двадцать пикселей высотой, то есть ниже минимума 24px из WCAG 2.5.8.
 * Палец попадал по ней через раз, а промах здесь выглядит как «сайт не
 * работает»: ответа не видно, и понять почему нельзя.
 *
 * Те же 18px, перенесённые на summary, делают целью всю строку
 * заголовка — 289×56. Закрытая карточка при этом выглядит ровно как
 * прежде: 18 + 20 + 18 = 56 и там, и там.
 *
 * Раскрытая отличается на шесть пикселей: зазор между вопросом и
 * ответом был 12px, стал 18px — это нижний отступ summary, и снять его
 * нельзя, не уменьшив цель обратно. Возвращать 12px отрицательным
 * полем у абзаца не стали: правило, которое существует ради шести
 * пикселей, дороже самих шести пикселей.
 */
.faq details { border: 1.5px solid var(--line); border-radius: 14px; padding: 0 22px; }
.faq summary { font-weight: 800; font-size: 16px; cursor: pointer; padding: 18px 0; }
.faq p { margin: 0 0 18px; font-size: 15px; line-height: 1.65; color: var(--muted); }

/* ---------------------------------------------------------- каталог */

/*
 * Раскладка каталога: слева колонка подбора, справа товары и под ними
 * плашка помощи.
 *
 * Здесь была починена «плашка WhatsApp наезжает на фильтры», и лечить
 * её z-index'ом было нельзя — вот почему.
 *
 * Раньше стояло `.catalog__help { grid-column: 1; grid-row: 2 }` и
 * `.catalog__main { grid-column: 2; grid-row: 1 / span 2 }`: помощь
 * уходила под категории, товары занимали оба ряда. Обе строки при этом
 * auto, и лишнюю высоту товаров сетка делит между ними поровну. На
 * замере при 1280px и 40 карточках строки вышли 3237px и 2551px, и
 * плашка помощи оказалась на 3581-м пикселе страницы высотой 6583 —
 * посреди пустого левого столбца, в двух с половиной тысячах пикселей
 * от категорий, к которым она прижималась в макете.
 *
 * Наезжала она не поэтому, а из-за второй половины: `.sidebar` —
 * `position: sticky`, а sticky молча создаёт контекст наложения. Липкая
 * колонка ехала вниз вместе с прокруткой, доезжала до зависшей плашки и
 * закрывала её собой, рисуясь поверх. Отсюда и соблазн: поднять
 * z-index плашке — и «починилось». Только плашка от этого не перестаёт
 * висеть посреди пустоты, а поднятая над липкой колонкой — начинает
 * закрывать уже фильтры.
 *
 * Настоящая причина — координаты, расставленные руками по строкам,
 * высоту которых никто не контролирует. Поэтому левый столбец теперь
 * занимает ровно одна вещь — колонка подбора, — и наехать в нём не на
 * что: чтобы два блока перекрылись, они должны сначала оказаться в
 * одной ячейке.
 */
.catalog {
    display: grid;
    grid-template-columns: 260px minmax(0, 1fr);
    gap: 28px 40px;
    align-items: start;
    padding-top: 40px;
}
.catalog__main { grid-column: 2; grid-row: 1; }
.catalog__help { grid-column: 2; grid-row: 2; }
/*
 * Колонка подбора растянута на обе строки — не ради содержимого, а ради
 * прокрутки: у sticky-элемента ход ограничен его же ячейкой сетки, и в
 * ячейке высотой ровно по себе он никуда не поедет. Строка 1/-1 отдаёт
 * ему высоту всей страницы каталога, align-items: start оставляет саму
 * колонку прежней высоты.
 *
 * max-height — вторая половина той же истории. Без него длинный список
 * характеристик просто не помещался в экран: замер показал низ колонки
 * на 932-м пикселе при высоте окна 900, то есть «Сбросить» и «Показать»
 * оказывались под сгибом навсегда — прокрутка страницы их не поднимала,
 * потому что колонка прокручивалась вместе с ней. Теперь лишнее
 * прокручивается внутри самой колонки.
 */
.sidebar {
    grid-column: 1; grid-row: 1 / -1;
    position: sticky; top: 24px;
    display: flex; flex-direction: column; gap: 24px;
    max-height: calc(100vh - 48px);
    overflow-y: auto;
    overscroll-behavior: contain;
    scrollbar-gutter: stable;
}
.sidebar__box { border: 1.5px solid var(--line); border-radius: 16px; padding: 10px; display: flex; flex-direction: column; }
.sidebar__title {
    font-weight: 800; font-size: 13px; letter-spacing: 1.5px; text-transform: uppercase;
    color: var(--muted-3); padding: 12px 14px 8px;
}
.sidebar__link {
    padding: 11px 14px; border-radius: 10px; font-weight: 700; font-size: 14.5px;
    display: flex; justify-content: space-between; gap: 10px; align-items: baseline;
}
.sidebar__link:hover { background: var(--surface); }
.sidebar__link.is-active { background: var(--orange-soft); color: var(--orange-dark); }
.sidebar__link small { font-weight: 700; font-size: 12.5px; color: var(--muted-4); }
.sidebar__link--child { padding-left: 28px; font-weight: 600; font-size: 14px; }

.catalog__main { display: flex; flex-direction: column; gap: 14px; min-width: 0; }
.catalog__head { display: flex; align-items: baseline; justify-content: space-between; gap: 20px; flex-wrap: wrap; }
.catalog__count { font-size: 14px; color: var(--muted-3); }

/* Текст раздела под сеткой товаров. Отбивка сверху и линия — чтобы он
   читался как подпись к разделу, а не как ещё одна карточка, потерявшая
   картинку. Ширину ограничиваем: строка в 900 px при 15 px шрифта — это
   130 знаков, и глаз на такой длине теряет начало следующей строки. */
.catalog__text {
    margin-top: 18px; padding-top: 22px; border-top: 1px solid var(--line);
    max-width: 760px; font-size: 15px; color: var(--muted-2);
}
.catalog__text > *:last-child { margin-bottom: 0; }
.toolbar { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
.toolbar select {
    border: 1.5px solid var(--line); border-radius: 12px; padding: 12px 14px;
    font-size: 14px; font-weight: 700; color: var(--ink); background: #fff; outline: none; cursor: pointer;
}
/* Кнопка, открывающая подбор. На широком экране панель и так стоит слева
   перед глазами, поэтому кнопка нужна только на телефоне, где панель
   свёрнута в шторку. */
.toolbar__filters { display: none; }
/* С выбранными фильтрами кнопка перестаёт быть тихой: рядом с ней стоит
   число, и оно должно бросаться в глаза — это единственный на телефоне
   ответ на вопрос «почему так мало товаров». */
.toolbar__filters.is-on {
    border-color: var(--orange); background: var(--orange-soft); color: var(--orange-dark);
}

/* --------------------------------------------------- подбор: чипсы */

.chips { display: flex; flex-wrap: wrap; gap: 8px; }
.chip {
    display: inline-flex; align-items: center; gap: 8px;
    border: 1.5px solid var(--line); border-radius: 999px;
    padding: 8px 14px; font-size: 13.5px; font-weight: 700;
    color: var(--ink); background: var(--surface);
    transition: border-color .15s, background-color .15s, color .15s;
}
.chip:hover { border-color: var(--orange); background: var(--orange-soft); color: var(--orange-dark); }
/* Крестик — часть ссылки, поэтому собственных состояний у него нет:
   он лишь показывает, что нажатие снимает фильтр, а не ставит его. */
.chip__x { font-size: 16px; line-height: 1; color: var(--muted-3); }
.chip:hover .chip__x { color: var(--orange-dark); }
.chip--reset { background: none; border-style: dashed; color: var(--muted); }

/* --------------------------------------------------- подбор: форма */

.filters { display: flex; flex-direction: column; gap: 4px; }
.filters__head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
.filters__head .sidebar__title { padding-right: 0; }
.linkish--sm { font-size: 12.5px; padding-right: 14px; }

/* Группы разделяем линией, а не отступом: без неё галочки характеристик
   сливаются в один длинный список, в котором не видно, где кончается
   «Сорт» и начинается «Страна». */
.filters__group {
    display: flex; flex-direction: column; gap: 2px;
    padding: 12px 4px; border-top: 1px solid var(--line-soft);
}
.filters__group:first-of-type { border-top: none; padding-top: 4px; }
.filters__legend {
    font-weight: 800; font-size: 13px; color: var(--muted);
    padding: 0 10px 6px;
}
.filters__check {
    display: flex; align-items: center; gap: 10px;
    padding: 8px 10px; border-radius: 9px; cursor: pointer;
    font-size: 14px; font-weight: 600; color: var(--ink);
}
.filters__check:hover { background: var(--surface); }
.filters__check input { accent-color: var(--orange); width: 16px; height: 16px; flex: none; margin: 0; }
.filters__check span { flex: 1; min-width: 0; }
.filters__check small { font-weight: 700; font-size: 12px; color: var(--muted-4); }
/* Переключатель без товаров остаётся видимым, но гаснет: покупатель
   видит, что фильтр существует — просто сегодня под него нет привоза. */
.filters__check.is-empty { color: var(--muted-4); cursor: default; }
.filters__check.is-empty:hover { background: none; }

.filters__range { display: flex; align-items: center; gap: 8px; padding: 0 10px; }
.filters__dash { color: var(--muted-4); flex: none; }
/* Поле цены не тянется по всей колонке: два поля по 6 символов и тире
   между ними читаются как диапазон, а растянутые — как две анкеты. */
.input--num { padding: 10px 12px; font-size: 14px; text-align: center; min-width: 0; }
/* Стрелки-крутилки в 260px колонке отъедают половину поля, а вводить
   ими цену никто не станет. */
.input--num::-webkit-outer-spin-button,
.input--num::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.input--num { -moz-appearance: textfield; appearance: textfield; }

.filters__apply { margin-top: 10px; }
/* Скрипт вешает на форму is-live — и кнопка «Показать» уходит: фильтры
   применяются сами при любом изменении. Пока app.js не загрузился,
   класса нет и кнопка на месте, поэтому подбор работает всегда.
   На телефоне кнопка возвращается — см. медиазапрос. */
.filters.is-live .filters__apply { display: none; }
.filters__note { font-size: 12.5px; line-height: 1.55; color: var(--muted-3); padding: 12px 10px 4px; }

/* Крестик закрывает раскрытую панель на телефоне. На широком экране
   закрывать нечего — панель стоит в колонке постоянно. */
.filters__close { display: none; }
/* Затемнение позади шторки — тоже только на телефоне, см. медиазапрос. */
.filters__backdrop { display: none; }

/* Выезд шторки подбора. Не украшение: без движения панель возникает из
   ниоткуда и читается как перерисовка страницы, а не как «поверх списка
   открылось окно». Системную просьбу «поменьше движения» уважает общее
   правило в начале файла. */
@keyframes sheet-up {
    from { transform: translateY(100%); }
    to   { transform: none; }
}

/* ------------------------------------------------ плитка разделов */

.tiles__title { font-size: 20px; margin: 10px 0 0; }
.tiles { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }
.tiles__card {
    border: 1.5px solid var(--line); border-radius: 16px; overflow: hidden;
    display: flex; flex-direction: column; background: #fff;
    transition: border-color .15s, box-shadow .15s, transform .15s;
}
.tiles__card:hover { border-color: var(--orange); box-shadow: var(--shadow-card); transform: translateY(-2px); }
.tiles__card img { width: 100%; aspect-ratio: 16 / 10; object-fit: cover; display: block; background: var(--surface-2); }
/* Раздел без картинки не должен ломать ряд: заглушка занимает ровно
   ту же высоту, что и фотография соседа. */
.tiles__blank {
    aspect-ratio: 16 / 10; background: var(--surface-2); color: var(--muted-4);
    display: flex; align-items: center; justify-content: center;
    font-family: var(--font-display); font-size: 44px;
}
.tiles__body { display: flex; flex-direction: column; gap: 3px; padding: 14px 16px; }
.tiles__body b { font-size: 15px; font-weight: 800; }
.tiles__body small { font-size: 12.5px; color: var(--muted-3); }

.breadcrumbs { display: flex; gap: 8px; flex-wrap: wrap; font-size: 13.5px; color: var(--muted-3); padding-top: 24px; }
/* Хлебные крошки — 13.5px, то есть цель высотой 17px. Формально их
   спасает исключение по расстоянию: соседние крошки разнесены на 8px и
   круги в 24px не пересекаются. Но «Главная» — обычный путь назад с
   карточки товара, и мимо 17px промахиваются. */
.breadcrumbs a { color: var(--muted-3); display: inline-flex; align-items: center; min-height: 24px; }
.breadcrumbs a:hover { color: var(--orange); }

.pagination { display: flex; gap: 8px; justify-content: center; padding: 36px 0 0; flex-wrap: wrap; }
.pagination a, .pagination span {
    min-width: 42px; height: 42px; padding: 0 12px; border-radius: 10px;
    border: 1.5px solid var(--line); display: flex; align-items: center; justify-content: center;
    font-weight: 700; font-size: 14.5px;
}
.pagination a:hover { border-color: var(--orange); color: var(--orange); }
.pagination .is-current { background: var(--navy); border-color: var(--navy); color: #fff; }
.pagination .is-gap { border: none; }

/* ------------------------------------------------------------ товар */

.product { display: grid; grid-template-columns: 1.05fr 1fr; gap: 48px; align-items: start; padding-top: 12px; }

/* Галерея переключается радиокнопками, без скрипта: миниатюра — это
   <label>, за ней стоит скрытое радио, а показом кадра управляет
   :checked. Отсюда и порядок в разметке — радио, кадр, лента: селектор
   ~ ходит только вперёд по соседям. Плата за это — десять пар правил
   ниже; взамен галерея работает с выключенным JS, до загрузки app.js
   и с клавиатуры (стрелки внутри группы радио — поведение браузера). */
.gallery { display: flex; flex-direction: column; gap: 12px; }
.gallery__frame {
    border: 1.5px solid var(--line); border-radius: 20px; overflow: hidden;
    background: var(--surface-2); aspect-ratio: 4 / 3;
}
/* Кадр — обёртка, а не сама картинка: при готовых копиях она приезжает
   внутри <picture>, и тогда картинки перестают быть соседями. Правила
   ниже считают :nth-of-type, и считать они обязаны кадры, а не то, во
   что их завернул ImageResolver. */
.gallery__slide { width: 100%; height: 100%; display: block; }
.gallery__slide img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* Второй и дальше кадры прячем, а не первый показываем: у товара с
   единственным фото радиокнопок нет вовсе, и правило :checked ~ ему бы
   не помогло — карточка осталась бы без картинки. */
.gallery__slide:not(:first-child) { display: none; }

.gallery__thumbs { display: flex; gap: 10px; flex-wrap: wrap; }
.gallery__thumb {
    width: 84px; height: 64px; border-radius: 10px; overflow: hidden;
    border: 1.5px solid var(--line); background: var(--surface-2);
    cursor: pointer; padding: 0; display: block;
    transition: border-color .15s;
}
.gallery__thumb:hover { border-color: var(--muted-4); }
.gallery__thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* Радио скрыто визуально, но остаётся в потоке фокуса — иначе с
   клавиатуры до галереи было бы не добраться. Рамку рисуем вокруг
   ленты: сам элемент размером в пиксель, обводка на нём невидима. */
.gallery__pick:focus-visible ~ .gallery__thumbs {
    outline: 2px solid var(--orange); outline-offset: 6px; border-radius: 12px;
}

.gallery__pick:nth-of-type(1):checked ~ .gallery__frame .gallery__slide:nth-of-type(1),
.gallery__pick:nth-of-type(2):checked ~ .gallery__frame .gallery__slide:nth-of-type(2),
.gallery__pick:nth-of-type(3):checked ~ .gallery__frame .gallery__slide:nth-of-type(3),
.gallery__pick:nth-of-type(4):checked ~ .gallery__frame .gallery__slide:nth-of-type(4),
.gallery__pick:nth-of-type(5):checked ~ .gallery__frame .gallery__slide:nth-of-type(5),
.gallery__pick:nth-of-type(6):checked ~ .gallery__frame .gallery__slide:nth-of-type(6),
.gallery__pick:nth-of-type(7):checked ~ .gallery__frame .gallery__slide:nth-of-type(7),
.gallery__pick:nth-of-type(8):checked ~ .gallery__frame .gallery__slide:nth-of-type(8) { display: block; }

/* Кадр показали — предыдущий надо убрать. Одним правилом это не
   выражается: :checked знает только про свой номер. */
.gallery__pick:nth-of-type(n + 2):checked ~ .gallery__frame .gallery__slide:first-child { display: none; }

.gallery__pick:nth-of-type(1):checked ~ .gallery__thumbs .gallery__thumb:nth-of-type(1),
.gallery__pick:nth-of-type(2):checked ~ .gallery__thumbs .gallery__thumb:nth-of-type(2),
.gallery__pick:nth-of-type(3):checked ~ .gallery__thumbs .gallery__thumb:nth-of-type(3),
.gallery__pick:nth-of-type(4):checked ~ .gallery__thumbs .gallery__thumb:nth-of-type(4),
.gallery__pick:nth-of-type(5):checked ~ .gallery__thumbs .gallery__thumb:nth-of-type(5),
.gallery__pick:nth-of-type(6):checked ~ .gallery__thumbs .gallery__thumb:nth-of-type(6),
.gallery__pick:nth-of-type(7):checked ~ .gallery__thumbs .gallery__thumb:nth-of-type(7),
.gallery__pick:nth-of-type(8):checked ~ .gallery__thumbs .gallery__thumb:nth-of-type(8) { border-color: var(--orange); }

.product__info { display: flex; flex-direction: column; gap: 18px; }
.product__title { font-size: 34px; line-height: 1.25; }
.product__subtitle { font-size: 18px; margin: 6px 0 -6px; }
.product__meta { display: flex; gap: 16px; flex-wrap: wrap; font-size: 13.5px; color: var(--muted-3); }
.product__price { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; }
.product__price b { font-family: var(--font-display); font-weight: 400; font-size: 34px; color: var(--navy); }
.product__buy { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
.product__desc { font-size: 15px; line-height: 1.7; color: var(--muted); white-space: pre-line; }
.product__specs { display: grid; grid-template-columns: 170px 1fr; gap: 10px 20px; font-size: 14.5px; }
.product__specs dt { color: var(--muted-3); font-weight: 700; margin: 0; }
.product__specs dd { margin: 0; font-weight: 700; }
/* Значение-ссылка ведёт в каталог с этой же характеристикой. Подчёркивание
   пунктиром, а не сплошное: это подсказка «так можно», а не призыв уйти
   со страницы товара. */
.product__specs a { color: var(--ink); border-bottom: 1px dashed var(--muted-4); }
.product__specs a:hover { color: var(--orange-dark); border-bottom-color: var(--orange); }

.product__terms {
    display: flex; flex-direction: column; gap: 4px;
    background: var(--surface); border-radius: 14px; padding: 14px 16px;
    font-size: 14px; line-height: 1.6; color: var(--muted);
}
.product__terms b { color: var(--ink); font-size: 14.5px; }

.stock { display: inline-flex; align-items: center; gap: 8px; font-size: 14px; font-weight: 700; }
.stock--in { color: var(--green-ink); }
.stock--low { color: var(--orange-dark); }
.stock--out { color: var(--muted-3); }
.stock::before { content: ''; width: 9px; height: 9px; border-radius: 50%; background: currentColor; }

/* --------------------------------------------------------- корзина */

.cart { display: grid; grid-template-columns: 1fr 380px; gap: 32px; align-items: start; }
.cart__lines { display: flex; flex-direction: column; border: 1.5px solid var(--line); border-radius: 18px; overflow: hidden; }
.cart__line {
    display: grid; grid-template-columns: 88px 1fr auto auto auto;
    gap: 18px; align-items: center; padding: 16px 20px; border-bottom: 1px solid var(--line-soft);
}
.cart__line:last-child { border-bottom: none; }
.cart__line img { width: 88px; height: 66px; object-fit: cover; border-radius: 10px; display: block; background: var(--surface-2); }
.cart__name { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.cart__name b { font-weight: 800; font-size: 15.5px; }
.cart__name span { font-size: 13px; color: var(--muted-2); }
.cart__sum { font-weight: 800; font-size: 16px; min-width: 90px; text-align: right; }
.cart__remove { border: none; background: none; cursor: pointer; color: var(--muted-5); padding: 6px; display: flex; }
.cart__remove:hover { color: var(--red); }

.summary {
    border: 1.5px solid var(--line); border-radius: 18px; padding: 26px;
    display: flex; flex-direction: column; gap: 16px; position: sticky; top: 24px;
}
.summary__title { font-weight: 800; font-size: 18px; }
.summary__row { display: flex; justify-content: space-between; gap: 12px; font-size: 14.5px; color: var(--muted); }
.summary__row b { color: var(--ink); font-weight: 800; }
.summary__total {
    border-top: 1px solid var(--line-soft); padding-top: 14px;
    display: flex; justify-content: space-between; align-items: baseline; gap: 12px;
}
.summary__total span { font-weight: 800; font-size: 16px; }
.summary__total b { font-family: var(--font-display); font-weight: 400; font-size: 26px; color: var(--navy); }
.summary__note { font-size: 12.5px; color: var(--muted-4); line-height: 1.55; }
.summary__note b { color: var(--green-ink); font-weight: 800; }

/* Скидка — единственная строка, где сумма уменьшается. Цветом
   отделяем её от начислений: «−350 ₽» и «350 ₽» на беглый взгляд
   различаются только минусом, а он в русском начертании узкий. */
.summary__row--discount { color: var(--green-ink); }
.summary__row--discount b { color: var(--green-ink); }
.summary__row--discount span span { font-weight: 800; text-transform: uppercase; }

/* Согласие в сводке — не очередное примечание мелким серым, а
   действие, без которого кнопка ниже не сработает. Линия сверху
   отделяет его от расчёта сумм, а цвет держим читаемым: текст,
   который просят прочитать, не набирают самым бледным оттенком. */
.summary__consent { border-top: 1px solid var(--line-soft); padding-top: 16px; }
.summary__consent span { color: var(--muted-2); }

/* ------------------------------------------- шкала бесплатной доставки */

.freebar { display: flex; flex-direction: column; gap: 8px; }
.freebar__track { height: 6px; border-radius: 999px; background: var(--surface-2); overflow: hidden; }
.freebar__fill {
    height: 100%; border-radius: 999px; background: var(--green);
    /* Ширину меняет JS после каждого пересчёта — без перехода полоса
       дёргалась бы скачком на каждое нажатие «+». */
    transition: width .25s ease;
}
.freebar__note { font-size: 12.5px; font-weight: 700; color: var(--muted-2); }

/* ------------------------------------------------- промокод и баллы */

.promo {
    display: flex; flex-direction: column; gap: 10px;
    border-top: 1px solid var(--line-soft); padding-top: 16px;
    /* Цвет задан явно, хотя и совпадает с наследуемым. Не «на всякий
       случай»: именно его отсутствие и сделало форму нечитаемой, когда
       выше по файлу оказался второй .promo с тёмно-синей заливкой. */
    color: var(--ink);
}
.promo__label { font-weight: 800; font-size: 14px; display: flex; justify-content: space-between; gap: 10px; }
.promo__label span { font-weight: 700; font-size: 12.5px; color: var(--muted-4); }
/*
 * Поле и кнопка в строку.
 *
 * Кнопка не сжимается (flex:none), ширину отдаёт поле (flex:1;
 * min-width:0). Считаем, сколько ей достаётся: колонка итогов 380px,
 * минус padding .summary 26px с двух сторон и 8px зазора — 320px на
 * двоих. «Применить» базовыми отступами .btn (14px 28px, кегль 15)
 * занимает около 140px, полю остаётся 180px, и в него не влезает
 * ни подсказка, ни длинный код.
 *
 * Отсюда две правки. Отступы кнопки ужаты до .btn--sm (её ширина ~105px,
 * поле получает 215px) — но кеглем кнопка не мельчает, иначе она
 * перестанет читаться как кнопка. Высоту держит align-items:stretch:
 * кнопка тянется по полю, а не по своим отступам, поэтому строка не
 * разъезжается по высоте от смены текста в админке.
 *
 * white-space:nowrap обязателен: текст кнопки задаёт владелец
 * (cart.promo_btn), и «Применить промокод» без него сложится в две
 * строки внутри flex:none — то есть кнопка станет вдвое выше поля.
 */
.promo__row { display: flex; gap: 8px; align-items: stretch; }
.promo__row .input { flex: 1; min-width: 0; }
.promo__row .btn { flex: none; white-space: nowrap; padding: 10px 16px; }
.promo__applied {
    display: flex; flex-direction: column; gap: 3px;
    background: var(--green-soft); border-radius: 12px; padding: 12px 14px;
}
.promo__applied b { font-weight: 800; font-size: 14.5px; color: var(--green-ink); text-transform: uppercase; }
.promo__applied span { font-size: 12.5px; color: var(--green-ink); line-height: 1.5; }
/* 12.5px, а не 12: подсказка под промокодом теперь несёт текст владельца
   из админки, а не одну строку про баллы, и её действительно читают. */
.promo__hint { font-size: 12.5px; color: var(--muted-3); line-height: 1.5; }
.promo__hint b { font-weight: 800; color: var(--muted); }
.promo__error { font-size: 12.5px; font-weight: 700; color: var(--red); line-height: 1.5; }
/* Тот же элемент показывает и удачу: заводить второй ради цвета
   значило бы держать в JS правило «в какой из них писать». */
.promo__error--ok { color: var(--green-ink); }

.notice {
    background: var(--orange-soft); border-radius: 12px; padding: 14px 16px;
    font-size: 13.5px; line-height: 1.55; color: #8a5210;
}
.notice--green { background: var(--green-soft); color: var(--green-ink); }

/* --------------------------------------------------------- checkout */

.checkout { display: grid; grid-template-columns: 1fr 380px; gap: 32px; align-items: start; }
.checkout__block { display: flex; flex-direction: column; gap: 16px; }
.checkout__legend { font-weight: 800; font-size: 18px; }
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }

.order-mini { display: flex; flex-direction: column; gap: 10px; max-height: 280px; overflow-y: auto; }
.order-mini__row { display: flex; justify-content: space-between; gap: 12px; font-size: 14px; }
.order-mini__row span { color: var(--muted); }
.order-mini__row b { white-space: nowrap; }

/* ------------------------------------------- checkout: адрес и время */

/* Блок доставки — колонка, а не .form-grid: адрес, зона, даты и
   интервалы идут строго друг за другом. В две колонки дата встала бы
   вровень с адресом, хотя зависит от него и меняется вслед за ним. */
[data-delivery-fields] { display: flex; flex-direction: column; gap: 16px; }
[data-delivery-fields] .field__label { font-size: 13.5px; font-weight: 700; color: var(--muted); }

/* Зона — вывод из адреса, не выбор покупателя: показываем как подпись,
   а не как поле, чтобы её не пытались исправить вводом. */
.delivery-zone {
    display: flex; flex-direction: column; gap: 2px;
    border-left: 3px solid var(--green); padding: 2px 0 2px 12px;
    font-size: 13.5px;
}
.delivery-zone__name { font-weight: 800; color: var(--green-ink); }
.delivery-zone__desc:empty { display: none; }
.delivery-zone__desc { color: var(--muted-2); }

/* Чипсы дат и интервалов — те же, что в каталоге, но с настоящей
   радиокнопкой внутри. Кнопку прячем, а не убираем: :checked красит
   ярлык, а браузер бесплатно даёт стрелки и чтение с экрана. */
.chip:has(input) { cursor: pointer; }
.chip input { position: absolute; opacity: 0; width: 0; height: 0; }
.chip:has(input:checked) {
    border-color: var(--orange); background: var(--orange-soft); color: var(--orange-dark);
}
.chip:has(input:focus-visible) { outline: 2px solid var(--orange); outline-offset: 2px; }
.chip--off, .chip--off:hover {
    border-style: dashed; border-color: var(--line);
    background: none; color: var(--muted-4); cursor: not-allowed;
    text-decoration: line-through;
}

/* --------------------------------------------- точка на карте */

/*
 * Кнопка «отметить место» и окно с картой.
 *
 * Правила стоят в разделе оформления заказа, но принадлежат двум
 * страницам: тот же блок лежит у каждого адреса в кабинете
 * (views/account/addresses.php). Ключ к тому, чтобы они не разъехались,
 * — не в CSS, а в том, что разметка у них общая: её печатает один и тот
 * же кусок шаблона, и классы здесь названы по блоку .point, а не по
 * странице.
 *
 * Весь блок приходит с сервера скрытым и открывается из map-picker.js:
 * без JS карту не показать, и кнопка, которая ничего не делает, хуже её
 * отсутствия.
 */
.point { display: flex; flex-direction: column; gap: 8px; }

/* Пунктирная рамка вместо сплошной — это «можно, но не обязательно».
   Кнопка стоит между обязательным адресом и обязательным телефоном, и
   сплошная оранжевая кнопка читалась бы как ещё один шаг, который надо
   пройти, чтобы заказ ушёл. */
.point__open {
    align-self: start;
    display: inline-flex; align-items: center; gap: 8px;
    min-height: 44px; padding: 10px 16px;
    border: 1px dashed var(--line-dash); border-radius: 12px;
    background: #fff; color: var(--orange-dark);
    font-family: var(--font); font-weight: 700; font-size: 14.5px;
    text-align: left; cursor: pointer;
}
.point__open:hover { border-color: var(--orange); background: var(--orange-soft); }
.point__open:focus-visible { outline: 2px solid var(--orange); outline-offset: 2px; }

/* Метку рисуем, а не берём картинкой или эмодзи: картинка — это лишний
   запрос ради двенадцати пикселей, а эмодзи на каждой системе своё, и в
   Windows на месте оранжевой булавки приходит синий значок мимо всей
   палитры. Круг с одним прямым углом, повёрнутый на 45°, — это и есть
   булавка. */
.point__mark, .point__open::before {
    content: ''; flex: none;
    width: 12px; height: 12px;
    border-radius: 50% 50% 50% 0;
    transform: rotate(-45deg);
    background: var(--orange);
}

/* Отмеченная точка — зелёная плашка: тот же цвет, которым на этой
   странице говорит зона доставки, то есть «с адресом всё в порядке». */
.point__done {
    display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
    margin: 0; padding: 10px 14px;
    border-radius: 12px; background: var(--green-soft);
    font-size: 14px; color: var(--green-ink);
}
.point__done .point__mark { background: var(--green); }
.point__done .linkish { font-size: 14px; }

/* Подпись приходит от геокодера и бывает длиннее строки («Рязанская
   область, Рыбновский район, село Речицы, Дачная улица, 12»). Без
   anywhere она распирала плашку и уводила кнопки «изменить»/«убрать» за
   край экрана на телефоне. */
.point__text { flex: 1 1 200px; min-width: 0; font-weight: 700; overflow-wrap: anywhere; }

/*
 * Окно с картой.
 *
 * Высота задана числом, и это не украшение: у <iframe> нет собственной
 * высоты — без явного размера он встаёт в 150px по умолчанию, и вместо
 * карты получается полоска, на которой ничего не выбрать.
 *
 * Внутри рамки лежит /map/point — отдельная страница со своей вёрсткой.
 * Отсюда ей достаётся только прямоугольник; всё остальное она рисует
 * сама, и в этом весь смысл разделения (см. map-picker.js).
 */
.point-dialog {
    width: min(880px, 100vw - 40px);
    height: min(640px, 100vh - 40px);
    padding: 0; border: none; border-radius: 18px; overflow: hidden;
    background: #fff; box-shadow: var(--shadow-pop);
}
.point-dialog::backdrop { background: rgba(14, 44, 80, .55); }
.point-dialog__frame { display: block; width: 100%; height: 100%; border: 0; }

/* Страница под окном не должна ехать под колесом мыши: показ модального
   <dialog> делает документ inert для щелчков, но прокрутку фона браузеры
   не запрещают, и оформление заказа уезжало вверх, пока покупатель
   пытался подвинуть карту. Скриптом это не делаем — стилю достаточно
   знать, что окно открыто. */
body:has(.point-dialog[open]) { overflow: hidden; }

/* На телефоне окно во весь экран. Карта — единственное, ради чего его
   открыли, и поля вокруг неё здесь отнимают у неё же место.
   100dvh, а не 100vh: с vh нижняя панель браузера накрывает кнопки
   «Отмена» и «Готово», до которых после этого не дотянуться вовсе. */
@media (max-width: 640px) {
    .point-dialog {
        width: 100vw; max-width: none;
        height: 100vh; height: 100dvh; max-height: none;
        border-radius: 0;
    }
}

/* ------------------------------------------------------- заказ/чек */

.order-head {
    display: flex; justify-content: space-between; gap: 16px;
    flex-wrap: wrap; align-items: center;
}
.badge {
    display: inline-flex; align-items: center; font-weight: 800; font-size: 12.5px;
    padding: 6px 12px; border-radius: 8px; background: var(--surface); color: var(--muted);
}
.badge--new { background: var(--orange-soft); color: #b06008; }
.badge--confirmed, .badge--assembling { background: var(--orange-soft); color: #b06008; }
.badge--ready, .badge--assembled, .badge--shipped { background: #eaf1fb; color: var(--blue); }
.badge--completed { background: var(--green-soft); color: var(--green-ink); }
.badge--cancelled { background: var(--red-soft); color: var(--red); }

.order-table { width: 100%; border-collapse: collapse; font-size: 14.5px; }
.order-table th {
    text-align: left; font-size: 12.5px; text-transform: uppercase; letter-spacing: .8px;
    color: var(--muted-3); font-weight: 800; padding: 0 0 10px;
}
.order-table td { padding: 12px 0; border-top: 1px solid var(--line-soft); vertical-align: top; }
.order-table td:last-child, .order-table th:last-child { text-align: right; }

/* Код заказа на экране «спасибо».
   Пунктирная рамка — это визуальная цитата бумажного талона: она
   говорит «перепиши меня» раньше, чем человек дочитает подпись. */
.order-code {
    display: flex; flex-direction: column; align-items: center; gap: 6px;
    margin: 18px auto 4px; max-width: 420px; padding: 18px 22px;
    border: 2px dashed var(--orange); border-radius: 16px; background: var(--orange-soft);
}
.order-code__label {
    font-size: 12px; font-weight: 800; text-transform: uppercase;
    letter-spacing: 1.2px; color: #b06008;
}
.order-code__value {
    /* Моноширинный — чтобы 0 и O, 5 и S различались начертанием, а не
       догадкой: код переписывают с экрана на бумагу и диктуют вслух.
       Разрядка помогает не слипнуться соседним знакам. */
    font-family: 'Courier New', Courier, monospace;
    font-size: 30px; font-weight: 700; letter-spacing: 3px; color: var(--navy);
    line-height: 1.2;
    /* Чтобы код выделялся двойным щелчком целиком, без дефиса-разрыва. */
    user-select: all;
    /* И по той же причине — исключение из общей страховки на body:
       код переписывают с экрана и диктуют вслух, разрыв посреди него
       добавил бы к диктовке ещё и лишний перенос. */
    overflow-wrap: normal;
}
.order-code__hint { font-size: 12.5px; line-height: 1.55; color: var(--muted); text-align: center; }

@media (max-width: 480px) {
    .order-code__value { font-size: 25px; letter-spacing: 2px; }
}

/* -------------------------------------------------------- кабинет */

.account { display: grid; grid-template-columns: 280px 1fr; gap: 32px; align-items: start; }
.account__nav {
    border: 1.5px solid var(--line); border-radius: 18px; padding: 12px;
    display: flex; flex-direction: column; gap: 4px;
}
.account__user {
    display: flex; align-items: center; gap: 14px; padding: 16px 14px;
    border-bottom: 1px solid var(--line-soft); margin-bottom: 8px;
}
.avatar {
    width: 48px; height: 48px; border-radius: 50%; background: var(--green-soft);
    color: var(--green); display: flex; align-items: center; justify-content: center;
    font-weight: 800; font-size: 19px; flex-shrink: 0;
}
.account__user b { font-weight: 800; font-size: 15.5px; }
.account__user span { font-size: 13px; color: var(--muted-3); }
.account__link {
    text-align: left; font-weight: 700; font-size: 14.5px;
    padding: 12px 14px; border-radius: 10px; color: var(--ink);
}
.account__link:hover { background: var(--surface); }
.account__link.is-active { background: var(--surface); }
button.account__link { border: none; background: none; cursor: pointer; font: inherit; text-align: left; width: 100%; }
.account__link--exit { color: var(--red); }
.account__link--exit:hover { background: var(--red-soft); color: var(--red); }

.bonus-card {
    background: linear-gradient(120deg, #0e2c50, #1d4e8f); border-radius: 16px;
    padding: 26px; display: flex; flex-direction: column; gap: 6px; color: #fff;
}
.bonus-card__label { font-size: 13px; color: var(--on-navy-dim); letter-spacing: 1px; text-transform: uppercase; font-weight: 700; }
.bonus-card__value { font-family: var(--font-display); font-size: 36px; }
.bonus-card__note { font-size: 13.5px; color: var(--on-navy-dim); }

/* Счётчик в пункте меню: «Избранное 7». Прижат вправо, поэтому ссылка
   становится flex-строкой, а не просто блоком с текстом. */
.account__link { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.account__count {
    font-size: 12px; font-weight: 800; min-width: 22px; text-align: center;
    padding: 3px 7px; border-radius: 7px; background: var(--orange-soft); color: var(--orange-dark);
}
.account__link.is-active .account__count { background: #fff; }

/* Ряд кнопок под формой. Прижат вправо: «сохранить» ищут там же, где
   в любой другой форме магазина. */
.form-actions { display: flex; align-items: center; justify-content: flex-end; gap: 12px; flex-wrap: wrap; }

/* Подпись под строкой — комментарий к операции, адрес под названием.
   В админке такой же класс; витрине он понадобился вместе с кабинетом. */
.table__sub { display: block; font-size: 12.5px; color: var(--muted-3); font-weight: 500; }

/* ------------------------------------------------- кабинет: заказы */

.order-card { gap: 0; padding: 0; overflow: hidden; }
.order-card__link {
    display: flex; flex-direction: column; gap: 14px;
    padding: 22px 24px; color: var(--ink);
}
.order-card__link:hover { background: var(--surface-2); }
.order-card__foot {
    display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
    padding: 14px 24px 18px; border-top: 1px dashed var(--line);
}
.order-head { display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap; }

/* ---------------------------------------------- кабинет: избранное */

/* Карточки те же, что в каталоге, — и правило то же: считаем от ширины
   карточки. Жёсткие три колонки в кабинете были уже каталожных: слева
   стоит меню кабинета, и на ноутбуке карточка сжималась ниже читаемого. */
.favorites-grid {
    display: grid; gap: 16px;
    grid-template-columns: repeat(auto-fill, minmax(var(--card, 210px), 1fr));
}

/* ------------------------------------------------- кабинет: адреса */

/*
 * Раскладушка адреса — тот же <details>, что в справочнике зон админки:
 * открывается без JS, и внутри лежит обычная форма. Правила повторены,
 * а не вынесены в общий файл: витрина и админка грузят разные таблицы
 * стилей, и связывать их одним файлом ради семи селекторов — значит
 * заставить покупателя качать админку.
 */
.address-list { padding: 0; gap: 0; overflow: hidden; }
.editor { border-bottom: 1px solid var(--line-soft); }
.editor:last-child { border-bottom: none; }
.editor[open] { background: #fbfcfe; }

.editor__head {
    display: flex; align-items: center; justify-content: space-between; gap: 16px;
    padding: 16px 22px; cursor: pointer; list-style: none; font-size: 14.5px;
}
.editor__head::-webkit-details-marker { display: none; }
.editor__head:hover { background: var(--surface); }
.editor[open] .editor__head { border-bottom: 1px solid var(--line-soft); }
.editor__head:focus-visible { outline: 2px solid var(--orange); outline-offset: -2px; }

.editor__title b { display: block; font-weight: 800; }
.editor__title b::before {
    content: '▸'; display: inline-block; width: 16px;
    color: var(--muted-3); font-weight: 400;
}
.editor[open] .editor__title b::before { content: '▾'; }
.editor__title .table__sub { padding-left: 16px; }
.editor--new .editor__title b::before { content: ''; width: 0; }
.editor--new .editor__title b { color: var(--orange-dark); }

.editor__body { padding: 20px 22px 24px; display: flex; flex-direction: column; gap: 16px; }
.editor__foot {
    display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap;
    padding: 14px 0 18px; border-top: 1px dashed var(--line); margin: 0 22px 4px;
}

/* Подъезд, этаж и квартира короткие — в одну строку, а не три поля
   во всю ширину под улицей. */
.address-parts { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; }

/* ------------------------------------------------- кабинет: баллы */

.bonus-table td:first-child { white-space: nowrap; color: var(--muted-2); }
.bonus-table__amount { font-weight: 800; white-space: nowrap; }
.bonus-table__amount.is-plus { color: var(--green-ink); }
.bonus-table__amount.is-minus { color: var(--red); }

/*
 * Вход, регистрация и восстановление пароля.
 *
 * Ширину держат оба блока — шапка и карточка, — а не общая обёртка.
 * Переменная, чтобы они не разъехались: раньше `max-width` стоял только
 * у карточки, шапка тянулась на все 1280px контейнера, и заголовок
 * «Вход в кабинет» висел у левого края в двух третях пустого экрана.
 */
.auth-head, .auth-card { --auth-w: 460px; max-width: var(--auth-w); margin: 0 auto; }

/* Внутри остался один заголовок: логотип отсюда убран, он дублировал
   тот, что и так стоит в шапке раскладки. Flex с align-items здесь
   больше не нужен — центрует уже text-align, — но gap оставлять тем
   более незачем: центровать и разделять нечего. */
.auth-head { text-align: center; margin-bottom: 24px; }
.auth-head h1 { font-size: 30px; }

.auth-card {
    border: 1.5px solid var(--line); border-radius: 20px; padding: 36px;
    display: flex; flex-direction: column; gap: 16px;
    /* Тень, а не только рамка: карточка — единственное активное место на
       пустой странице, и она должна читаться как поднятая над фоном,
       иначе выглядит рамкой вокруг текста. */
    background: #fff; box-shadow: var(--shadow-card);
}
.auth-card__title { font-weight: 800; font-size: 20px; }
.auth-card__text { font-size: 14.5px; color: var(--muted-2); line-height: 1.6; }
/* Отступы между полями держит класс, а не style="gap:16px" в разметке:
   инлайновые стили здесь уже убирали ради того, чтобы не открывать
   style-src 'unsafe-inline' в CSP (см. .section--narrow). */
.auth-form { display: flex; flex-direction: column; gap: 16px; }

.auth-tabs { display: flex; gap: 8px; margin-bottom: 4px; }
.auth-tabs a {
    flex: 1; text-align: center; padding: 11px; border-radius: 10px;
    font-weight: 800; font-size: 14.5px; background: var(--surface); color: var(--muted);
    transition: background-color .15s, color .15s;
}
.auth-tabs a:hover { background: #e9eff7; color: var(--navy); }
.auth-tabs a.is-active, .auth-tabs a.is-active:hover { background: var(--navy); color: #fff; }

/* Приписка под карточкой: почему форму вообще можно заполнять. */
.auth-foot {
    max-width: var(--auth-w, 460px); margin: 18px auto 0;
    display: flex; align-items: flex-start; justify-content: center; gap: 8px;
    font-size: 12.5px; line-height: 1.5; color: var(--muted-3); text-align: left;
}
.auth-foot svg { flex: none; margin-top: 1px; color: var(--green); }

/* ------------------------------------------------------- реквизиты */

.requisites {
    border: 1.5px solid var(--line); border-radius: 18px; padding: 30px;
    display: grid; grid-template-columns: 240px 1fr; gap: 14px 24px;
    font-size: 15px; max-width: 900px; margin: 0;
}
.requisites dt { color: var(--muted-3); font-weight: 700; margin: 0; }
.requisites dd { margin: 0; }

.numbered { display: flex; flex-direction: column; gap: 10px; border: 1.5px solid var(--line); border-radius: 18px; padding: 28px; }
.numbered__num { font-family: var(--font-display); font-size: 30px; }
.numbered__title { font-weight: 800; font-size: 17px; }
.numbered__text { font-size: 14.5px; color: var(--muted); line-height: 1.65; }

.contact-card { border: 1.5px solid var(--line); border-radius: 18px; padding: 28px; display: flex; flex-direction: column; gap: 14px; }
.contact-card__phone { font-weight: 800; font-size: 22px; color: var(--navy); }
.contact-card__label { font-size: 13.5px; color: var(--muted-3); }
.map-stub {
    background: var(--surface); border-radius: 18px; display: flex; align-items: center;
    justify-content: center; min-height: 240px; color: var(--muted-3); font-size: 14.5px;
    flex-direction: column; gap: 8px;
}
.map-frame {
    width: 100%; min-height: 320px; height: 100%; border: 0;
    border-radius: 18px; display: block; background: var(--surface);
}

/* ----------------------------------------------------------- флеш */

.flashes { display: flex; flex-direction: column; gap: 10px; padding-top: 20px; }
.flash {
    border-radius: 12px; padding: 14px 18px; font-size: 14.5px; font-weight: 700;
    display: flex; gap: 10px; align-items: center;
}
.flash--success { background: var(--green-soft); color: var(--green-ink); }
.flash--error { background: var(--red-soft); color: var(--red); }
.flash--info { background: var(--surface); color: var(--muted); }

/* ---------------------------------------------------------- подвал */

.footer { background: var(--navy); color: var(--on-navy); margin-top: 70px; flex-shrink: 0; }
.footer a { color: var(--on-navy); }
.footer a:hover { color: #fff; }
.footer__cols {
    display: grid; grid-template-columns: 1.2fr 1fr 1fr 1.3fr; gap: 40px;
    padding-top: 54px;
}
.footer__col { display: flex; flex-direction: column; gap: 12px; font-size: 14px; }
.footer__col h4 { font-weight: 800; color: #fff; font-size: 15px; margin: 0 0 4px; font-family: var(--font); }
.footer__about { display: flex; flex-direction: column; gap: 16px; align-items: flex-start; }
/* Белая плашка под логотипом. Эмблема нарисована под светлый фон:
   на тёмно-синем подвале её синие листья и лента сливаются с фоном,
   и от знака остаются одни оранжевые пятна. Скруглённый прямоугольник,
   а не круг: лента «Витаминный рай» выступает за окружность эмблемы,
   и в круге её концы упирались бы в край. */
.footer__about img {
    width: 128px; height: auto; display: block;
    padding: 12px; background: #fff; border-radius: 26px;
}
.footer__about p { margin: 0; font-size: 14px; line-height: 1.65; color: var(--on-navy-dim); }
.footer__phone { color: #fff; font-weight: 700; font-size: 17px; }
.footer__phone-note { color: var(--on-navy-dim); margin-top: -8px; }
.footer__hours { color: var(--on-navy-dim); }
.footer__bottom {
    margin-top: 40px; padding-top: 20px; padding-bottom: 20px;
    border-top: 1px solid rgba(255, 255, 255, .14);
    display: flex; justify-content: space-between; gap: 16px; flex-wrap: wrap;
    font-size: 13px; color: var(--on-navy-mute);
}

/* ------------------------------------- юридические документы */

/* Ширина ограничена намеренно: строка в 90 символов читается тяжело,
   а политику конфиденциальности и без того мало кто дочитывает. */
.doc { max-width: 820px; margin: 0 auto; gap: 0; }
.doc__title { font-family: var(--font-display); font-size: 32px; margin: 0 0 8px; }
.doc__body { font-size: 15.5px; line-height: 1.75; color: var(--ink); }
.doc__body h2 { font-size: 19px; margin: 28px 0 10px; font-weight: 800; }
.doc__body h2:first-child { margin-top: 8px; }
.doc__body p { margin: 0 0 14px; }
.doc__body ul, .doc__body ol { margin: 0 0 14px; padding-left: 22px; }
.doc__body li { margin-bottom: 7px; }
.doc__updated { margin: 24px 0 0; font-size: 13.5px; color: var(--muted-3); }
.doc__empty { font-size: 15px; color: var(--muted-2); line-height: 1.7; margin: 0; }

/* --------------------------------------------------------- ошибка */

.error-page { text-align: center; padding: 90px 0 40px; display: flex; flex-direction: column; align-items: center; gap: 18px; }
.error-page__code { font-family: var(--font-display); font-size: 72px; color: var(--orange); line-height: 1; }

/* --------------------------------------------- нижняя навигация */

/*
 * Панель под большим пальцем. На широком экране её нет совсем:
 * там шапка всегда на виду, и дублировать её незачем.
 *
 * Показывается в медиазапросе ниже — по умолчанию скрыта, чтобы
 * на десктопе не мелькнуть до применения стилей.
 */
.bottomnav { display: none; }

.bottomnav__item {
    flex: 1 1 0;
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    gap: 3px; padding: 8px 2px 6px;
    /* 48px — нижняя граница удобного нажатия; ниже палец промахивается. */
    min-height: 48px;
    font-size: 11px; font-weight: 700; line-height: 1.1;
    color: var(--muted); text-align: center;
    /* Серый прямоугольник подсветки на тапе выглядит как поломка. */
    -webkit-tap-highlight-color: transparent;
}
.bottomnav__item svg { flex-shrink: 0; }
.bottomnav__item.is-active { color: var(--orange); }
.bottomnav__item:hover { color: var(--orange); }

.bottomnav__icon { position: relative; display: flex; }
.bottomnav__badge {
    position: absolute; top: -6px; left: 12px;
    background: var(--orange); color: #fff; font-size: 10.5px; font-weight: 800;
    min-width: 17px; height: 17px; border-radius: 999px;
    display: flex; align-items: center; justify-content: center; padding: 0 4px;
}
/* Пустая корзина не рисует нолик: [hidden] сильнее display:flex выше. */
.bottomnav__badge[hidden] { display: none; }

/* ------------------------------------------------------- адаптив */

@media (max-width: 1100px) {
    .hero, .split { grid-template-columns: 1fr; gap: 32px; }
    .hero h1 { font-size: 38px; }
    .promo-banner__body { padding: 40px 34px; }
    /* Колонка подбора ужимается до 220px, но карточка товара сжиматься
       вслед за ней не должна — auto-fill сам уберёт лишнюю колонку. */
    .catalog { grid-template-columns: 220px minmax(0, 1fr); gap: 24px 28px; }
    .cart, .checkout { grid-template-columns: 1fr; }
    .summary { position: static; }
    .product { grid-template-columns: 1fr; gap: 28px; }
    /* Две колонки, а не три: сетка стоит под четырьмя плитками
       преимуществ, и три колонки оставляют четвёртую сиротой на
       отдельной строке. Два на два — ровно. */
    .grid--4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .account { grid-template-columns: 1fr; }
    .account__nav { flex-direction: row; flex-wrap: wrap; }
    .account__user { border-bottom: none; margin-bottom: 0; width: 100%; }

    /* Меню стало строкой — пункт больше не растягивается на всю ширину,
       и счётчику незачем улетать к правому краю экрана. */
    .account__link { justify-content: flex-start; }
}

@media (max-width: 860px) {
    :root { --gutter: 20px; }
    h1 { font-size: 30px; }
    h2 { font-size: 26px; }
    .section { padding-top: 44px; }

    /* Липкая шапка перекрывает якорь, если просто прыгнуть к #contacts.
       Браузер сам оставит запас, если сказать ему высоту шапки. */
    html { scroll-padding-top: 78px; }

    /* Нижняя панель занимает свои 56px поверх страницы — освобождаем их,
       иначе последняя строка подвала окажется под ней навсегда. */
    body { padding-bottom: calc(56px + env(safe-area-inset-bottom, 0px)); }

    /* В альбомной ориентации «чёлка» съедает края экрана. max() оставляет
       обычные поля там, где выреза нет, и раздвигает их там, где он есть. */
    .container {
        padding-left: max(var(--gutter), env(safe-area-inset-left, 0px));
        padding-right: max(var(--gutter), env(safe-area-inset-right, 0px));
    }

    /* Верхняя строка целиком переезжает вниз: её ссылки (о нас, доставка,
       контакты, телефон) целиком повторяются в подвале и в нижней панели,
       а высота строчки — 17px, промахнуться по ней пальцем проще, чем
       попасть. Экран телефона слишком дорог, чтобы платить за дубли. */
    .topbar { display: none; }

    /* Шапка остаётся на экране: поиск — главный способ найти товар в
       магазине на полторы тысячи позиций, и уезжать вверх ему нельзя.
       Всё остальное из шапки ушло в нижнюю панель, поэтому строка одна. */
    .masthead {
        position: sticky; top: 0; z-index: 30;
        background: #fff; box-shadow: 0 1px 0 var(--line);
        grid-template-columns: auto 1fr; gap: 12px;
        padding-top: 8px; padding-bottom: 8px;
    }
    .masthead__logo { order: 1; justify-self: start; }
    .masthead__logo img { height: 42px; }
    .masthead__actions { display: none; }

    /* На телефоне прилипает поиск, а не меню — правило выше. Двух липких
       полос подряд экран не выдержит: строка меню тут прокручивается вбок
       и занимает 46px, вместе с поиском это 104px из 640, шестая часть.

       position тут не трогаем: ниже в этом же медиазапросе меню и так
       становится relative — ему нужен якорь для ::after, подтаивающего
       края прокручиваемой полосы. Гасим только вид «прилипшего»:
       наблюдатель на телефоне работает и класс всё равно повесит, а тень
       под неприлипшей строкой выглядела бы поломкой.

       Корзина здесь не нужна вовсе: она в нижней панели, всегда на виду. */
    .mainnav.is-stuck { box-shadow: none; border-bottom-color: var(--line); }
    .mainnav__cart { display: none; }

    /*
     * 16px — не про красоту. Safari на iPhone увеличивает страницу, когда
     * фокус попадает в поле мельче 16px, и обратно её уже не отматывает:
     * человек тыкает в поиск и оказывается на зумленном сайте, из которого
     * выбирается щипком. У нас поля были 14–15px, то есть под этот зум
     * попадали все формы разом, включая оформление заказа.
     *
     * .field__control > input — поля пароля. Их этот список пропускал ровно
     * по той же причине, по какой их пропускало и оформление (см. .field
     * выше): пароль лежит в обёртке, а не прямо в .field. Получалось хуже,
     * чем «зумит везде»: почта в форме регистрации зум не вызывала, а
     * следующее поле под ней — вызывало, и страница прыгала посреди
     * заполнения. Именно на пароле это больнее всего: экранная клавиатура
     * уже открыта, половина формы уехала за край, и куда вернуться —
     * непонятно.
     */
    .field > input, .field > textarea, .field > select, .input,
    .field__control > input,
    .masthead .search input, .toolbar select { font-size: 16px; }

    .masthead .search { order: 2; max-width: none; padding: 0 14px 0 0; }
    .masthead .search input { padding: 13px 0; min-width: 0; }
    /* Кнопка поиска была 18×18 — это размер значка, а не зоны нажатия.
       Тянем её на всю высоту поля: 46px по обеим сторонам. flex:none
       обязателен, иначе в тесной строке её ужимает обратно до значка. */
    .search__submit {
        flex: none; width: 46px; align-self: stretch;
        align-items: center; justify-content: center;
    }

    /* Ссылок в меню больше, чем влезает в 375px. Полоска и так
       прокручивалась, но выглядела обрезанной случайно — теперь край
       подтаивает, и видно, что список продолжается. */
    .mainnav { position: relative; border-top: none; }
    .mainnav::after {
        content: ''; position: absolute; top: 0; right: 0; bottom: 0; width: 34px;
        background: linear-gradient(to right, rgba(255, 255, 255, 0), #fff);
        pointer-events: none;
    }
    .mainnav__inner {
        justify-content: flex-start; flex-wrap: nowrap; gap: 22px;
        padding-bottom: 2px; overflow-x: auto;
        scrollbar-width: none; scroll-snap-type: x proximity;
    }
    .mainnav__inner::-webkit-scrollbar { display: none; }
    .mainnav a, .mainnav__link { scroll-snap-align: start; }

    /* Выпадающие списки на телефоне выключены целиком.

       Не «спрятаны, чтобы не мешали» — им тут физически некуда
       раскрыться. Полоса меню прокручивается по горизонтали, а панель
       внутри overflow-x обрезается родителем; открыть её по наведению
       на сенсорном экране всё равно нельзя, а по касанию она перекрыла
       бы первый экран страницы.

       Ничего при этом не теряется: пункт раздела остаётся ссылкой на
       свою страницу каталога, а подразделы там выложены плитками (см.
       partial/category-tiles.php) — крупными, попасть пальцем проще,
       чем в строку списка. «Покупателям» — кнопка без списка, поэтому
       её убираем совсем, а сами страницы остаются в подвале. */
    .mainnav__drop { display: none; }
    .mainnav__caret { display: none; }
    .mainnav__link--btn { display: none; }

    /* Нижняя навигация — единственный элемент, который не уезжает вообще
       никогда. Корзина и каталог в одном движении большого пальца. */
    .bottomnav {
        display: flex; position: fixed; left: 0; right: 0; bottom: 0; z-index: 40;
        background: #fff; border-top: 1px solid var(--line);
        padding-bottom: env(safe-area-inset-bottom, 0px);
    }

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

       49px — это min-height пункта (48) плюс верхняя граница панели (1).
       Значение зашито числом, потому что взять высоту фактически CSS не
       умеет; если высота пункта поменяется, поменять надо и здесь —
       поэтому она и посчитана вслух. */
    .cookie-note { bottom: calc(49px + env(safe-area-inset-bottom, 0px) + 12px); }

    /* Сетки товаров (.grid--products, .favorites-grid) сюда не входят:
       они считают колонки от ширины карточки и в подсказке не нуждаются. */
    .grid--3, .grid--4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .form-grid { grid-template-columns: 1fr; }

    /* Сердечко тоже пальцем: 36px — курсорный размер. */
    .fav__btn { width: 44px; height: 44px; font-size: 19px; }

    /* Пальцем, а не курсором. «Плюс» и «минус» в 34×38 стоят вплотную
       друг к другу: промах по ним означает лишний килограмм в заказе,
       а это уже деньги покупателя. Кнопка покупки — тем более. */
    .stepper button { width: 44px; height: 44px; }
    .stepper--muted button { width: 42px; height: 42px; }
    .stepper__value { min-width: 40px; }
    .btn--sm { min-height: 44px; padding: 10px 18px; }
    /* Ссылка-действие получает палец: 44px в высоту и inline-flex, чтобы
       область нажатия не схлопнулась по высоте строки. Ссылку, набранную
       внутри фразы, это правило ломает — её возвращает в строку
       .field--check .linkish, оно же и объясняет почему. */
    .section-head__link, .linkish { display: inline-flex; align-items: center; min-height: 44px; }

    /* Ссылка внутри текста остаётся текстом. inline-flex делает её
       неделимой коробкой, и строка рвётся по обе стороны от неё: «
       остаётся висеть в конце одной строки, а ». уезжает на отдельную
       третью. Так выглядели подписи под входом и регистрацией и согласие
       в оформлении заказа. Высота в 44px для ссылки посреди абзаца всё
       равно недостижима — она бы раздвинула строки текста. */
    p .linkish, label .linkish, .auth-foot .linkish { display: inline; min-height: 0; }
    .product-card__name { padding: 2px 0; }

    /*
     * flex отменяет и колонки, и расстановку по координатам разом —
     * блоки встают в том порядке, в каком лежат в шаблоне.
     *
     * align-items: stretch обязателен, и это не «на всякий случай».
     * В сетке выше стоит align-items: start — там он держит колонки
     * прижатыми к верху. Здесь ось поменялась на вертикальную, и тот же
     * start начинает означать «шириной по содержимому»: полоса
     * категорий не переносится (white-space: nowrap), её max-content —
     * это все разделы в одну строку, и на замере при экране 320px
     * .sidebar раздувался до 424px, утаскивая за собой всю страницу в
     * горизонтальную прокрутку. Прокрутка полосы при этом переставала
     * работать: скроллить было нечего, наружу торчал сам блок.
     *
     * Липкость и ограничение высоты тоже снимаем: они существуют ради
     * длинной боковой колонки, а здесь колонки нет.
     */
    .catalog { display: flex; flex-direction: column; gap: 20px; align-items: stretch; }
    .sidebar {
        position: static; gap: 12px; min-width: 0;
        max-height: none; overflow: visible; scrollbar-gutter: auto;
    }

    /* Категории строкой, а не колонкой: раньше список занимал треть
       экрана, и до первого товара надо было листать. Прокрутка вбок
       вместо вертикали — товары начинаются сразу под шапкой.
       Панель подбора под это правило не попадает: в ней галочки и поля
       ввода, которые в строку не укладываются. */
    .sidebar__box:not(.sidebar__box--filters) {
        flex-direction: row; align-items: center; gap: 6px;
        padding: 6px; overflow-x: auto; scrollbar-width: none;
        /* Правый край растворяется вместе с рамкой — видно, что строка
           не кончилась, а уходит за экран. Полосу прокрутки прячем: на
           телефоне её всё равно не видно, а место она занимает. */
        -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 26px), transparent);
                mask-image: linear-gradient(to right, #000 calc(100% - 26px), transparent);
    }
    .sidebar__box:not(.sidebar__box--filters)::-webkit-scrollbar { display: none; }
    .sidebar__box:not(.sidebar__box--filters) .sidebar__title { display: none; }
    .sidebar__link {
        flex: none; white-space: nowrap; align-items: center;
        min-height: 44px; padding: 10px 14px; background: var(--surface);
    }
    .sidebar__link.is-active { background: var(--orange-soft); }
    .sidebar__link--child { padding-left: 14px; }

    /*
     * Подбор на телефоне — шторка снизу, а не панель в потоке.
     *
     * В потоке она стоять не может ни раскрытой, ни свёрнутой. Раскрытая
     * — это два экрана галочек между шапкой и первым товаром, то есть
     * ровно та беда, от которой выше спасались строкой категорий.
     * Свёрнутая-в-потоке (так было раньше) вроде бы не мешает, но
     * раскрывается там, где лежит в разметке — над списком товаров, — и
     * покупателя после нажатия «Фильтры» уносит прокруткой к якорю,
     * теряя место, на котором он читал.
     *
     * Шторка выезжает поверх страницы у нижнего края — там, где палец.
     * Открывает кнопка «Фильтры (3)» из строки инструментов, закрывает —
     * «Показать» (уходит на новый адрес, уже без якоря), крестик или
     * нажатие по затемнению. Весь механизм — :target, без единой строчки
     * скрипта, поэтому работает и с отключённым JS, и до загрузки app.js.
     */
    .toolbar__filters { display: inline-flex; }
    .sidebar__box--filters { display: none; }
    .sidebar__box--filters:target {
        display: flex;
        position: fixed; left: 0; right: 0; bottom: 0; z-index: 60;
        /* Верхнюю треть экрана шторка не занимает намеренно: за ней
           должен остаться виден список, ради которого её и открыли. */
        max-height: min(78vh, 620px);
        overflow-y: auto; overscroll-behavior: contain;
        background: #fff; border: none; border-radius: 20px 20px 0 0;
        box-shadow: 0 -10px 40px rgba(14, 44, 80, .22);
        padding: 6px 14px calc(14px + env(safe-area-inset-bottom, 0px));
        animation: sheet-up .22s ease-out;
    }
    /* Полоска-«ручка» сверху: единственное, что сообщает, что это
       шторка, а не приклеенный к низу кусок страницы. */
    .sidebar__box--filters:target::before {
        content: ''; align-self: center; flex: none;
        width: 40px; height: 4px; margin: 4px 0 6px;
        border-radius: 999px; background: var(--line);
    }

    /* Затемнение позади шторки. Оно же — ссылка «закрыть»: см. разметку
       каталога, где `.filters__backdrop` стоит соседом после #filters. */
    #filters:target ~ .filters__backdrop {
        display: block; position: fixed; inset: 0; z-index: 55;
        background: rgba(14, 44, 80, .45);
    }

    /* Шапка и кнопка шторки прилипают к её краям: список характеристик
       длиннее шторки, и без этого «Сбросить всё» уезжало бы вверх, а
       «Показать» приходилось бы искать в самом низу прокрутки. */
    .sidebar__box--filters:target .filters__head {
        position: sticky; top: 0; z-index: 1;
        background: #fff; padding: 4px 0 8px;
    }
    /* Тень тут вместо подложки: сплошной белый прямоугольник ровно под
       кнопкой и на 14px в стороны — по размеру нижнего поля шторки.
       Без него следующая галочка видна в щели под кнопкой и выглядит
       как наполовину обрезанная строка. Смещение равно разбегу, поэтому
       вверх белое не лезет и список над кнопкой не закрывает. */
    .sidebar__box--filters:target .filters__apply {
        position: sticky; bottom: 0; margin-top: 14px;
        box-shadow: 0 14px 0 14px #fff;
    }

    /*
     * Фокус на шторке ставит сам браузер, когда переходит к якорю, — и
     * это правильно: Tab после открытия попадает внутрь подбора, а не в
     * конец страницы за ним. А вот рамку вокруг всей шторки убираем.
     * Общее правило рисует её в три пикселя оранжевым, и на блоке в
     * пол-экрана, который только что выехал поверх затемнённой
     * страницы, она читается не как фокус, а как сбой отрисовки. Полям
     * и кнопкам внутри свои рамки остаются.
     */
    .sidebar__box--filters:target:focus-visible { outline: none; }

    .filters__close {
        display: flex; align-items: center; justify-content: center;
        align-self: center; flex: none;
        width: 44px; height: 44px; margin: -6px -6px -6px 0;
        font-size: 24px; line-height: 1; color: var(--muted-3);
    }
    /* Кнопка возвращается: панель раскрыта поверх списка, и уход на
       новый адрес после каждой галочки закрывал бы её, не дав отметить
       вторую. Человек отмечает всё, что хотел, и жмёт «Показать». */
    .filters.is-live .filters__apply { display: inline-flex; }

    /* Чипсы — те же 44px под палец, что и остальные ссылки. */
    .chip { min-height: 44px; }
    .tiles { grid-template-columns: repeat(2, 1fr); gap: 12px; }

    /* Пять колонок в 375px не помещаются. Раскладываем в две строки и
       расставляем ячейки вручную: автопоток положил бы «убрать» под
       фотографию, а корзину чистят промахом пальца по сумме.
       Адресуем именно формы — кнопка внутри формы гриду не ребёнок. */
    .cart__line { grid-template-columns: 64px 1fr auto; gap: 12px; row-gap: 10px; }
    .cart__line img { width: 64px; height: 52px; }
    .cart__line > a:first-child { grid-row: 1 / span 2; grid-column: 1; }
    .cart__name { grid-row: 1; grid-column: 2; }
    .cart__drop { grid-row: 1; grid-column: 3; }
    .cart__qty  { grid-row: 2; grid-column: 2; }
    .cart__sum  { grid-row: 2; grid-column: 3; text-align: right; min-width: 0; }

    .requisites { grid-template-columns: 1fr; gap: 4px 0; }
    .requisites dd { margin-bottom: 12px; font-weight: 700; }
    .product__specs { grid-template-columns: 1fr; gap: 2px; }
    .product__specs dd { margin-bottom: 10px; }

    .footer { margin-top: 48px; }
    .footer__cols { grid-template-columns: 1fr 1fr; gap: 28px; }
    /* Ссылки подвала были 19px высотой и стояли впритык — по такому
       списку пальцем попадаешь через раз. */
    .footer__col { gap: 2px; }
    .footer__col a { display: flex; align-items: center; min-height: 44px; }
}

/*
 * Экран ниже 460px — это либо альбомная ориентация, либо (на Android)
 * открытая экранная клавиатура, которая ужимает область просмотра.
 * В обоих случаях закреплённая полоска внизу отнимает последнее место
 * и норовит закрыть поле ввода, поэтому её тут нет.
 */
@media (max-width: 860px) and (max-height: 460px) {
    .bottomnav { display: none; }
    body { padding-bottom: 0; }
}

@media (max-width: 560px) {
    .grid--3, .grid--4, .grid--2 { grid-template-columns: 1fr; }

    /* Кнопки про cookie — на свою строку и пополам. Рядом с текстом им
       не хватает ширины: «Только необходимые» переносится в две строки
       внутри самой кнопки и та вырастает вдвое. */
    .cookie-note { gap: 12px; padding: 14px; }
    .cookie-note__actions { flex: 1 1 100%; }
    .cookie-note__actions .btn { flex: 1; }

    /*
     * Товары на телефоне — две колонки, а не одна.
     *
     * Одна колонка означает одну карточку на экран: чтобы дойти до
     * десятого товара, надо пролистать девять фотографий во весь экран.
     * В магазине на полторы тысячи позиций так не выбирают — так
     * сдаются. Две колонки при 375px дают карточку в 161px: мелко, но
     * именно столько и показывают все крупные магазины, потому что
     * выбирают по фотографии и цене, а их видно и в 161px.
     *
     * auto-fill в этом месте отключён нарочно: он считает по минимуму, а
     * минимум на 320px не влезает дважды, и экран узкого телефона
     * схлопнулся бы в одну колонку. Здесь нужен ровно счёт, а не
     * подгонка.
     */
    .grid--products, .favorites-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }

    /* Поля и кегль ужимаем — в 161px прежние 16px по краям съедали бы
       десятую часть карточки. Цену с кнопкой ставить друг под друга тут
       уже не нужно: карточка такой ширины и так проходит по условию
       @container (max-width: 260px), где стопка задана один раз для всех
       ширин сразу — см. .product-card__foot. */
    .product-card__body { padding: 12px 12px 14px; gap: 5px; }
    .product-card__name { font-size: 14.5px; }
    .product-card__foot { padding-top: 10px; }

    /* Подъезд/этаж/квартира — три коротких поля; в одну колонку они
       растянулись бы на всю ширину ради двух символов. */
    .address-parts { grid-template-columns: repeat(3, 1fr); gap: 8px; }
    .editor__head, .editor__body { padding-left: 16px; padding-right: 16px; }
    .editor__foot { margin: 0 16px 4px; }
    .order-card__link, .order-card__foot { padding-left: 18px; padding-right: 18px; }

    /* Столбец «Остаток» уводим — на 320px он вытесняет саму операцию,
       а остаток всегда виден в карточке сверху. */
    .bonus-table th:last-child, .bonus-table td:last-child { display: none; }

    /*
     * Подвал не сваливаем в одну колонку: со зонами нажатия по 44px
     * получалась лента почти на тысячу пикселей, которую надо
     * пролистывать до реквизитов. Два коротких списка встают рядом,
     * а логотип и контакты — во всю ширину, они длиннее.
     */
    .footer__cols { grid-template-columns: 1fr 1fr; gap: 24px; }
    .footer__about, .footer__col:last-child { grid-column: 1 / -1; }

    .hero h1 { font-size: 30px; }
    .product__title { font-size: 26px; }
    /* Заголовок правовой страницы оказался единственным, кто не ужимался
       на телефоне: общее правило `h1` в этом же адаптиве проигрывает
       классу по специфичности, и «Политика конфиденциальности» так и
       оставалась в 32px — крупнее, чем заголовок главной. */
    .doc__title { font-size: 26px; }
    .empty { padding: 48px 24px; }
    .card, .summary, .auth-card { padding: 20px; }
}

@media print {
    .topbar, .masthead, .mainnav, .footer, .btn, .flashes, .bottomnav { display: none !important; }
    body { padding-bottom: 0; }
}

/* -------------------------------------------------- акции и отзывы */

.flag--promo { background: var(--red); color: #fff; }
.flag--rating { background: var(--surface); color: var(--ink); text-decoration: none; }
.cart__gross { display: block; font-size: 12.5px; color: var(--muted-4); }
.cart__promo { display: inline-block; margin-top: 2px; padding: 1px 6px; border-radius: 6px; background: var(--red); color: #fff; font-size: 11px; font-weight: 700; }

.reviews { display: grid; gap: 14px; margin-bottom: 18px; }
.review__head { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin-bottom: 8px; }
.review__verified { font-size: 12px; color: var(--green-ink); background: var(--green-soft); padding: 2px 8px; border-radius: 6px; }
.review__date { font-size: 12.5px; color: var(--muted-4); margin-left: auto; }
.reviews__stars { color: var(--orange); letter-spacing: 1px; }
.reviews__avg { display: inline-flex; align-items: center; gap: 8px; font-size: 14px; color: var(--muted); }
.reviews__empty { color: var(--muted); margin: 0 0 16px; }
.review__reply { margin-top: 10px; padding: 10px 14px; border-left: 3px solid var(--green); background: var(--green-soft); border-radius: 0 10px 10px 0; }
.review__reply p { margin: 4px 0 0; }
.review-form > summary { cursor: pointer; list-style: none; }
.review-form > summary::-webkit-details-marker { display: none; }
.review-form > form { margin-top: 16px; }
.review-form__rating { border: 0; padding: 0; margin: 0; display: flex; flex-direction: row-reverse; justify-content: flex-end; gap: 4px; }
.review-form__rating legend { font-size: 13.5px; font-weight: 700; color: var(--muted); width: 100%; margin-bottom: 6px; }
.review-form__star input { position: absolute; opacity: 0; width: 0; height: 0; }
.review-form__star span { font-size: 28px; color: var(--line); cursor: pointer; }
.review-form__star input:checked ~ span,
.review-form__star:hover span,
.review-form__star:hover ~ .review-form__star span,
.review-form__star input:checked ~ span ~ * { color: var(--orange); }
.review-form__rating .review-form__star:has(input:checked) ~ .review-form__star span { color: var(--orange); }

/* --------------------------------------------- отмена заказа покупателем */

/* Раскрывающаяся карточка: заголовок — как у остальных блоков заказа, а
   форма показывается только по нажатию — на странице статуса кнопка
   «отменить» на виду ловит случайные касания. */
details.card > summary { cursor: pointer; list-style: none; display: flex; align-items: center; gap: 10px; }
details.card > summary::-webkit-details-marker { display: none; }
details.card > summary::after { content: '+'; margin-left: auto; font-size: 22px; line-height: 1; color: var(--muted); }
details.card[open] > summary::after { content: '−'; }
details.card > form { margin-top: 16px; }
details.card fieldset { border: 0; padding: 0; margin: 0; }
