/* ============================================================
   ORTA — веб-магазин покупателя: токены, основа и общие компоненты.

   Владелец файла — каркас магазина (apps/shop). Области пишут свои
   файлы рядом (catalog.css, cart.css, account.css, auth.css) и берут
   отсюда токены и компоненты; цвет в правилах областей — только
   var(--…), без литералов.

   Токены сняты с промо-сайта (website/css/style.css, блок токенов), а
   тот — с mobile/lib/core/theme/app_colors.dart; имена в комментариях —
   имена там. Дописано то, чего на сайте нет, а у приложения есть
   (map/design.md §6): --star, --stat-tile, --on-danger, --scrim,
   --accent-soft (песок, не шалфей), --tag (шалфей), --stepper-inset.

   Правила бренда, которые держат токены:
   - карточка на ступень СВЕТЛЕЕ страницы, край — волосяная рамка и
     тёплая тень днём; ночью тени нет, остаётся рамка;
   - золото — только заливкой и только с тёмной подписью;
   - хвоя ночью не видна: текст и значки — шалфей (--accent), кнопки —
     лесная заливка (--accent-fill);
   - ссылка в тексте — вес 600 и подчёркивание: хвоя к тексту 1.3:1.

   Тёмная тема объявлена ДВАЖДЫ — для системной (prefers-color-scheme)
   и для выбранной вручную (data-theme на <html>, ставит shop/js/theme.js).
   Без препроцессора общий блок не вынести: любую правку — в оба.

   CSP магазина запрещает inline-стили (apps/shop/middleware.py): всё
   оформление — здесь и в файлах областей, в разметке style="…" нет.

   :hover — только внутри @media (hover: hover), во всех файлах
   магазина: на телефоне наведения нет, и браузер «залипает» в нём после
   касания — кнопка остаётся подсвеченной, будто нажата. Отклик на
   касание — :active. Сторожит apps/shop/tests/test_frontend.py.
   ============================================================ */

/* ---------- Шрифты ----------
   Те же файлы, что у кабинета и лендинга (static/brand/fonts: WOFF2 без
   подмножеств — казахские буквы, ₸ и № на месте). Начертаний ровно пять,
   как в приложении. Google Fonts не подключаем. */

@font-face {
  font-family: "Golos Text";
  src: url("../../brand/fonts/golos-text-400.a3d0edeb8c7a.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Golos Text";
  src: url("../../brand/fonts/golos-text-500.623097474b1d.woff2") format("woff2");
  font-weight: 500 600;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Onest";
  src: url("../../brand/fonts/onest-500.fecca46ec6ec.woff2") format("woff2");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Onest";
  src: url("../../brand/fonts/onest-600.f2ace7a4829d.woff2") format("woff2");
  font-weight: 600;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Onest";
  src: url("../../brand/fonts/onest-700.c79e2ccbd084.woff2") format("woff2");
  font-weight: 700 800;
  font-style: normal;
  font-display: swap;
}

/* ---------- Токены: светлая тема ---------- */

:root {
  /* бренд — одинаков в обеих темах */
  --pine: #02361B;            /* pine */
  --pine-raised: #0B4527;     /* pineRaised: светлый край баннера */
  --pine-soft: #1D5A3B;       /* pineSoft: плашки на хвое днём */
  --pine-plate: #153A30;      /* pinePlate */
  --gold: #F3BB5E;            /* gold: только заливкой */
  --on-gold: #0B1D17;         /* onGold: 10:1 на золоте */
  --on-pine: #FFFFFF;         /* onBrand */
  --on-pine-muted: #B8C0BA;   /* onBrandMuted: 7.3:1 на pine */
  --on-pine-line: rgba(255, 255, 255, .4);
  --on-pine-rule: rgba(255, 255, 255, .14);
  --slogan-on-pine: #E7C27A;  /* sloganDark: 8:1 на хвое */
  --gold-ring: rgba(243, 187, 94, .35);
  --tab-shadow: 0 -6px 24px rgba(42, 33, 23, .06);  /* таббар — лист с тенью вверх */

  /* поверхности */
  --paper: #FDF9F3;           /* paper: страница */
  --surface: #FFFDF9;         /* surface: карточки */
  --surface-raised: #FFFFFF;  /* surfaceRaised */
  --surface-alt: #F6F0E6;     /* surfaceAlt: поля ввода, дорожка степпера */
  --surface-high: #EEE7DB;    /* surfaceHigh: скелетон, подложка фото */
  --line: #ECE5D9;            /* line: волосяные рамки — декор */
  --outline: #86817A;         /* outline: граница поля, 3.7:1 */

  /* текст */
  --ink: #0B1D17;             /* text */
  --ink-2: #505153;           /* textSecondary: 7.6:1 */
  --ink-3: #646361;           /* textMuted: 5.7:1 */
  --slogan: #8F6728;          /* slogan: 4.8:1 — только на paper и surface */

  /* акцент покупателя */
  --accent: var(--pine);      /* текст, ссылки, значки */
  --accent-fill: var(--pine); /* заливка главной кнопки */
  --accent-fill-hover: var(--pine-raised);
  --on-accent: #FFFFFF;       /* 13.6:1 */
  /* primaryContainer приложения: песок с хвоей — плашка «в работе»,
     аватар, «наверх». На сайте этим именем назван шалфей; у магазина
     шалфей — --tag. */
  --accent-soft: #F2E9DA;
  --on-accent-soft: var(--pine);
  --tag: #E4ECDC;             /* шалфей: FeatureTag, метки */
  --on-tag: var(--pine);
  --brand-mark: var(--pine);  /* знак и надпись днём — хвоя */

  /* бежевые плашки */
  --sand: #F2E9DA;            /* sand: кружки под значками */
  --stat-tile: #F9F0E3;       /* statTile: плитки цифр площадки */
  --ornament-page: #F9F0E3;   /* ornament: орнамент-подложка */
  --tint-1: #F2E9DA;          /* круги категорий: песок */
  --tint-2: #FBE7D6;          /* персик */
  --tint-3: #F7E1DA;          /* роза */
  --tint-4: #E4ECDC;          /* шалфей */
  --tint-5: #F5ECCC;          /* топлёное масло */

  /* баннер */
  --banner-from: var(--pine-raised);
  --banner-to: var(--pine);
  --banner-plate: var(--pine-soft);
  --banner-ornament: .16;

  /* семантика — отдельно от бренда */
  --success: #27704A;
  --success-bg: #E3EFE6;
  --warning: #8A6200;
  --warning-bg: #F7EDD3;
  --danger: #B8352A;          /* ещё и заливка плашки скидки */
  --danger-bg: #FBE6E1;
  --on-danger: #FFFFFF;       /* подпись на заливке --danger: 5.9:1 */
  --star: #B57B10;            /* звезда рейтинга: 3.2:1, не макетное золото */

  /* поверх фото */
  --scrim: rgba(0, 0, 0, .45);      /* photoScrim */
  --scrim-top: rgba(0, 0, 0, .40);  /* photoScrimTop */
  --on-photo: #FFFFFF;

  /* степпер: вставка с числом */
  --stepper-inset: #FFFFFF;

  /* карточки и полосы */
  --card-bg: var(--surface);
  --card-shadow: 0 3px 12px rgba(42, 33, 23, .06);  /* shadow #2A2117, 6 % */
  --tab-bg: var(--paper);
  --header-bg: rgba(253, 249, 243, .97);

  --r-btn: 10px;
  --r-field: 10px;
  --r-chip: 14px;
  --r-card: 16px;
  --r-banner: 20px;
  --r-pill: 999px;

  --font-head: "Onest", "Golos Text", system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
  --font-body: "Golos Text", system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;

  --header-h: 72px;
  --tabbar-h: 64px;
  --tap: 44px;                /* минимальная зона нажатия */

  color-scheme: light;
}

/* ---------- Токены: тёмная тема ----------
   «Кость с выключенным светом», а не графит. Два одинаковых блока —
   системная и выбранная вручную. */

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --paper: #15120E;           /* paperDark */
    --surface: #1F1B16;         /* surfaceDark */
    --surface-raised: #29241D;
    --surface-alt: #29241D;     /* surfaceAltDark */
    --surface-high: #332D25;    /* surfaceHighDark */
    --line: #3B352C;            /* lineDark */
    --outline: #8F8679;         /* outlineDark */
    --ink: #F7F1E7;             /* textDark: 16.6:1 */
    --ink-2: #D0C7B9;           /* textSecondaryDark */
    --ink-3: #A89F91;           /* textMutedDark: 7.1:1 */
    --slogan: #E7C27A;          /* sloganDark: 11:1 */
    --accent: #B5DCCB;          /* buyerDark: 12.5:1 */
    --accent-fill: #21704A;     /* buyerFillDark: белая подпись 6:1 */
    --accent-fill-hover: #1D5A3B;
    --accent-soft: #153A30;     /* pinePlate */
    --on-accent-soft: #CDE9DA;  /* onBuyerSoftDark */
    --tag: #24302A;
    --on-tag: #B5DCCB;
    --brand-mark: #F7F1E7;      /* знак ночью — цвет текста */
    --sand: #332D25;            /* sandPlate ночью */
    --stat-tile: #2F2920;       /* sandDark */
    --ornament-page: #3B2F1F;   /* ornamentDark */
    --tint-1: #2F2920;
    --tint-2: #38281D;
    --tint-3: #382625;
    --tint-4: #24302A;
    --tint-5: #353020;
    --banner-from: #153A30;     /* ночью баннер светлее */
    --banner-to: #0B4527;
    --banner-plate: #02361B;
    --banner-ornament: .12;
    --success: #5CC489;
    --success-bg: #263529;
    --warning: #E0B44F;
    --warning-bg: #3B3321;
    --danger: #F2836F;
    --danger-bg: #3F2B26;
    --on-danger: #15120E;       /* 7.4:1 на #F2836F */
    --star: #E7AF5D;            /* starDark */
    --stepper-inset: #332D25;   /* raisedInset */
    --card-bg: var(--surface);
    --card-shadow: none;
    --tab-shadow: none;
    --tab-bg: #1F1B16;
    --header-bg: rgba(21, 18, 14, .86);
    color-scheme: dark;
  }
}

:root[data-theme="dark"] {
  --paper: #15120E;
  --surface: #1F1B16;
  --surface-raised: #29241D;
  --surface-alt: #29241D;
  --surface-high: #332D25;
  --line: #3B352C;
  --outline: #8F8679;
  --ink: #F7F1E7;
  --ink-2: #D0C7B9;
  --ink-3: #A89F91;
  --slogan: #E7C27A;
  --accent: #B5DCCB;
  --accent-fill: #21704A;
  --accent-fill-hover: #1D5A3B;
  --accent-soft: #153A30;
  --on-accent-soft: #CDE9DA;
  --tag: #24302A;
  --on-tag: #B5DCCB;
  --brand-mark: #F7F1E7;
  --sand: #332D25;
  --stat-tile: #2F2920;
  --ornament-page: #3B2F1F;
  --tint-1: #2F2920;
  --tint-2: #38281D;
  --tint-3: #382625;
  --tint-4: #24302A;
  --tint-5: #353020;
  --banner-from: #153A30;
  --banner-to: #0B4527;
  --banner-plate: #02361B;
  --banner-ornament: .12;
  --success: #5CC489;
  --success-bg: #263529;
  --warning: #E0B44F;
  --warning-bg: #3B3321;
  --danger: #F2836F;
  --danger-bg: #3F2B26;
  --on-danger: #15120E;
  --star: #E7AF5D;
  --stepper-inset: #332D25;
  --card-bg: var(--surface);
  --card-shadow: none;
  --tab-shadow: none;
  --tab-bg: #1F1B16;
  --header-bg: rgba(21, 18, 14, .86);
  color-scheme: dark;
}

/* ---------- Основа ----------
   Кегли — в rem, а не px: крупный шрифт браузера (настройка «размер
   шрифта», не масштаб) обязан увеличивать текст. Раскладка держится
   переносами, min-width: 0 и min-height вместо height. */

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

/* Отступы прокрутки — по НАСТОЯЩЕЙ высоте липкой шапки и нижнего
   таббара: их меряет shop.js (--header-live, --tabbar-live). Элемент в
   фокусе браузер прокручивает в видимую область за вычетом этих
   отступов; по постоянным 72 px он вставал под шапку (её высота — от
   119 px на телефоне до 260 px при крупном шрифте), а под таббар —
   целиком: без отступа снизу браузер считал полосу под ним видимой и
   не прокручивал вовсе (WCAG 2.4.11). Без скрипта — запас в rem: растёт
   со шрифтом, как и сама шапка. */
html {
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
  scroll-padding-top: calc(var(--header-live, 7.5rem) + 16px);
}
@media (max-width: 767px) {
  html {
    scroll-padding-bottom: calc(
      var(--tabbar-live, calc(var(--tabbar-h) + env(safe-area-inset-bottom, 0px))) + 8px
    );
  }
}

@media (prefers-reduced-motion: no-preference) {
  html { scroll-behavior: smooth; }
}

body {
  margin: 0;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  background: var(--paper);
  color: var(--ink-2);
  font-family: var(--font-body);
  font-size: 1rem;
  line-height: 1.45;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow-wrap: break-word;
}

h1, h2, h3, h4, p, ul, ol, figure, blockquote, dl, dd { margin: 0; }
ul, ol { padding: 0; list-style: none; }
img, svg { display: block; max-width: 100%; }
img { height: auto; }
a { color: inherit; }
button, input, select, textarea { font: inherit; color: inherit; }
button { cursor: pointer; }
[hidden] { display: none !important; }

/* Только со скриптом. Метку js на <html> ставит theme.js синхронно в
   <head>, поэтому место под [data-js-only] отведено с первой отрисовки:
   раньше их открывал отложенный shop.js, и на медленной сети «Показать
   пароль» вырастал уже на экране — «Создать аккаунт» уезжал из-под
   пальца. Без скрипта элемента нет вовсе. Видимым и нажимаемым его
   делает data-js-live — метка того скрипта, что повесил обработчик
   (shop.js, auth.js): не загрузился он — место пустое, а не мёртвая
   кнопка. hidden у таких элементов не ставят: правило выше с !important
   его не отпустило бы. */
:root:not(.js) [data-js-only] { display: none !important; }
[data-js-only]:not([data-js-live]) { visibility: hidden; }

h1, h2, h3, h4 {
  font-family: var(--font-head);
  color: var(--ink);
  line-height: 1.2;
  text-wrap: balance;
}

::selection { background: var(--gold); color: var(--on-gold); }

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 6px;
}

/* Ссылка в тексте: вес и подчёркивание — один цвет её не отличает. */
.prose a,
.link {
  color: var(--accent);
  font-weight: 600;
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
}
@media (hover: hover) {
  .link:hover,
  .prose a:hover { text-decoration-thickness: 2px; }
}

.sr-only,
.visually-hidden {
  position: absolute !important;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* Спрайт значков: в документе, но не на экране. */
.sprite {
  position: absolute;
  width: 0;
  height: 0;
  overflow: hidden;
}

.icon {
  width: 24px;
  height: 24px;
  flex: none;
}

/* Спрятана сдвигом на собственную высоту, а не на фиксированные
   пиксели: при крупном шрифте ссылка выше, и низ её торчал бы над
   шапкой. */
.skip-link {
  position: absolute;
  left: calc(16px + env(safe-area-inset-left, 0px));
  top: 0;
  transform: translateY(-110%);
  z-index: 100;
  padding: 12px 18px;
  border-radius: var(--r-btn);
  background: var(--accent-fill);
  color: var(--on-accent);
  font-family: var(--font-head);
  font-weight: 600;
  text-decoration: none;
}
.skip-link:focus { top: 12px; transform: none; }

/* Боковое поле — 16 / 24 / 32 px плюс вырез экрана: с viewport-fit=cover
   (base.html) страница идёт под вырез и закруглённые углы, и в альбомной
   ориентации iPhone шапка и текст ушли бы под камеру. Берётся больший из
   двух вырезов с обеих сторон: поле симметрично, и содержимое не заходит
   под вырез, с какой бы стороны он ни был. Без выреза env() — ноль. */
.container {
  --gutter: 16px;
  width: min(1200px, 100% - 2 * (var(--gutter) + max(env(safe-area-inset-left, 0px), env(safe-area-inset-right, 0px))));
  margin-inline: auto;
}
@media (min-width: 768px) {
  .container { --gutter: 24px; }
}
@media (min-width: 1200px) {
  .container { --gutter: 32px; }
}

.main {
  flex: 1 0 auto;
  padding-block: 16px 32px;
}
.main:focus { outline: none; }

/* ---------- Типографика (роли app_theme.dart) ---------- */

.page { display: grid; gap: 20px; }
.page__title {           /* displaySmall: заголовок вкладки */
  font-size: 1.75rem;
  font-weight: 700;
  letter-spacing: -.02em;
  line-height: 1.15;
}
.page__lead { color: var(--ink-2); font-size: 1rem; }
.section-title {         /* titleLarge: заголовок секции */
  font-size: 1.125rem;
  font-weight: 600;
  line-height: 1.25;
}
.title-md { font-family: var(--font-head); font-size: .9375rem; font-weight: 600; color: var(--ink); line-height: 1.3; }
.body-sm { font-size: .78125rem; line-height: 1.35; color: var(--ink-3); }
.muted { color: var(--ink-3); }
.overline {
  font-size: .71875rem;
  font-weight: 500;
  line-height: 1.35;
  letter-spacing: .2em;
  text-transform: uppercase;
  color: var(--ink-2);
}
.num,
.price { font-variant-numeric: tabular-nums; }
.price {
  font-family: var(--font-head);
  font-weight: 700;
  color: var(--ink);
  white-space: nowrap;
}
.price--old {
  font-weight: 500;
  color: var(--ink-3);
  text-decoration: line-through;
}

@media (min-width: 1024px) {
  .page__title { font-size: 2rem; }
}

/* ---------- Логотип ---------- */

.brand {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  min-height: var(--tap);
  color: var(--brand-mark);
  text-decoration: none;
  flex: 0 1 auto;
  min-width: 0;
}
.brand__mark { width: 34px; height: 34px; flex: none; }
.brand__text { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.brand__word { width: 80px; height: 15px; flex: none; }
.brand__slogan {
  overflow: hidden;
  color: var(--slogan);
  font-size: .75rem;
  font-weight: 500;
  line-height: 1.2;
  letter-spacing: .01em;
  white-space: nowrap;
  text-overflow: ellipsis;
}
.brand--on-pine { color: var(--on-pine); }
.brand--on-pine .brand__mark { color: var(--gold); }
.brand--on-pine .brand__slogan { color: var(--slogan-on-pine); }

@media (min-width: 1024px) {
  .brand__mark { width: 38px; height: 38px; }
  .brand__word { width: 88px; height: 16.5px; }
}

/* ---------- Шапка ----------
   Липкая, полупрозрачная кость; линия снизу — после прокрутки (shop.js).
   До 768: «логотип · значки», плашка поиска вторым рядом; вкладки —
   нижним таббаром. С 768: логотип · поиск · значки в один ряд и строка
   вкладок под ними — если ряд не уже 42em. В разметке (_header.html)
   значки стоят раньше поиска — в порядке телефона, чтобы Tab шёл по
   рядам; на широкой шапке место поиску между ними возвращает order. Порог в em шапки, не в px
   экрана: при крупном шрифте логотипу, значкам с «Войти» и плашке в
   одном ряду тесно и на 1280, и плашка уходит вторым рядом, как на
   телефоне, вместо того чтобы сжаться до буквы города. */

.header {
  position: sticky;
  top: 0;
  z-index: 50;
  background: var(--header-bg);
  -webkit-backdrop-filter: saturate(1.4) blur(14px);
  backdrop-filter: saturate(1.4) blur(14px);
  border-bottom: 1px solid transparent;
  transition: border-color .2s ease;
}
.header.is-scrolled { border-bottom-color: var(--line); }

/* Низкий экран — телефон в альбомной ориентации, 320–480 px в высоту:
   липкая шапка в два ряда (~119 px) и таббар съедали бы больше половины
   экрана. Шапка уезжает вместе со страницей, и отступ прокрутки на её
   высоту больше не нужен. */
@media (max-height: 480px) {
  .header { position: static; }
  html { scroll-padding-top: 16px; }
}

.header__inner {
  container: header / inline-size;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 16px;
  padding-block: 8px;
}
.header__inner > .brand { order: 1; }
.header__actions {
  order: 2;
  display: flex;
  align-items: center;
  gap: 4px;
  margin-left: auto;
}
.searchbar { order: 3; flex: 1 1 100%; }

@media (min-width: 768px) {
  /* Ряды переносятся и здесь, но в один ряд шапку держит основа плашки:
     она контейнер (container-type), и её собственная ширина по
     содержимому — ноль. Во второй ряд её отправляет только правило ниже. */
  .header__inner { min-height: var(--header-h); gap: 8px 24px; }
  /* Сжимается поиск, а не логотип: слоган не должен уходить в многоточие. */
  .header__inner > .brand { flex-shrink: 0; }
  .searchbar { order: 2; flex: 1 1 auto; max-width: 640px; margin-inline: auto; }
  .header__actions { order: 3; margin-left: 0; }
}
@container header (max-width: 42em) {
  .header__inner > .searchbar { order: 3; flex: 1 1 100%; max-width: none; margin-inline: 0; }
  .header__actions { order: 2; margin-left: auto; }
}
/* Узкий ряд (320 px при обычном шрифте, телефон при крупном): логотип
   со слоганом (~147 px) и три значка (140 px) в ряд не вставали, значки
   уходили отдельным вторым рядом, и липкая шапка с таббаром занимали
   почти половину экрана 320×640. Слоган прячется — знак и слово ORTA
   остаются, имя ссылке даёт aria-label. Порог в em: слоган растёт со
   шрифтом, значки — нет. */
@container header (max-width: 19em) {
  .brand__slogan { display: none; }
}

.icon-btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  min-width: var(--tap);
  min-height: var(--tap);
  padding: 0 10px;
  border: 0;
  border-radius: 12px;
  background: transparent;
  color: var(--ink);
  text-decoration: none;
}
@media (hover: hover) {
  .icon-btn:hover { background: var(--surface-alt); }
}
.icon-btn:active { background: var(--surface-alt); }
.icon-btn .icon { width: 24px; height: 24px; }
.icon-btn[aria-current="page"] { color: var(--accent); }
.header__login-text {
  font-family: var(--font-head);
  font-size: .9375rem;
  font-weight: 600;
}
@media (max-width: 479px) {
  .header__login-text { display: none; }
}

/* Золотой счётчик с тёмной цифрой, «99+» (brand_buttons.dart). */
.counter {
  position: absolute;
  top: 2px;
  right: 0;
  min-width: 20px;
  height: 20px;
  padding: 0 5px;
  border-radius: var(--r-pill);
  background: var(--gold);
  color: var(--on-gold);
  font-family: var(--font-head);
  font-size: .6875rem;
  font-weight: 700;
  line-height: 20px;
  text-align: center;
  font-variant-numeric: tabular-nums;
}

/* Плашка «город | поиск» (home_header.dart): белая, радиус 14, рамка
   line; город — до двух пятых, поиск — остальное.

   Узкая плашка — уже 17em — раскладывается в два ряда: город целиком
   сверху, поиск под ним. Порог в em контейнера, а не в px экрана: он
   растёт вместе со шрифтом. При обычном шрифте плашка в один ряд и на
   320 px, а при 200 % на 360 px от города в ряду оставалась одна буква
   («В.»). Предел ширины города — в rem по той же причине.
   Плашка переносит ряды всегда; в один ряд её держат основы детей:
   40 % + 0 + кнопка никогда не больше ширины. */
.searchbar {
  container: searchbar / inline-size;
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  min-height: 50px;
  border: 1px solid var(--line);
  border-radius: var(--r-chip);
  background: var(--surface-raised);
  overflow: hidden;
}
.searchbar:focus-within { border-color: var(--accent); }
/* Фокус в самом поле — кольцо 2 px вокруг плашки, как у всего магазина
   (outline поля выключен ниже: внутри плашки его не видно). У ссылки
   города и кнопки кольцо своё, внутрь: снаружи его срезал бы
   overflow плашки. */
.searchbar:has(.searchbar__input:focus-visible) {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.searchbar__city:focus-visible,
.searchbar__submit:focus-visible { outline-offset: -3px; }
.searchbar__city {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  flex: 0 1 40%;
  max-width: 13.75rem;     /* 220 px при обычном шрифте */
  min-width: 0;
  padding: 0 12px;
  border-right: 1px solid var(--line);
  color: var(--ink);
  font-size: .875rem;
  font-weight: 500;
  text-decoration: none;
}
.searchbar__city .icon { width: 20px; height: 20px; flex: none; color: var(--accent); }
.searchbar__city-name { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
@media (hover: hover) {
  .searchbar__city:hover { background: var(--surface-alt); }
}

/* Поле и подсказка — в одной клетке сетки. Подсказка переносится по
   словам, и клетка — а с ней поле — растёт по её высоте. При обычном
   шрифте на телефоне две её строки встают в те же 50 px плашки; при
   крупном плашка выше, но подсказка видна целиком. Сам placeholder
   прозрачный: он остаётся ради :placeholder-shown и диктора, а видна
   подсказка — пока поле пустое. */
.searchbar__field {
  display: grid;
  flex: 1 1 0;
  min-width: 0;
}
.searchbar__input,
.searchbar__hint { grid-area: 1 / 1; }
.searchbar__input {
  min-width: 0;
  padding: 0 12px;
  border: 0;
  background: transparent;
  color: var(--ink);
  font-size: 1rem;
}
.searchbar__input::placeholder { color: transparent; opacity: 1; }
.searchbar__input:focus { outline: none; }
.searchbar__hint {
  align-self: center;
  min-width: 0;
  padding: 6px 12px;
  color: var(--ink-3);
  font-size: .875rem;      /* bodyMedium подсказки приложения */
  line-height: 1.25;
  pointer-events: none;
}
.searchbar__input:not(:placeholder-shown) + .searchbar__hint { visibility: hidden; }

@container searchbar (max-width: 17em) {
  .searchbar__city {
    flex: 1 1 100%;
    max-width: none;
    min-height: var(--tap);
    padding-block: 6px;
    border-right: 0;
    border-bottom: 1px solid var(--line);
  }
  /* Ненулевая основа — иначе поле нулевой ширины осталось бы в ряду
     города, а кнопка ушла бы во второй ряд одна. */
  .searchbar__field { flex-basis: 50%; }
  .searchbar__city-name { white-space: normal; }
}
.searchbar__submit {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: var(--tap);
  min-height: var(--tap);
  border: 0;
  background: transparent;
  color: var(--ink);
}
@media (hover: hover) {
  .searchbar__submit:hover { color: var(--accent); }
}

/* Строка вкладок под шапкой (≥768): активная — акцент и вес, без таблетки. */
.tabs { display: none; }
@media (min-width: 768px) {
  .tabs { display: block; }
}
.tabs__inner {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 8px;
  padding-bottom: 6px;
}
.tabs__link {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap);
  padding: 0 12px;
  border-radius: var(--r-btn);
  color: var(--ink-2);
  font-family: var(--font-head);
  font-size: .9375rem;
  font-weight: 500;
  text-decoration: none;
}
@media (hover: hover) {
  .tabs__link:hover { color: var(--ink); background: var(--surface-alt); }
}
.tabs__link[aria-current="page"] { color: var(--accent); font-weight: 600; }

/* ---------- Нижний таббар (до 768) ----------
   Лист со скруглёнными верхними углами и тенью вверх; активная вкладка —
   акцент (buyer_tabs.dart). Под ним — запас снизу у .main. */

.tabbar {
  position: fixed;
  inset: auto 0 0 0;
  z-index: 40;
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  /* Снизу — полоска «домой», по бокам — вырез в альбомной ориентации
     (viewport-fit=cover в base.html). */
  padding: 6px calc(4px + env(safe-area-inset-right, 0px)) calc(6px + env(safe-area-inset-bottom, 0px)) calc(4px + env(safe-area-inset-left, 0px));
  border-radius: 24px 24px 0 0;
  background: var(--tab-bg);
  box-shadow: var(--tab-shadow);
  border-top: 1px solid var(--line);
}
.tabbar__item {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  min-height: 52px;
  padding: 4px 2px;
  border-radius: 12px;
  color: var(--ink-3);
  /* Подпись растёт со шрифтом, но не дальше, чем самая длинная из них
     («Для своих», 58 px при 11 px и весе 600) встаёт в пятую часть
     экрана, и не больше чем в 1,3 раза, — правило _labelScaleLimit
     приложения (buyer_shell.dart). Без предела при 200 % на 360 px от
     подписей оставалось по три буквы, а «Войти» не влезало в ячейку.
     Нижняя граница — обычные 11 px: меньше обычного подпись не бывает. */
  font-size: min(.6875rem, max(11px, min(14.3px, (20vw - 8px) * .189)));
  font-weight: 500;
  line-height: 1.2;
  letter-spacing: .02em;
  text-align: center;
  text-decoration: none;
  min-width: 0;
}
/* Подпись в одну строку: если не встала и при пределе выше (очень узкий
   экран) — многоточие, а не слово, разорванное посередине. Диктор читает
   подпись целиком. */
.tabbar__item > span:not(.tabbar__icon) {
  max-width: 100%;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  overflow-wrap: normal;
}
.tabbar__item[aria-current="page"] { color: var(--accent); font-weight: 600; }
/* Наведения на телефоне нет — касание отвечает тоном под пальцем. */
.tabbar__item:active { background: var(--surface-alt); }
.tabbar__icon { position: relative; display: inline-flex; }
.tabbar__icon .counter { top: -6px; right: -12px; }

/* Запас под таббар растёт вместе со шрифтом: таббар выше при крупном
   шрифте, и фиксированный запас прятал бы под ним конец страницы. */
@media (max-width: 767px) {
  .main { padding-bottom: calc(var(--tabbar-h) + 2.5rem + env(safe-area-inset-bottom, 0px)); }
}
@media (min-width: 768px) {
  .tabbar { display: none; }
}

/* ---------- Подвал ----------
   Хвойный градиент, как баннеры; ночью — тот же светлый градиент. */

.footer {
  padding: 48px 0 28px;
  background: linear-gradient(160deg, var(--banner-from), var(--banner-to));
  color: var(--on-pine-muted);
  font-size: .9375rem;
}
/* Запас под таббар стоит после базового правила, а не рядом с запасом
   .main: та же специфичность, и раньше объявленный запас перекрывался
   `padding` выше — переключатель темы в конце подвала уходил под таббар. */
@media (max-width: 767px) {
  .footer { padding-bottom: calc(var(--tabbar-h) + 2.5rem + env(safe-area-inset-bottom, 0px)); }
}
.footer__grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 28px;
}
@media (min-width: 560px) {
  .footer__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .footer__brand { grid-column: 1 / -1; }
}
@media (min-width: 1024px) {
  .footer__grid { grid-template-columns: minmax(0, 1.4fr) repeat(3, minmax(0, 1fr)); }
  .footer__brand { grid-column: auto; }
}
.footer__brand .brand__mark { width: 40px; height: 40px; }
.footer__brand .brand__word { width: 92px; height: 17.5px; }
.footer__desc { max-width: 22em; margin-top: 16px; }
.footer__title {
  margin-bottom: 12px;
  color: var(--on-pine);
  font-size: .9375rem;
  font-weight: 600;
}
/* Зоны нажатия — 44 px, как у всего магазина (контракт §6): ссылки
   подвала стоят столбцом, и при 36 px палец попадал в соседнюю. */
.footer__links { display: grid; gap: 4px; }
.footer__links a {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap);
  color: var(--on-pine-muted);
  text-decoration: none;
}
@media (hover: hover) {
  .footer__links a:hover { color: var(--on-pine); text-decoration: underline; text-underline-offset: 3px; }
}
.footer__contacts li { display: grid; gap: 2px; }
.footer__contacts span { font-size: .8125rem; }
.footer__contacts a { color: var(--on-pine); font-weight: 500; }
.footer a:focus-visible,
.footer button:focus-visible { outline-color: var(--gold); }
.footer__bottom {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 12px 24px;
  margin-top: 40px;
  padding-top: 20px;
  border-top: 1px solid var(--on-pine-rule);
  font-size: .84375rem;
}

/* Переключатель темы: система / светлая / тёмная. */
.theme-switch {
  display: inline-flex;
  flex-wrap: wrap;
  gap: 4px;
  padding: 4px;
  border: 1px solid var(--on-pine-rule);
  border-radius: var(--r-pill);
}
.theme-switch__option {
  min-height: var(--tap);
  padding: 0 14px;
  border: 0;
  border-radius: var(--r-pill);
  background: transparent;
  color: var(--on-pine-muted);
  font-size: .8125rem;
  font-weight: 500;
}
@media (hover: hover) {
  .theme-switch__option:hover { color: var(--on-pine); }
}
.theme-switch__option[aria-pressed="true"] {
  background: var(--banner-plate);
  color: var(--on-pine);
}

/* ---------- Кнопки ----------
   Главная — прямоугольник 50 px, радиус 10, заливка акцента
   (ElevatedButton приложения). Золотая — только на хвое, пилюлей. */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  min-height: 50px;
  padding: 0 22px;
  border: 1px solid transparent;
  border-radius: var(--r-btn);
  background: transparent;
  color: var(--ink);
  font-family: var(--font-head);
  font-size: .9375rem;
  font-weight: 600;
  line-height: 1.2;
  text-align: center;
  text-decoration: none;
  transition: background-color .18s ease, border-color .18s ease, color .18s ease;
}
.btn:active { transform: translateY(1px); }
.btn .icon { width: 20px; height: 20px; }
.btn--sm { min-height: var(--tap); padding: 0 16px; font-size: .875rem; }
.btn--lg { min-height: 56px; padding: 0 26px; font-size: 1rem; border-radius: 14px; }
.btn--block { width: 100%; }

.btn--primary { background: var(--accent-fill); color: var(--on-accent); }
.btn--ghost { border-color: var(--outline); color: var(--ink); }
.btn--danger { border-color: var(--danger); color: var(--danger); }
.btn--gold { border-radius: var(--r-pill); background: var(--gold); color: var(--on-gold); }
.btn--gold:focus-visible { outline-color: var(--gold); }

/* Наведение — только у кнопки, которую можно нажать: выключенная и
   занятая (замок отправки, shop.js) не должны «откликаться». Условие —
   в :where(), чтобы вес остался прежним (0,2,0): наведение своих кнопок
   в файлах разделов (.guest-banner__secondary:hover в catalog.css)
   перебивает вариант порядком, а с весом (0,4,0) проигрывало — белая
   подпись на светлом фоне. */
@media (hover: hover) {
  .btn--primary:where(:not(:disabled):not([aria-disabled="true"])):hover { background: var(--accent-fill-hover); }
  .btn--ghost:where(:not(:disabled):not([aria-disabled="true"])):hover { background: var(--surface-alt); }
  .btn--danger:where(:not(:disabled):not([aria-disabled="true"])):hover { background: var(--danger-bg); }
  .btn--gold:where(:not(:disabled):not([aria-disabled="true"])):hover { box-shadow: 0 0 0 4px var(--gold-ring); }
}

.btn:disabled,
.btn[aria-disabled="true"] {
  opacity: .55;
  cursor: not-allowed;
  transform: none;
}

/* Занята — форма ушла и ждёт ответа (замок одной отправки, shop.js).
   Подпись прозрачная, но остаётся в потоке: кнопка не меняет размер и
   ничего под ней не прыгает. Кольцо — поверх, цветом подписи варианта
   (у прозрачной подписи currentColor не взять). Бледность выключенной
   кнопки занятой не нужна: она не «нельзя», а «уже жмём». Вращение —
   только если человек не просил убрать движение. */
.btn.is-busy {
  --busy-ring: var(--ink);
  position: relative;
}
.btn--primary.is-busy { --busy-ring: var(--on-accent); }
.btn--danger.is-busy { --busy-ring: var(--danger); }
.btn--gold.is-busy { --busy-ring: var(--on-gold); }
.btn.is-busy,
.btn.is-busy * { color: transparent; }
.btn.is-busy[aria-disabled="true"] { opacity: 1; cursor: progress; }
.btn.is-busy::after {
  content: "";
  position: absolute;
  inset: 0;
  width: 1.25rem;
  height: 1.25rem;
  margin: auto;
  border: 2px solid var(--busy-ring);
  border-right-color: transparent;
  border-radius: 50%;
}
@keyframes shop-spin {
  to { transform: rotate(1turn); }
}
@media (prefers-reduced-motion: no-preference) {
  .btn.is-busy::after { animation: shop-spin .8s linear infinite; }
}
/* Кнопка-ссылка и пункт меню без кольца: форма с aria-busy — курсор
   ожидания на всём, чем её отправляют. */
form[aria-busy="true"] button,
.btn-link[aria-disabled="true"] { cursor: progress; }

/* Кнопка-ссылка без рамки: «Изменить», «Смотреть все». */
.btn-link {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: var(--tap);
  padding: 0;
  border: 0;
  background: none;
  color: var(--accent);
  font-family: var(--font-head);
  font-size: .875rem;
  font-weight: 600;
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
}
/* Как ссылка в тексте: наведение утолщает черту, касание — бледнеет. */
@media (hover: hover) {
  .btn-link:hover { text-decoration-thickness: 2px; }
}
.btn-link:active { opacity: .7; }

/* «Показать пароль» под полем — у левого края, а не растянутой во всю
   колонку сетки поля. Здесь, а не в auth.css: кнопка есть и в кабинете
   («Пароль», «Почта»), где auth.css не подключён. */
.auth-reveal { justify-self: start; }

/* ---------- Поля ----------
   Заливка surfaceAlt, рамка outline (3:1 по WCAG 1.4.11); в фокусе —
   рамка акцента (app_theme.dart) и кольцо 2 px, как у всего магазина
   (контракт §6). Одна рамка 1.6 px в приложении заметна на экране
   телефона в руке, а на сайте поле адреса или промокода в фокусе почти
   не отличалось от соседних. */

.field { display: grid; gap: 6px; }
.field__label {
  color: var(--ink);
  font-family: var(--font-head);
  font-size: .875rem;
  font-weight: 600;
}
.field input:not([type="checkbox"]):not([type="radio"]),
.field select,
.field textarea {
  width: 100%;
  min-height: 50px;
  padding: 0 14px;
  border: 1px solid var(--outline);
  border-radius: var(--r-field);
  background: var(--surface-alt);
  color: var(--ink);
  font-size: 1rem;
  line-height: 1.4;
}
.field textarea { padding: 12px 14px; resize: vertical; min-height: 110px; }
.field input::placeholder,
.field textarea::placeholder { color: var(--ink-3); opacity: 1; }
/* Селекторы фокуса и ошибки — не слабее правила полей выше: с
   `.field input:focus` и `.field.is-invalid input` (на один :not() меньше)
   рамку перебивал серый --outline — красная была только у select.
   Сторожит apps/shop/tests/test_frontend.py. */
.field input:not([type="checkbox"]):not([type="radio"]):focus,
.field select:focus,
.field textarea:focus {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-color: var(--accent);
}
.field__hint { color: var(--ink-3); font-size: .8125rem; }
.field__error { color: var(--danger); font-size: .8125rem; font-weight: 500; }
/* «— не обязательно» в подписи поля (регистрация): тише самой подписи. */
.field__optional { color: var(--ink-3); font-family: var(--font-body); font-weight: 400; }
/* Ошибочное поле — рамка ошибки и внутренняя черта в 1 px: толщина
   отличает его и без цвета. Объявлено после фокуса: в фокусе ошибка
   остаётся видна, а кольцо фокуса — своё. aria-invalid без is-invalid —
   поле, которое сервер пометил, а класс на обёртке не поставил. */
.field.is-invalid input:not([type="checkbox"]):not([type="radio"]),
.field.is-invalid select,
.field.is-invalid textarea,
.field input[aria-invalid="true"]:not([type="checkbox"]):not([type="radio"]),
.field select[aria-invalid="true"],
.field textarea[aria-invalid="true"] {
  border-color: var(--danger);
  box-shadow: inset 0 0 0 1px var(--danger);
}

/* Галочка: подпись — обычный текст рядом с квадратом. */
.check {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  min-height: var(--tap);
  color: var(--ink-2);
  font-size: .9375rem;
}
.check input {
  flex: none;
  width: 22px;
  height: 22px;
  margin: 1px 0 0;
  accent-color: var(--accent-fill);
}

.form { display: grid; gap: 16px; }

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

.card {
  border: 1px solid var(--line);
  border-radius: var(--r-card);
  background: var(--card-bg);
  box-shadow: var(--card-shadow);
}
.card--pad { padding: 16px; }
@media (min-width: 768px) {
  .card--pad { padding: 20px; }
}

/* ---------- Плашка статуса (StatusPill) ----------
   Всегда со словом: цвет никогда не работает один. Тона — имена
   apps.core.badges (ORDER_TONES, PAYMENT_TONES), цвета — бренда:
   «в работе» — мягким акцентом, «ждёт» — внимание, «хорошо» — успех,
   «плохо» — ошибка. */

.pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  max-width: 100%;
  padding: 4px 10px;
  border-radius: 20px;
  font-size: .8125rem;
  font-weight: 500;
  line-height: 1.3;
  background: var(--surface-high);
  color: var(--ink-2);
}
.pill[data-tone="neutral"] { background: var(--surface-high); color: var(--ink-2); }
.pill[data-tone="info"] { background: var(--tag); color: var(--on-tag); }
.pill[data-tone="progress"] { background: var(--accent-soft); color: var(--on-accent-soft); }
.pill[data-tone="success"] { background: var(--success-bg); color: var(--success); }
.pill[data-tone="warning"] { background: var(--warning-bg); color: var(--warning); }
.pill[data-tone="danger"] { background: var(--danger-bg); color: var(--danger); }

/* Плашка скидки на фото: заливка ошибки, «−17 %». */
.discount {
  display: inline-block;
  padding: 3px 7px;
  border-radius: 8px;
  background: var(--danger);
  color: var(--on-danger);
  font-family: var(--font-head);
  font-size: .6875rem;
  font-weight: 700;
  line-height: 1.3;
  white-space: nowrap;
}

/* ---------- Степпер количества ----------
   Пилюля 44 цвета surfaceAlt, «−» и «+» — квадраты 44, число на
   вставке (белая днём). Без JS работает как обычное поле числа. */

.stepper {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap);
  border-radius: var(--r-pill);
  background: var(--surface-alt);
}
.stepper__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--tap);
  height: var(--tap);
  padding: 0;
  border: 0;
  border-radius: var(--r-pill);
  background: transparent;
  color: var(--ink);
}
.stepper__btn .icon { width: 20px; height: 20px; }
.stepper__btn:disabled { color: var(--ink-3); cursor: not-allowed; }
/* Вставка видна 36 px, как в приложении, а зона нажатия поля — 44:
   прозрачная рамка 4 px вокруг неё нажимается вместе с полем, фон
   рисуется только внутри рамки. Радиус 14 снаружи — 10 у вставки. */
.stepper__value {
  width: calc(3.2em + 8px);
  min-width: 48px;
  min-height: var(--tap);
  padding: 0 4px;
  border: 4px solid transparent;
  border-radius: 14px;
  background: var(--stepper-inset);
  background-clip: padding-box;
  color: var(--ink);
  font-family: var(--font-head);
  font-size: .9375rem;
  font-weight: 600;
  text-align: center;
  font-variant-numeric: tabular-nums;
  -moz-appearance: textfield;
  appearance: textfield;
}
.stepper__value::-webkit-outer-spin-button,
.stepper__value::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }

/* ---------- Пустое состояние ---------- */

.empty {
  position: relative;
  display: grid;
  justify-items: center;
  gap: 10px;
  padding: 40px 16px;
  text-align: center;
}
.empty__ornament {
  width: 96px;
  height: 96px;
  color: var(--ornament-page);
}
.empty__title {
  color: var(--ink);
  font-family: var(--font-head);
  font-size: 1.25rem;
  font-weight: 700;
}
.empty__text { max-width: 32em; color: var(--ink-2); }
.empty__actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 10px;
  margin-top: 8px;
}

/* ---------- Хлебные крошки ----------
   Ссылки — 44 px в высоту, как любая зона нажатия магазина: при 32 px
   «Каталог» над карточкой товара на телефоне ловился через раз. */

.breadcrumbs {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0 6px;
  color: var(--ink-3);
  font-size: .8125rem;
}
.breadcrumbs a {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap);
  color: var(--ink-2);
  text-decoration: none;
}
@media (hover: hover) {
  .breadcrumbs a:hover { color: var(--accent); text-decoration: underline; }
}
.breadcrumbs li + li::before { content: "/"; margin-right: 6px; color: var(--line); }
.breadcrumbs [aria-current="page"] { color: var(--ink); font-weight: 500; }

/* ---------- Сетка товаров ----------
   Как productGridDelegate приложения: задана ширина колонки, а не их
   число. 360 → 2, 768 → 3, 1024 → 4, 1280 → 5. */

.grid-products {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 200px), 1fr));
  gap: 16px;
}
@media (max-width: 767px) {
  .grid-products {
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 150px), 1fr));
    gap: 10px;
  }
}
@media (min-width: 768px) and (max-width: 1023px) {
  .grid-products { gap: 12px; }
}

/* ---------- Чипы фильтров (BrandFilterChip) ----------
   Выбранный — заливка акцента; остальные — surface с рамкой line.
   Высота — 44, зона нажатия магазина: у чипа приложения видимые 40, но
   Material добирает касание до 48 невидимым полем, а у ссылки сайта
   зона нажатия — её собственный прямоугольник. */

.chips { display: flex; flex-wrap: wrap; gap: 8px; }
.chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: var(--tap);
  padding: 0 14px;
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  background: var(--surface);
  color: var(--ink);
  font-size: .875rem;
  font-weight: 500;
  text-decoration: none;
  white-space: nowrap;
}
@media (hover: hover) {
  .chip:hover { border-color: var(--outline); }
}
.chip[aria-pressed="true"],
.chip[aria-current="true"],
.chip[aria-current="page"],
.chip.is-active {
  border-color: var(--accent-fill);
  background: var(--accent-fill);
  color: var(--on-accent);
}

/* Метка свойства (FeatureTag): шалфей, значок в акценте. */
.tag {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 12px;
  border-radius: 20px;
  background: var(--tag);
  color: var(--ink);
  font-size: .8125rem;
  font-weight: 500;
}
.tag .icon { width: 16px; height: 16px; color: var(--on-tag); }

/* ---------- Плашка сообщения ---------- */

.messages { display: grid; gap: 8px; margin-bottom: 16px; }
.notice {
  padding: 12px 16px;
  border: 1px solid var(--line);
  border-radius: var(--r-chip);
  background: var(--surface);
  color: var(--ink);
  font-size: .9375rem;
}
.notice[data-tone="success"] { border-color: transparent; background: var(--success-bg); color: var(--success); }
.notice[data-tone="warning"] { border-color: transparent; background: var(--warning-bg); color: var(--warning); }
.notice[data-tone="danger"] { border-color: transparent; background: var(--danger-bg); color: var(--danger); }
.notice[data-tone="info"],
.notice[data-tone="progress"] { border-color: transparent; background: var(--accent-soft); color: var(--on-accent-soft); }
/* Блок отказа с tabindex="-1" (auth-error, account-error): сервер ставит
   на него фокус, когда отказ не про поле. Кольцо видно и при фокусе
   из кода — по нему зрячий с клавиатурой видит, где он. */
.notice[tabindex="-1"]:focus {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* ---------- Тост ----------
   Одно сообщение витрины (catalog.js): «Нет связи», «Страница
   устарела» — над таббаром на телефоне, у нижнего края на компьютере, с
   запасом под полоску «домой» и вырез. Тёмная плашка на светлой странице
   и светлая на тёмной — как SnackBar приложения. Новое сообщение
   заменяет прежнее, а не копится. Пустой тост скрыт прозрачностью, а не
   display: живая область должна быть в дереве до появления текста. */
.toast {
  position: fixed;
  left: calc(16px + env(safe-area-inset-left, 0px));
  right: calc(16px + env(safe-area-inset-right, 0px));
  bottom: calc(var(--tabbar-live, calc(var(--tabbar-h) + env(safe-area-inset-bottom, 0px))) + 12px);
  z-index: 60;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 4px 16px;
  max-width: 480px;
  margin-inline: auto;
  padding: 10px 12px 10px 16px;
  border-radius: var(--r-chip);
  background: var(--ink);
  color: var(--paper);
  box-shadow: var(--card-shadow);
  font-size: .9375rem;
  opacity: 0;
}
.toast.is-shown { opacity: 1; }
@media (min-width: 768px) {
  .toast { bottom: calc(24px + env(safe-area-inset-bottom, 0px)); }
}
@media (prefers-reduced-motion: no-preference) {
  .toast { transition: opacity .2s ease; }
}
.toast__text { flex: 1 1 12em; min-width: 0; }
.toast__action {
  min-height: var(--tap);
  padding: 0 4px;
  border: 0;
  background: transparent;
  color: var(--paper);
  font-family: var(--font-head);
  font-weight: 600;
  text-decoration: underline;
  text-underline-offset: 3px;
}
.toast__action:focus-visible { outline-color: var(--paper); }

/* ---------- Утилиты ---------- */

.stack { display: grid; gap: 12px; }
.stack--lg { gap: 24px; }
.stack--sm { gap: 6px; }
.row { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; }
.row--between { justify-content: space-between; }
.text-center { text-align: center; }
.nowrap { white-space: nowrap; }
.mt-0 { margin-top: 0; }
.mt-8 { margin-top: 8px; }
.mt-16 { margin-top: 16px; }
.mt-24 { margin-top: 24px; }
.divider { height: 1px; border: 0; margin: 0; background: var(--line); }

/* Раскладка «содержимое + липкая колонка итога» (корзина, оформление,
   профиль): до 1024 — одна колонка. */
.layout-aside {
  display: grid;
  gap: 20px;
  align-items: start;
}
@media (min-width: 1024px) {
  .layout-aside { grid-template-columns: minmax(0, 1fr) 360px; gap: 24px; }
  /* Под липкой шапкой — по её настоящей высоте (shop.js), как и
     отступ прокрутки; без скрипта — прежний запас. */
  .layout-aside__side { position: sticky; top: calc(var(--header-live, calc(var(--header-h) + 48px)) + 16px); }
}
/* Шапка на низком экране не липкая (раздел «Шапка») — колонке итога
   отступ на её высоту не нужен. */
@media (min-width: 1024px) and (max-height: 480px) {
  .layout-aside__side { top: 16px; }
}

/* Печать: чёрное на белом, без шапки и таббара. */
@media print {
  .header, .tabbar, .footer, .skip-link, .toast { display: none !important; }
  body { background: #FFFFFF; color: #000000; }
}
