/* REMPIRE — standalone prototype (direction К).
   Ink on white, radius 0, no shadows, Oswald display + Golos Text body.
   Mobile-first; desktop is the enhancement. */

/* ---------- web fonts ----------
   The same Google Fonts files (fonts.gstatic.com, woff2, one file per script
   subset — Oswald and Golos Text are variable, so one file serves every
   weight) declared here instead of through the fonts.googleapis.com
   stylesheet: that stylesheet was a render-blocking round trip to a third
   origin on every page, before this file could even be parsed. Same faces,
   same weights (400/500/600 of both, PT Mono for the admin's codes), same
   font-display: swap. Regenerate from
   https://fonts.googleapis.com/css2?family=Oswald:wght@400;500;600&family=Golos+Text:wght@400;500;600&family=PT+Mono&display=swap
   (fetched with a current Chrome UA) if a family is ever added. */
/* Golos Text 400 cyrillic-ext */
@font-face { font-family: 'Golos Text'; font-style: normal; font-weight: 400; font-display: swap; src: url(https://fonts.gstatic.com/s/golostext/v7/q5uCsoe9Lv5t7Meb31EcEx58hLxR.woff2) format('woff2'); unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F }
/* Golos Text 400 cyrillic */
@font-face { font-family: 'Golos Text'; font-style: normal; font-weight: 400; font-display: swap; src: url(https://fonts.gstatic.com/s/golostext/v7/q5uCsoe9Lv5t7Meb31EcExd8hLxR.woff2) format('woff2'); unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116 }
/* Golos Text 400 latin-ext */
@font-face { font-family: 'Golos Text'; font-style: normal; font-weight: 400; font-display: swap; src: url(https://fonts.gstatic.com/s/golostext/v7/q5uCsoe9Lv5t7Meb31EcEx18hLxR.woff2) format('woff2'); unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF }
/* Golos Text 400 latin */
@font-face { font-family: 'Golos Text'; font-style: normal; font-weight: 400; font-display: swap; src: url(https://fonts.gstatic.com/s/golostext/v7/q5uCsoe9Lv5t7Meb31EcExN8hA.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD }
/* Golos Text 500 cyrillic-ext */
@font-face { font-family: 'Golos Text'; font-style: normal; font-weight: 500; font-display: swap; src: url(https://fonts.gstatic.com/s/golostext/v7/q5uCsoe9Lv5t7Meb31EcEx58hLxR.woff2) format('woff2'); unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F }
/* Golos Text 500 cyrillic */
@font-face { font-family: 'Golos Text'; font-style: normal; font-weight: 500; font-display: swap; src: url(https://fonts.gstatic.com/s/golostext/v7/q5uCsoe9Lv5t7Meb31EcExd8hLxR.woff2) format('woff2'); unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116 }
/* Golos Text 500 latin-ext */
@font-face { font-family: 'Golos Text'; font-style: normal; font-weight: 500; font-display: swap; src: url(https://fonts.gstatic.com/s/golostext/v7/q5uCsoe9Lv5t7Meb31EcEx18hLxR.woff2) format('woff2'); unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF }
/* Golos Text 500 latin */
@font-face { font-family: 'Golos Text'; font-style: normal; font-weight: 500; font-display: swap; src: url(https://fonts.gstatic.com/s/golostext/v7/q5uCsoe9Lv5t7Meb31EcExN8hA.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD }
/* Golos Text 600 cyrillic-ext */
@font-face { font-family: 'Golos Text'; font-style: normal; font-weight: 600; font-display: swap; src: url(https://fonts.gstatic.com/s/golostext/v7/q5uCsoe9Lv5t7Meb31EcEx58hLxR.woff2) format('woff2'); unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F }
/* Golos Text 600 cyrillic */
@font-face { font-family: 'Golos Text'; font-style: normal; font-weight: 600; font-display: swap; src: url(https://fonts.gstatic.com/s/golostext/v7/q5uCsoe9Lv5t7Meb31EcExd8hLxR.woff2) format('woff2'); unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116 }
/* Golos Text 600 latin-ext */
@font-face { font-family: 'Golos Text'; font-style: normal; font-weight: 600; font-display: swap; src: url(https://fonts.gstatic.com/s/golostext/v7/q5uCsoe9Lv5t7Meb31EcEx18hLxR.woff2) format('woff2'); unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF }
/* Golos Text 600 latin */
@font-face { font-family: 'Golos Text'; font-style: normal; font-weight: 600; font-display: swap; src: url(https://fonts.gstatic.com/s/golostext/v7/q5uCsoe9Lv5t7Meb31EcExN8hA.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD }
/* Oswald 400 cyrillic-ext */
@font-face { font-family: 'Oswald'; font-style: normal; font-weight: 400; font-display: swap; src: url(https://fonts.gstatic.com/s/oswald/v57/TK3IWkUHHAIjg75cFRf3bXL8LICs1_Fv40pKlN4NNSeSASz7FmlbHYjedg.woff2) format('woff2'); unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F }
/* Oswald 400 cyrillic */
@font-face { font-family: 'Oswald'; font-style: normal; font-weight: 400; font-display: swap; src: url(https://fonts.gstatic.com/s/oswald/v57/TK3IWkUHHAIjg75cFRf3bXL8LICs1_Fv40pKlN4NNSeSASz7FmlSHYjedg.woff2) format('woff2'); unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116 }
/* Oswald 400 latin-ext */
@font-face { font-family: 'Oswald'; font-style: normal; font-weight: 400; font-display: swap; src: url(https://fonts.gstatic.com/s/oswald/v57/TK3IWkUHHAIjg75cFRf3bXL8LICs1_Fv40pKlN4NNSeSASz7FmlYHYjedg.woff2) format('woff2'); unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF }
/* Oswald 400 latin */
@font-face { font-family: 'Oswald'; font-style: normal; font-weight: 400; font-display: swap; src: url(https://fonts.gstatic.com/s/oswald/v57/TK3IWkUHHAIjg75cFRf3bXL8LICs1_Fv40pKlN4NNSeSASz7FmlWHYg.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD }
/* Oswald 500 cyrillic-ext */
@font-face { font-family: 'Oswald'; font-style: normal; font-weight: 500; font-display: swap; src: url(https://fonts.gstatic.com/s/oswald/v57/TK3IWkUHHAIjg75cFRf3bXL8LICs1_Fv40pKlN4NNSeSASz7FmlbHYjedg.woff2) format('woff2'); unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F }
/* Oswald 500 cyrillic */
@font-face { font-family: 'Oswald'; font-style: normal; font-weight: 500; font-display: swap; src: url(https://fonts.gstatic.com/s/oswald/v57/TK3IWkUHHAIjg75cFRf3bXL8LICs1_Fv40pKlN4NNSeSASz7FmlSHYjedg.woff2) format('woff2'); unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116 }
/* Oswald 500 latin-ext */
@font-face { font-family: 'Oswald'; font-style: normal; font-weight: 500; font-display: swap; src: url(https://fonts.gstatic.com/s/oswald/v57/TK3IWkUHHAIjg75cFRf3bXL8LICs1_Fv40pKlN4NNSeSASz7FmlYHYjedg.woff2) format('woff2'); unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF }
/* Oswald 500 latin */
@font-face { font-family: 'Oswald'; font-style: normal; font-weight: 500; font-display: swap; src: url(https://fonts.gstatic.com/s/oswald/v57/TK3IWkUHHAIjg75cFRf3bXL8LICs1_Fv40pKlN4NNSeSASz7FmlWHYg.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD }
/* Oswald 600 cyrillic-ext */
@font-face { font-family: 'Oswald'; font-style: normal; font-weight: 600; font-display: swap; src: url(https://fonts.gstatic.com/s/oswald/v57/TK3IWkUHHAIjg75cFRf3bXL8LICs1_Fv40pKlN4NNSeSASz7FmlbHYjedg.woff2) format('woff2'); unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F }
/* Oswald 600 cyrillic */
@font-face { font-family: 'Oswald'; font-style: normal; font-weight: 600; font-display: swap; src: url(https://fonts.gstatic.com/s/oswald/v57/TK3IWkUHHAIjg75cFRf3bXL8LICs1_Fv40pKlN4NNSeSASz7FmlSHYjedg.woff2) format('woff2'); unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116 }
/* Oswald 600 latin-ext */
@font-face { font-family: 'Oswald'; font-style: normal; font-weight: 600; font-display: swap; src: url(https://fonts.gstatic.com/s/oswald/v57/TK3IWkUHHAIjg75cFRf3bXL8LICs1_Fv40pKlN4NNSeSASz7FmlYHYjedg.woff2) format('woff2'); unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF }
/* Oswald 600 latin */
@font-face { font-family: 'Oswald'; font-style: normal; font-weight: 600; font-display: swap; src: url(https://fonts.gstatic.com/s/oswald/v57/TK3IWkUHHAIjg75cFRf3bXL8LICs1_Fv40pKlN4NNSeSASz7FmlWHYg.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD }
/* PT Mono 400 cyrillic-ext */
@font-face { font-family: 'PT Mono'; font-style: normal; font-weight: 400; font-display: swap; src: url(https://fonts.gstatic.com/s/ptmono/v14/9oRONYoBnWILk-9AnCIzM-Py.woff2) format('woff2'); unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F }
/* PT Mono 400 cyrillic */
@font-face { font-family: 'PT Mono'; font-style: normal; font-weight: 400; font-display: swap; src: url(https://fonts.gstatic.com/s/ptmono/v14/9oRONYoBnWILk-9AnCszM-Py.woff2) format('woff2'); unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116 }
/* PT Mono 400 latin-ext */
@font-face { font-family: 'PT Mono'; font-style: normal; font-weight: 400; font-display: swap; src: url(https://fonts.gstatic.com/s/ptmono/v14/9oRONYoBnWILk-9AnCEzM-Py.woff2) format('woff2'); unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF }
/* PT Mono 400 latin */
@font-face { font-family: 'PT Mono'; font-style: normal; font-weight: 400; font-display: swap; src: url(https://fonts.gstatic.com/s/ptmono/v14/9oRONYoBnWILk-9AnC8zMw.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD }

:root {
  --ink: #1c1a00;
  --page: #ffffff;
  --paper: #fdfcf9;
  --shell: #edeae1;
  --shell-dark: #e5e1d6;
  /* --paper on the white page is a ~1% luminance difference — invisible as a
     hover. This is the one visible hover fill for rows and quiet controls. */
  --hover: rgba(28, 26, 0, .05);
  --rule: rgba(28, 26, 0, .15);
  --rule-soft: rgba(28, 26, 0, .08);
  --muted: #6f6b57;
  --error: #8c1a0f;
  --ok: #2f6a3f;

  --ease: cubic-bezier(.2, .8, .2, 1);
  --ease-inout: cubic-bezier(.45, 0, .25, 1);

  --s1: 4px; --s2: 8px; --s3: 12px; --s4: 16px;
  --s5: 24px; --s6: 32px; --s7: 48px; --s8: 64px;

  --gutter: 16px;
  --maxw: 1240px;
  --bottomnav-h: 58px;
  --bar-h: 68px;

  --z-nav: 30;
  --z-bar: 40;
  --z-scrim: 55;
  --z-drawer: 60;
  --z-toast: 70;
  --z-intro: 90;
  --z-scan: 95;
}

@media (min-width: 768px) { :root { --gutter: 32px; } }
@media (min-width: 1100px) { :root { --gutter: 48px; } }

* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; scrollbar-gutter: stable; }

body {
  margin: 0;
  background: var(--page);
  color: var(--ink);
  font-family: "Golos Text", ui-sans-serif, system-ui, "Segoe UI", Roboto, sans-serif;
  font-size: 16px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}
body.is-locked { overflow: hidden; }

.num { font-variant-numeric: tabular-nums; }
.muted { color: var(--muted); font-size: 13.5px; }

h1, h2, h3 { margin: 0; font-weight: 600; line-height: 1.15; }

.display {
  font-family: Oswald, "Golos Text", sans-serif;
  text-transform: uppercase;
  letter-spacing: .18em;
  font-weight: 600;
}
.h1 { font-size: 21px; margin-bottom: 10px; }
@media (min-width: 768px) { .h1 { font-size: 24px; } }

a { color: inherit; text-decoration: none; }
button { font: inherit; color: inherit; }

:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }

img { max-width: 100%; display: block; }

/* Visually hidden, still read out: the label of a control whose visible text
   is something else (a card's size picker), the home page's <h1>, the
   checkout's title on a phone. Not display:none — that takes the text out
   of the accessibility tree along with the screen. */
.vh {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

.wrap { max-width: var(--maxw); margin: 0 auto; padding-inline: var(--gutter); }
.wrap--narrow { max-width: 560px; }
.wrap--mid { max-width: 680px; }
.wrap--co { max-width: 1020px; }

/* ---------- product photography ----------
   Images are local PNG/WebP with a real alpha channel — the white and grey
   studio backings were stripped once, offline. Nothing here relies on
   mix-blend-mode, which silently dies inside any stacking context (an
   opacity transition, a transform) and used to leave a hard white box.
   Merch is a model shot: it keeps its frame and fills the tile instead. */
.ph {
  position: absolute; inset: 7%;
  background-size: contain; background-position: center; background-repeat: no-repeat;
}

/* ---------- buttons ---------- */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  min-height: 48px; padding: 0 22px;
  border: 1px solid var(--ink); background: var(--ink); color: var(--paper);
  font-family: Oswald, sans-serif; font-size: 13px; letter-spacing: .14em;
  text-transform: uppercase; cursor: pointer;
  transition: opacity .18s var(--ease), transform .12s var(--ease);
}
.btn:hover { opacity: .86; }
.btn:active { transform: translateY(1px); opacity: .75; }
.btn--ghost { background: transparent; color: var(--ink); }
.btn--ghost:hover { background: var(--shell); opacity: 1; }
.btn--wide { width: 100%; }
.btn--sm { min-height: 44px; padding: 0 15px; font-size: 11.5px; }
.btn--express { background: var(--ink); border-color: var(--ink); color: var(--paper); margin-bottom: 8px; }
.btn[disabled] { opacity: .4; cursor: not-allowed; }
/* One short pulse on the step's «Далее» the moment the step is complete —
   see coNextPulse() in app.js. Two beats and done: it points, it does not
   flash. `transform` and `box-shadow` only, so nothing around it reflows. */
.btn--nudge { animation: btnNudge .55s var(--ease) 2; }
@keyframes btnNudge {
  0%, 100% { transform: none; box-shadow: 0 0 0 0 rgba(28, 26, 0, 0); }
  45% { transform: translateY(-2px); box-shadow: 0 0 0 4px rgba(28, 26, 0, .12); }
}
@media (prefers-reduced-motion: reduce) { .btn--nudge { animation: none; } }
/* tight-cropped viewBox: 15px here is a real 15px-tall wordmark */
.gpay-mark { display: inline-flex; align-items: center; height: 15px; margin-left: 8px; }
.gpay-mark svg { height: 100%; width: auto; color: var(--paper); }

.link {
  border: none; background: none; padding: 0; cursor: pointer;
  border-bottom: 1px solid rgba(28, 26, 0, .35);
  font-size: 13px; line-height: 1.3; font-family: inherit;
}
.link:hover { border-bottom-color: var(--ink); }

/* ---------- header ---------- */
.hdr {
  position: sticky; top: 0; z-index: var(--z-nav);
  background: var(--page); border-bottom: 1px solid var(--rule);
}
/* The header lives in a persistent slot div. `display: contents` takes that
   div out of the box tree, otherwise it becomes the sticky header's
   containing block — a box exactly as tall as the header, i.e. zero travel,
   and the header scrolls away like a static element. */
#hdrslot { display: contents; }
#hdrslot[hidden] { display: none; }
.hdr__announce {
  background: var(--ink); color: var(--paper);
  font-size: 11px; letter-spacing: .04em; text-align: center;
  padding: 7px var(--gutter);
}
/* The header rows sit in the SAME centred column as the page content
   (.wrap and .hero__box both cap at --maxw and auto-margin). Without this the
   header was full-bleed: on a 2000px screen the logo sat at the window edge
   while the hero began ~400px in, so nothing in the header lined up with
   anything below it. */
.hdr__row {
  display: flex; align-items: center; gap: 10px 18px;
  padding: 10px var(--gutter);
  max-width: var(--maxw); margin-inline: auto;
}
.hdr__logo {
  display: inline-flex; align-items: center; gap: 9px;
  flex-shrink: 0; cursor: pointer; background: none; border: none; padding: 0;
}
.hdr__logo svg { height: 34px; width: auto; }
.hdr__word {
  font-family: Oswald, sans-serif; font-weight: 600; font-size: 15px;
  letter-spacing: .3em; text-transform: uppercase; padding-left: 2px;
}

/* the mark draws itself once, then answers taps */
.hdr__tower { transform-origin: 50% 100%; transition: transform .25s var(--ease); }
.hdr__logo:hover .hdr__tower { transform: translateY(-2px); }
/* The mark's RESTING state is the finished one, and the animation takes it
   away for a moment rather than bringing it in. Written the other way round
   — dashoffset 1 and opacity 0 at rest, animated to the visible state — an
   animation that does not run leaves the tower invisible for good, while the
   34px box it sits in keeps its width: no tower, and «REMPIRE» standing a
   tower's width to the right of where it belongs. That is what Ренат opened
   his iPad to on 20.09.2026, and with `backwards` / a keyframe that starts
   hidden the worst case is now a mark that simply appears without its intro. */
.hdr__tower .tw-draw {
  fill: none; stroke: var(--ink); stroke-width: 2.4;
  stroke-dasharray: 1; stroke-dashoffset: 0;
  animation: draw 1.1s var(--ease-inout) .2s backwards;
}
.hdr__tower .tw-fill { opacity: 1; animation: fillIn 1.5s ease backwards; }
.hdr__tower.is-ident { animation: ident .55s var(--ease); }
@keyframes ident {
  30% { transform: translateY(-4px) scaleY(1.05); }
  62% { transform: translateY(1px) scaleY(.985); }
  to { transform: none; }
}

.hdr__searchwrap {
  flex: 1 1 180px; min-width: 0; position: relative;
  display: flex; align-items: center;
}
.hdr__searchicon {
  position: absolute; left: 0; display: inline-flex; color: var(--muted); pointer-events: none;
}
.hdr__searchicon svg { width: 17px; height: 17px; }
.hdr__search {
  width: 100%; border: none; border-bottom: 1px solid rgba(28, 26, 0, .28);
  background: transparent; padding: 9px 2px 9px 25px; font: inherit; font-size: 14px;
  outline: none; min-height: 42px;
}
.hdr__search::placeholder { color: var(--muted); }
.hdr__search:focus { border-bottom-color: var(--ink); border-bottom-width: 2px; padding-bottom: 8px; }

.hdr__tools { display: inline-flex; align-items: center; gap: 2px; margin-left: auto; flex-shrink: 0; }
.iconbtn {
  border: none; background: none; cursor: pointer; padding: 10px;
  display: inline-flex; position: relative; color: var(--ink);
  min-width: 44px; min-height: 44px; align-items: center; justify-content: center;
  transition: background .18s var(--ease);
}
.iconbtn:hover { background: var(--hover); }
.iconbtn svg { width: 20px; height: 20px; }
.badge {
  position: absolute; top: 3px; right: 3px;
  background: var(--ink); color: var(--paper);
  font-size: 10px; min-width: 16px; height: 16px; padding: 0 3px;
  display: flex; align-items: center; justify-content: center;
}
.narrow-only { display: none; }
@media (max-width: 599px) {
  .wide-only { display: none; }
  .narrow-only { display: inline; }
}

.hdr__nav {
  display: flex; gap: 18px; padding: 0 var(--gutter) 9px;
  max-width: var(--maxw); margin-inline: auto;
  overflow-x: auto; white-space: nowrap; font-size: 13.5px;
  scrollbar-width: none;
  /* the row scrolls sideways on a phone; fade the right edge so that reads */
  -webkit-mask-image: linear-gradient(90deg, #000 calc(100% - 26px), transparent);
  mask-image: linear-gradient(90deg, #000 calc(100% - 26px), transparent);
}
@media (min-width: 900px) { .hdr__nav { -webkit-mask-image: none; mask-image: none; } }
.hdr__nav::-webkit-scrollbar { display: none; }
.hdr__nav button {
  border: none; background: none; padding: 11px 0; cursor: pointer;
  border-bottom: 2px solid transparent; color: var(--muted);
  transition: color .18s var(--ease); min-height: 44px;
}
.hdr__nav button:hover { color: var(--ink); }
.hdr__nav button[aria-current="true"] { border-bottom-color: var(--ink); color: var(--ink); }

/* Phone: search gets its own full-bleed row. It is separated by one hairline
   that runs the whole width — the field itself carries no border, so no stray
   edge appears where the input stops. */
@media (max-width: 767px) {
  .hdr__row { flex-wrap: wrap; padding-bottom: 0; }
  .hdr__tools { order: 2; }
  .hdr__searchwrap {
    order: 3; flex: 1 0 100%;
    margin: 0 calc(var(--gutter) * -1); padding: 0 var(--gutter);
    border-top: 1px solid var(--rule);
  }
  .hdr__searchicon { left: var(--gutter); }
  .hdr__search { border: none; padding-block: 11px; }
  .hdr__search:focus { border: none; padding-bottom: 11px; box-shadow: none; }
  .hdr__searchwrap:focus-within { background: var(--hover); }
  .hdr__nav { border-top: 1px solid var(--rule); padding-top: 9px; }
}
@media (min-width: 768px) { .hdr__searchwrap { flex: 1 1 240px; max-width: 440px; } }

/* ---------- language dropdown ---------- */
.lang { position: relative; }
.lang__btn { display: inline-flex; align-items: center; gap: 5px; border: none; background: none; cursor: pointer; padding: 10px 6px; min-height: 44px; }
.lang__caret { font-size: 9px; opacity: .55; }
/* no-repeat + exact stretch: the default repeat let a rounding sliver of the
   next tile show — a blue line under the Estonian flag, red above the Russian */
.lang__flag {
  width: 20px; height: 14px; border: 1px solid var(--rule); display: block;
  background-size: 100% 100%; background-position: center; background-repeat: no-repeat;
}
.lang__menu {
  position: absolute; right: 0; top: 100%; min-width: 160px;
  background: var(--page); border: 1px solid var(--ink); z-index: 40;
}
.lang__item {
  display: flex; align-items: center; gap: 10px; width: 100%;
  min-height: 44px; padding: 0 12px; border: none; background: none;
  cursor: pointer; font-size: 13.5px; text-align: left;
  transition: background .18s var(--ease);
}
.lang__item:hover { background: var(--hover); }
.lang__item[aria-selected="true"] { background: var(--shell); }
.lang__note {
  margin: 0; padding: 9px 12px; font-size: 11px; line-height: 1.4;
  color: var(--muted); border-top: 1px solid var(--rule);
}

/* ---------- bottom nav ---------- */
.botnav {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: var(--z-nav);
  display: flex; background: var(--page); border-top: 1px solid var(--ink);
  padding-bottom: env(safe-area-inset-bottom);
}
.botnav button {
  flex: 1; min-height: var(--bottomnav-h);
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 3px;
  border: none; background: none; cursor: pointer;
  font-family: Oswald, sans-serif; font-size: 10px; letter-spacing: .1em; text-transform: uppercase;
  color: var(--muted); padding: 8px 2px;
  transition: color .18s var(--ease);
}
.botnav__ico { position: relative; display: inline-flex; }
.botnav button svg { width: 21px; height: 21px; stroke-width: 1.5; }
/* The active marker used to be an inset box-shadow. Five tabs across a
   390px phone give each a fractional width, and the browser bleeds an inset
   shadow a subpixel down both side edges of such a box — faint full-height
   hairlines that read as borders (Renat saw a "left border" on Поиск).
   A real 2px bar has edges exactly where the tab's are. */
.botnav button { position: relative; }
.botnav button[aria-current="true"], .botnav button[aria-expanded="true"] { color: var(--ink); }
.botnav button[aria-current="true"]::after, .botnav button[aria-expanded="true"]::after {
  content: ""; position: absolute; top: 0; left: 0; right: 0; height: 2px; background: var(--ink);
}
.botnav button[aria-current="true"] svg, .botnav button[aria-expanded="true"] svg { stroke-width: 1.9; }
.botnav .badge { top: -4px; right: -9px; }
@media (min-width: 768px) { .botnav { display: none; } }

/* Content inset so nothing hides behind the fixed bars. Any screen carrying a
   sticky action bar has to clear the bar AND the nav — the product page did
   not, which parked the footer's payment row permanently under the buy
   button, unreachable at any scroll position. */
.screen { padding-bottom: calc(var(--bottomnav-h) + env(safe-area-inset-bottom) + 24px); }
.screen--co,
body:has(.stickybar) .screen,
body:has(.stickybar) .ftr {
  padding-bottom: calc(var(--bottomnav-h) + var(--bar-h) + env(safe-area-inset-bottom) + 24px);
}
@media (min-width: 768px) {
  .screen, .screen--co, body:has(.stickybar) .screen { padding-bottom: 40px; }
  body:has(.stickybar) .ftr { padding-bottom: 0; }
}

/* ---------- sections ---------- */
.sec { padding: var(--s6) 0 var(--s5); }
.sec__head {
  display: flex; justify-content: space-between; align-items: baseline; gap: 16px;
  border-bottom: 1px solid var(--ink); padding-bottom: 8px; margin-bottom: 8px;
}
.sec__head--sub { margin-top: 32px; }
.sec__title { font-family: Oswald, sans-serif; font-size: 13px; letter-spacing: .18em; text-transform: uppercase; }
.sec__intro { margin: 0 0 20px; font-size: 14px; color: var(--muted); max-width: 68ch; }
.sec__head .link { border-bottom-color: transparent; white-space: nowrap; }
.sec__head .link:hover { border-bottom-color: var(--ink); }

/* ---------- product grid ---------- */
.grid {
  display: grid; gap: 32px 16px;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 150px), 1fr));
}
@media (min-width: 768px) { .grid { gap: 36px 20px; grid-template-columns: repeat(auto-fill, minmax(min(100%, 200px), 1fr)); } }
/* minmax(0,1fr), NOT bare 1fr: a bare fr track's minimum is the widest card's
   min-content, so one product with a long unbreakable name silently widened
   all four tracks past the container and the page scrolled sideways. */
@media (min-width: 1100px) { .sec .grid { grid-template-columns: repeat(4, minmax(0, 1fr)); } }

/* The add link used to be a <span> inside the card <button> — invalid nesting
   and unreachable by keyboard. Card and add are now sibling buttons. */
.card { display: flex; flex-direction: column; height: 100%; min-width: 0; }
.card__name, .card__brand { overflow-wrap: anywhere; }
.card__go {
  display: flex; flex-direction: column; gap: 3px; flex: 1;
  cursor: pointer; background: none; border: none; padding: 0; text-align: left;
  color: inherit; font: inherit;
}
/* No frame around the photo. The images carry real alpha now, so a border
   would be the only box left on the page — and a box is exactly what the
   brief rules out. Structure comes from the grid gaps instead. */
.card__media {
  position: relative; aspect-ratio: 1; background: var(--page);
  margin-bottom: 9px; overflow: hidden;
}
/* …and a real in-flow height under it, not only the ratio.
 *
 * Renat's MacBook, 21.09.2026: Safari 17.3 showed every product card as text
 * with no photograph and no room for one. Chrome on the same machine was
 * fine, the image opened on its own URL, and there was no content blocker —
 * the box itself had collapsed. These three boxes hold nothing but
 * absolutely-positioned children, so their content height is zero and the
 * height comes entirely from `aspect-ratio`; that resolution is what 17.3
 * gets wrong inside a grid item, and 17.4 fixed. iPadOS and every other
 * engine were already past it, which is why only his laptop saw it.
 *
 * The pseudo-element is in flow and gives the box its height without asking
 * the ratio to resolve. Where `aspect-ratio` works it computes the same
 * number, so this changes nothing anywhere else — it only removes the
 * dependency. */
.card__media::before,
.cattile__shot::before,
.pdp__stage::before { content: ""; display: block; padding-top: 100%; }
.cattile__shot::before { padding-top: 75%; }   /* 4 / 3 */

.card__img, .card__img2 { transition: opacity .3s var(--ease), transform .5s var(--ease); }
.card__img2 { opacity: 0; }
@media (hover: hover) {
  /* Only cross-fade where a second photo exists — `.card--dual`. Fading the
     first image out on a single-photo product left an empty card on hover. */
  .card--dual:hover .card__img { opacity: 0; }
  .card--dual:hover .card__img2 { opacity: 1; }
  .card:hover .card__img, .card:hover .card__img2 { transform: scale(1.03); }
}
.card__wm {
  position: absolute; right: 7px; bottom: 7px; width: 12px; height: 19px;
  background: var(--ink); opacity: .13; pointer-events: none;
  -webkit-mask: url("../brand/rempire-tower.svg") center / contain no-repeat;
  mask: url("../brand/rempire-tower.svg") center / contain no-repeat;
}
.card__brand { font-size: 10px; letter-spacing: .14em; text-transform: uppercase; color: var(--muted); }
.card__name { font-size: 13px; line-height: 1.35; }
/* Exactly two lines, always — «make sure everything is on same height».
   A one-line name still occupies two, so the price and «В корзину» beneath it
   land on the same height in every card of a row; a longer one is cut with an
   ellipsis, which happens to 43 of the 220 products and never to the brand,
   the price or the button. The full name is on the product page. */
.card__name {
  margin: 4px 0 4px;
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
  overflow: hidden;
  /* the box the two lines sit in, whether or not there are two of them */
  min-height: calc(2 * 1.35em);
}
/* No line between products (owner, 09.09.2026): «the add to cart button
   already has it» — the underline under «В корзину» closes each card off, so
   the hairline that used to run under the whole card is gone, and with it
   markLastRows(), which measured every card after every paint just to leave
   the bottom row's line undrawn. */
.card { position: relative; box-sizing: border-box; }
/* Two rows, left-aligned: the price, «В корзину» under it. Pinned to the
   card's bottom, though with the name fixed at two lines every foot already
   starts at the same height — this only catches a card whose photo failed to
   load at the declared ratio. */
.card__foot {
  position: relative; display: flex; flex-direction: column; align-items: flex-start;
  /* 14px, and it is load-bearing, not taste: the add link stretches its own
     hit area 13px upwards (.card__add::before) to reach a thumb-sized target,
     and anything less than that here would put the invisible half of the
     button on top of the price — a tap on «9 €» would add to the basket. */
  gap: 14px; margin-top: auto; padding-top: 6px;
}
/* nowrap belongs here rather than on the foot, which is a column now: a price
   must not break, but «Сообщить о наличии» has to be allowed to. */
.card__price { font-size: 13px; font-weight: 600; white-space: nowrap; }
.card__add {
  display: inline-block; padding: 0; margin: 0; border: 0; border-bottom: 1px solid rgba(28, 26, 0, .4);
  background: none; font: inherit; font-size: 11px; line-height: 1.5; color: var(--ink);
  cursor: pointer; text-decoration: none; touch-action: manipulation; position: relative;
}
/* The thumb target, 43px tall around an 11px line. It reaches 13px up and
   14px down: up stops inside the foot's 14px gap, short of the price; down
   runs into the grid's 32px row gap, well short of the next card's photo. */
.card__add::before { content: ""; position: absolute; inset: -13px 0 -14px; }
.card__add:hover { border-bottom-color: var(--ink); }
.card__add:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }
/* «Сообщить о наличии» is the longest label on any card and it owns a whole
   row now, so it wraps rather than being cut. */
.card__add--notify { color: var(--muted); border-bottom-color: rgba(28, 26, 0, .25); white-space: normal; }
/* A set's crossed-out old price steps aside on the narrowest phones — the
   «−13 %» flag on the picture already says the same thing. */
@media (max-width: 360px) { .card__price .bwas { display: none; } }
/* stock / salon-price flags live on the photo, not beside the price.
   A partner sees TWO of them at once — «Цена для салонов» beside «нет в
   наличии» — and the row used to be bounded on the left only, so the pair
   was squeezed into whatever the photo happened to be: at 375px that is
   152px for 252px of chip, and «НЕТ В НАЛИЧИИ» ran out through its own
   border (Renat, 18.09.2026). `right` gives the row an end, `wrap` lets the
   second chip drop to its own line at full width instead of being crushed,
   and `max-width` keeps a chip that is wider than the photo wrapping its
   text rather than hanging off the side. Russian is the long one — «Цена для
   салонов» wants 136px of the 152 on its own — so it is the language that
   decides this rule; ET and EN fit inside whatever it allows. */
.card__flags {
  position: absolute; left: 6px; top: 6px; right: 6px;
  display: flex; flex-wrap: wrap; gap: 4px; pointer-events: none;
}
.card__flag { background: var(--page); max-width: 100%; }

.chip {
  display: inline-block; font-size: 10px; letter-spacing: .1em; text-transform: uppercase;
  padding: 2px 7px; border: 1px solid currentColor;
}
.chip--low { color: var(--error); }
.chip--out { color: var(--muted); }
.chip--ok { color: var(--ok); }

/* ---------- category tiles ---------- */
.cattiles {
  display: grid; gap: 16px 12px;
  grid-template-columns: repeat(auto-fill, minmax(min(46%, 130px), 1fr));
}
.cattile {
  background: none; border: none; cursor: pointer; padding: 0; text-align: left;
  display: flex; flex-direction: column; gap: 2px;
}
.cattile__shot {
  position: relative; aspect-ratio: 4 / 3; background: var(--shell);
  margin-bottom: 8px; overflow: hidden;
  transition: background .18s var(--ease);
}
.cattile:hover .cattile__shot { background: var(--shell-dark); }
.cattile__img { inset: 9%; }
.cattile__name { font-family: Oswald, sans-serif; font-size: 11.5px; letter-spacing: .18em; text-transform: uppercase; }
.cattile__n { font-size: 11.5px; color: var(--muted); }

/* ---------- brands ----------
   Real brand marks where the shop has them (alpha WebP, ink on nothing), the
   brand name in the display face where it does not — one tile either way. */
.brandtiles {
  display: grid; gap: 12px;
  grid-template-columns: repeat(auto-fill, minmax(min(46%, 168px), 1fr));
}
.brandtile {
  border: 1px solid var(--rule); background: var(--page); cursor: pointer;
  padding: 18px 14px 12px; display: flex; flex-direction: column; gap: 10px;
  align-items: center; text-align: center;
  transition: border-color .18s var(--ease), background .18s var(--ease);
}
.brandtile:hover { border-color: var(--ink); background: var(--hover); }
/* Every mark is pre-rendered onto one 3:1 ink canvas at matched optical
   weight (tools/normalise-brandlogos.mjs), so a box of this aspect renders
   them all at exactly the same size. Word fallbacks share the same box, so a
   brand with a logo and one without read as the same object. */
.brandtile__mark {
  display: flex; align-items: center; justify-content: center;
  width: 100%; aspect-ratio: 3 / 1; max-height: 66px;
}
.brandtile__logo {
  display: block; width: 100%; height: 100%;
  background-size: contain; background-position: center; background-repeat: no-repeat;
}
.brandtile__word {
  font-family: Oswald, sans-serif; font-size: 15px; letter-spacing: .1em;
  text-transform: uppercase; line-height: 1.2; text-align: center;
}
.brandtile__n { font-size: 11.5px; color: var(--muted); }

.brandhead { height: 62px; margin-bottom: 14px; }
.brandhead__logo {
  display: block; height: 100%; width: min(280px, 70%);
  background-size: contain; background-position: left center; background-repeat: no-repeat;
}
.brandhead__word { font-family: Oswald, sans-serif; font-size: 24px; letter-spacing: .18em; text-transform: uppercase; }

/* A grid, not a wrapped flex row: chips were sized by their content, so a
   two-digit count made one 160px and a single digit 153px, and the wrap fell
   ragged — the holes read as missing brands. Equal columns cannot do that. */
.brandrow {
  display: grid; gap: 10px;
  grid-template-columns: repeat(auto-fill, minmax(min(48%, 158px), 1fr));
}
.brandchip {
  display: flex; align-items: center; justify-content: center; gap: 10px;
  border: 1px solid var(--rule); background: var(--page); cursor: pointer;
  min-height: 56px; padding: 0 12px; font-size: 13.5px; width: 100%;
  transition: border-color .18s var(--ease), background .18s var(--ease);
}
.brandchip:hover { border-color: var(--ink); background: var(--hover); }
.brandchip__logo {
  display: block; width: 102px; height: 34px;  /* the canvas aspect, exactly */
  background-size: contain; background-position: center; background-repeat: no-repeat;
}
.brandchip__word {
  display: flex; align-items: center; justify-content: center;
  width: 102px; height: 34px;
  font-family: Oswald, sans-serif; font-size: 12.5px; letter-spacing: .1em;
  text-transform: uppercase; text-align: center; line-height: 1.15;
}
.brandchip .num { font-size: 11.5px; color: var(--muted); }
.pdp__brand {
  border: none; background: none; padding: 0; cursor: pointer;
  font: inherit; font-size: 10px; letter-spacing: .14em; text-transform: uppercase;
  color: var(--muted); text-align: left;
}
.pdp__brand { transition: color .18s var(--ease); }
.pdp__brand:hover { color: var(--ink); }

.infogrid {
  display: grid; gap: 16px 28px;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 220px), 1fr));
  font-size: 13.5px; color: var(--muted);
}

/* ---------- hero slider ----------
   Slides are stacked in one grid cell and crossfade. Opacity alone (not
   display swapping, not an entrance animation) because the animation did not
   re-run when autoplay flipped the attribute, and display swapping blanked
   the hero for the whole transition. */
.hero { position: relative; display: grid; background: var(--shell); overflow: hidden; touch-action: pan-y; }
.hero__slide {
  grid-area: 1 / 1; opacity: 0; visibility: hidden;
  transition: opacity .5s var(--ease), visibility 0s linear .5s;
}
.hero__slide[data-on="1"] {
  opacity: 1; visibility: visible;
  transition: opacity .5s var(--ease), visibility 0s;
}
.hero__box {
  max-width: var(--maxw); margin: 0 auto; width: 100%;
  padding: var(--s5) var(--gutter) 46px;
  min-height: 380px;
  display: flex; align-items: center; gap: clamp(16px, 4vw, 56px);
}
.hero__inner { flex: 1 1 46ch; max-width: 52ch; min-width: 0; }
/* --muted on the beige hero is 4.46:1 — just under AA. Darkened for this one
   surface rather than darkening --muted everywhere. */
.hero__eyebrow { font-family: Oswald, sans-serif; font-size: 10.5px; letter-spacing: .3em; text-transform: uppercase; color: #5c5947; margin-bottom: 8px; }
.hero__title { font-family: Oswald, sans-serif; font-size: clamp(23px, 4.4vw, 40px); text-transform: uppercase; letter-spacing: .02em; line-height: 1.08; margin-bottom: 10px; }
.hero__sub { font-size: 14px; color: rgba(28, 26, 0, .78); margin-bottom: 18px; }
.hero__art {
  flex: 0 1 36%; align-self: stretch; min-height: 210px;
  background-size: contain; background-position: center; background-repeat: no-repeat;
  pointer-events: none;
}
/* The gift card is the one banner target with no product photo behind it
   (heroArt() in app.js): the slide shows the same tower on the same shell
   panel the gift block on the home page uses, so the banner looks like the
   page its button opens. */
.hero__art--gift { display: flex; align-items: center; justify-content: center; background: var(--shell); }
.hero__giftmark { width: 42px; height: 66px; color: var(--ink); }
/* Ренат, 20.09.2026: «убрать квадратное оформление и оставить просто стрелки
   и чуть их увеличить», with a drawing — a red cross over a boxed chevron, a
   green tick beside a bare one. So the plate is gone and the chevron is the
   whole control. The BUTTON keeps its size: 48 × 48 is the touch target, and
   taking the plate away must not take the target with it.

   The plate was also what made the glyph legible over a photograph, so the
   stroke carries a shadow of its own instead — a banner image is the owner's
   to upload and can be light or dark under either arrow. */
.hero__arrow {
  position: absolute; top: 50%; transform: translateY(-50%); z-index: 3;
  width: 48px; height: 48px; border: none; padding: 0; cursor: pointer;
  background: none; color: var(--ink);
  display: flex; align-items: center; justify-content: center;
  transition: transform .18s var(--ease), opacity .18s var(--ease);
}
.hero__arrow svg {
  width: 15px; height: 34px;
  filter: drop-shadow(0 0 3px rgba(255, 255, 255, .95)) drop-shadow(0 1px 2px rgba(255, 255, 255, .8));
}
.hero__arrow:hover { opacity: .7; }
.hero__arrow--prev { left: 4px; }
.hero__arrow--next { right: 4px; }
.hero__arrow--prev:hover { transform: translate(-2px, -50%); }
.hero__arrow--next:hover { transform: translate(2px, -50%); }
/* …and the slide has to give them room. The box's own gutter is 32px and
   the left arrow ends at 52, so on a tablet the chevron stood on the first
   letters of «TALLINN · MARDI 1 · EST 2018» and the headline under it
   (Ренат, 20.09.2026). It only happens while the box reaches the window's
   edges: from --maxw + 2 × 32 up, the box is centred and its gutter already
   clears the arrow, so the wide desktop keeps the measure it has.

   `:has(.hero__arrow)` because a single banner is a picture, not a carousel,
   and draws no arrows at all — it must not be indented for controls that
   are not there. */
@media (min-width: 768px) and (max-width: 1303px) {
  .hero:has(.hero__arrow) .hero__box { padding-inline: max(var(--gutter), 64px); }
}
/* The phone used to have no arrows at all — swipe and autoplay only — and
   Ренат read that as the same fault («на телефоне они вообще отсутствуют»).
   A swipe nobody can see is not a control, so they are drawn there too.

   They sit over the PHOTO, not over the middle of the slide. The phone's
   hero is a column — picture on top, words underneath (.hero__art has
   order: -1 below) — so «the vertical centre» lands in the eyebrow line and
   the arrow clipped the first letter of «TALLINN · MARDI 1 · EST 2018». The
   band below is the picture's own height, written the same way .hero__art
   writes it, with .hero__box's top padding above it. */
@media (max-width: 767px) {
  .hero__arrow {
    width: 44px; top: var(--s5); transform: none;
    height: 44vw; max-height: 230px; min-height: 150px;
  }
  .hero__arrow svg { width: 13px; height: 29px; }
  .hero__arrow--prev { left: 0; }
  .hero__arrow--next { right: 0; }
  /* …so the hover nudge cannot put the -50% back */
  .hero__arrow--prev:hover { transform: translateX(-2px); }
  .hero__arrow--next:hover { transform: translateX(2px); }
}

/* UX fix 1: dot indicators removed (staging review) — arrows, swipe and
   autoplay carry the carousel on their own; the .hero__dots rules they used
   to draw are gone with them. */

/* Phone: photo on top, words underneath — a wash behind the text made the
   product unreadable and the merch shot unrecognisable. */
@media (max-width: 767px) {
  .hero__box { flex-direction: column; align-items: stretch; gap: 14px; min-height: 0; padding-bottom: 50px; }
  /* the column flex axis is vertical here, so the desktop `flex: 1 1 46ch`
     would set a 46ch *height* and blow the hero out to ~680px */
  .hero__inner { flex: 0 0 auto; max-width: none; }
  .hero__art { order: -1; flex: 0 0 auto; width: 100%; height: 44vw; max-height: 230px; min-height: 150px; }
  .hero__title { font-size: 26px; }
}

/* ---------- drawers ---------- */
.scrim { position: fixed; inset: 0; background: rgba(28, 26, 0, .45); z-index: var(--z-scrim); animation: fade .25s ease; }
@keyframes fade { from { opacity: 0; } }
.drawer {
  position: fixed; top: 0; bottom: 0; z-index: var(--z-drawer);
  width: min(390px, 92vw); background: var(--page);
  display: flex; flex-direction: column;
  animation: slideIn .3s var(--ease);
}
.drawer--right { right: 0; border-left: 1px solid var(--ink); --from: 100%; }
.drawer--left { left: 0; border-right: 1px solid var(--ink); --from: -100%; }
@keyframes slideIn { from { transform: translateX(var(--from)); } }
.drawer__head { display: flex; align-items: center; justify-content: space-between; padding: 14px 18px; border-bottom: 1px solid var(--rule); }
.drawer__t { font-size: 13px; }
.drawer__body { flex: 1; overflow: auto; padding: 16px 18px; -webkit-overflow-scrolling: touch; }
.drawer__foot { border-top: 1px solid var(--ink); padding: 14px 18px calc(14px + env(safe-area-inset-bottom)); display: flex; flex-direction: column; gap: 10px; }
.drawer__tot { display: flex; justify-content: space-between; font-weight: 600; }
.drawer__cont { align-self: center; }

.cline { display: flex; gap: 12px; padding-bottom: 14px; margin-bottom: 14px; border-bottom: 1px solid var(--rule); }
.cline__ph { position: relative; width: 62px; height: 62px; flex-shrink: 0; }
.cline__mid { flex: 1; min-width: 0; }
.cline__nm { display: block; font-size: 13.5px; margin-bottom: 7px; }
/* Renat, 13.09.2026: «the "remove" button is sometimes behind the "+" and
   sometimes on second row — this should remain same for all and be inline if
   possible». Both happened, and neither was about the kind of line: the
   stepper (inline-flex, 124px and unshrinkable) and «Убрать» (inline-block)
   used to sit loose in .cline__mid, a plain block, so they shared one
   anonymous line box. On a 390px drawer they fitted; on a phone, or under a
   long set name, or beside a four-digit price — .cline__pr had no
   flex-shrink — the button dropped to a second line, and where .cline__mid
   was squeezed past its min-content width the stepper overflowed into the
   price column and the button painted on top of the «+».
   One explicit flex row fixes all of it: nothing wraps, nothing overlaps, and
   a product, a set and a gift card lay out identically. */
.cline__acts { display: flex; align-items: center; gap: 8px; flex-wrap: nowrap; }
.cline__rm { font-size: 12px; padding: 14px 4px; flex: 0 0 auto; white-space: nowrap; }
.cline__pr { font-weight: 600; font-size: 13.5px; flex-shrink: 0; }

.freebar { margin-top: 6px; }
.freebar__track { height: 3px; background: var(--shell); }
.freebar__fill { height: 3px; background: var(--ink); transition: width .35s var(--ease); }
.freebar p { margin: 8px 0 0; font-size: 12px; }

/* ---------- forms ---------- */
.field { display: block; margin-bottom: 14px; }
.field__label { display: block; font-size: 11.5px; color: var(--muted); margin-bottom: 3px; }
.input {
  width: 100%; min-height: 48px;
  border: none; border-bottom: 1px solid rgba(28, 26, 0, .4);
  background: transparent; padding: 10px 2px; font: inherit; font-size: 16px; outline: none;
}
.input:focus { border-bottom-color: var(--ink); border-bottom-width: 2px; padding-bottom: 9px; }
.input--box { border: 1px solid rgba(28, 26, 0, .35); padding: 10px 12px; }
.input--box:focus { border: 1px solid var(--ink); box-shadow: inset 0 0 0 1px var(--ink); padding: 10px 12px; }
.input[aria-invalid="true"] { border-bottom: 2px solid var(--error); }
.err { color: var(--error); font-size: 12.5px; margin: 6px 0 10px; }
/* inside a .field the label already carries the gap below it */
.field .err { margin: 4px 0 0; }
.hint { color: var(--muted); font-size: 12.5px; margin: 6px 0 12px; }
.searchbox { font-size: 16px; margin-bottom: 16px; }

/* Custom select: the native control drew a hard black rectangle when focused
   and its own chevron fought the type. One chevron, one underline. */
.sel { position: relative; display: inline-flex; align-items: center; min-width: 0; }
.sel::after {
  content: ""; position: absolute; right: 8px; top: 50%; width: 7px; height: 7px;
  border-right: 1.5px solid var(--ink); border-bottom: 1.5px solid var(--ink);
  transform: translateY(-70%) rotate(45deg); pointer-events: none;
}
.sel select {
  -webkit-appearance: none; appearance: none;
  border: none; border-bottom: 1px solid rgba(28, 26, 0, .4);
  background: transparent; font: inherit; font-size: 14px;
  padding: 10px 26px 10px 2px; min-height: 46px; width: 100%;
  cursor: pointer; outline: none; color: var(--ink); border-radius: 0;
}
/* Strong enough to find with a keyboard, without the black rectangle the
   native outline drew around a small control. */
.sel select:focus-visible {
  border-bottom: 2px solid var(--ink); padding-bottom: 9px;
  background: var(--shell);
}
.sel--box { width: 100%; }
.sel--box select { border: 1px solid rgba(28, 26, 0, .35); padding: 12px 30px 12px 12px; font-size: 16px; }
.sel--box select:focus-visible { border: 1px solid var(--ink); box-shadow: inset 0 0 0 1px var(--ink); padding-bottom: 12px; }

.optlist { border: 1px solid var(--rule); }
.opt {
  display: flex; align-items: center; gap: 11px;
  min-height: 54px; padding: 8px 12px;
  border-bottom: 1px solid var(--rule); cursor: pointer; font-size: 14px;
  transition: background .18s var(--ease);
}
.opt:last-child { border-bottom: none; }
.opt:hover { background: var(--hover); }
.opt:has(input:checked) { background: var(--hover); box-shadow: inset 3px 0 0 var(--ink); }
/* The OS draws checkboxes with rounded corners and its own glyph — the last
   browser-default control in a radius-0, ink-drawn system. Drawn by hand;
   radios keep their circle (the shape is the affordance). */
.opt input { flex-shrink: 0; }
.opt input[type="checkbox"], .opt input[type="radio"] {
  appearance: none; -webkit-appearance: none;
  width: 18px; height: 18px; margin: 0;
  border: 1px solid var(--ink); background: var(--page);
  display: grid; place-content: center; cursor: pointer;
}
.opt input[type="radio"] { border-radius: 50%; }
.opt input[type="checkbox"]:checked { background: var(--ink); }
.opt input[type="checkbox"]:checked::before {
  content: ""; width: 9px; height: 5px;
  border-left: 2px solid var(--paper); border-bottom: 2px solid var(--paper);
  transform: rotate(-45deg) translate(1px, -1px);
}
.opt input[type="radio"]:checked::before {
  content: ""; width: 9px; height: 9px; border-radius: 50%; background: var(--ink);
}
.opt__price { margin-left: auto; font-weight: 600; white-space: nowrap; }
.opt__n { font-weight: 400; color: var(--muted); }
.opt--pay { align-items: flex-start; padding-block: 12px; }
.opt--pay input { margin-top: 2px; }
/* The filled row and its 3px ink bar mark which option you picked out of a
   list. The newsletter box is a lone checkbox with no border and no left
   padding, so the bar hung off its edge and the fill read as a grey slab
   across the form. Nothing to mark here — the tick says it. */
.opt--plain { border: none; padding-left: 0; min-height: 46px; }
.opt--plain:has(input:checked) { background: none; box-shadow: none; }
.opt--plain:hover { background: none; }
.opt--plain span { font-size: 13.5px; }
.opt__txt { display: flex; flex-direction: column; min-width: 0; }
.opt__hint { font-size: 11.5px; color: var(--muted); line-height: 1.35; }
.opt__logos { margin-left: auto; display: inline-flex; gap: 5px; align-items: center; flex-shrink: 0; }
.opt__logos svg { height: 20px; width: auto; }

.banks { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 12px; }
.bank {
  display: flex; align-items: center; justify-content: center;
  border: 1px solid var(--rule); background: var(--page); cursor: pointer;
  min-height: 44px; padding: 0 14px; font-size: 13px;
  transition: border-color .18s var(--ease), background .18s var(--ease);
}
.bank:hover { border-color: var(--ink); }
/* Pressed and picked used to fill the tile with --ink and flip the text to
   --paper. That works for the five built-in names, and it destroys the half
   of the real list whose logo is black artwork on nothing — SEB and LHV went
   completely black the moment they were tapped (Renat, 13.09.2026), because
   the mark the tile carries is the same colour as the fill under it. The pick
   is marked the way a chosen product thumbnail is marked instead
   (.pdp__thumb below): the ink ring, drawn as an inset shadow so nothing
   moves by a pixel, over the tile's own light ground — which every logo,
   black or coloured, stays readable on. The unpicked tile is untouched.
   The carrier chips at «Доставка», the step before this one, carry the same
   kind of mark and had gone the same way — .carrier[aria-current="true"] is
   the other half of this decision, made a day later. */
.bank:active { background: var(--hover); }
.bank[aria-current="true"] {
  border-color: var(--ink); background: var(--hover); color: var(--ink);
  box-shadow: inset 0 0 0 1px var(--ink);
}
/* UX fix 9: real bank logo, GET /api/payments/methods — height:24 as briefed,
   width left to the image's own ratio; a 404/timeout swaps it for the bank
   name (the "error" listener near the bottom of app.js) */
.bank__logo { height: 24px; width: auto; display: block; }

/* ---------- catalogue toolbar ---------- */
.toolbar {
  display: grid; gap: 10px 14px; align-items: center; margin-bottom: 14px;
  grid-template-columns: auto 1fr;
}
.toolbar__filter { min-height: 46px; padding: 0 16px; }
/* .btn is a flex row with a gap; an empty count span still claims that gap
   and pushed ФИЛЬТРЫ off centre. */
.toolbar__filter [data-fcount]:empty { display: none; }
.toolbar__count { font-size: 13px; color: var(--muted); }
.toolbar__sort {
  grid-column: 1 / -1; display: flex; align-items: center; gap: 10px;
  font-size: 13px; color: var(--muted); min-width: 0;
  border-top: 1px solid var(--rule-soft); padding-top: 4px;
}
.toolbar__sortlbl { flex-shrink: 0; }
.toolbar__sort .sel { flex: 1; min-width: 0; }
/* Flex, not a fixed 3-column grid: the brand view drops the Фильтры button,
   so with `auto auto 1fr` the sort landed in column 2 and `justify-self: end`
   had nothing to push against — it sat on the left. margin-left:auto is right
   whether the toolbar has two children or three. */
@media (min-width: 640px) {
  .toolbar { display: flex; flex-wrap: wrap; }
  .toolbar__sort {
    margin-left: auto; border-top: none; padding-top: 0; flex: 0 0 auto;
  }
  .toolbar__sort .sel { flex: 0 0 auto; min-width: 168px; }
}

.fchips { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; margin-bottom: 18px; }
.fchips[hidden] { display: none; }
.fchip {
  border: 1px solid var(--ink); background: var(--page); cursor: pointer;
  min-height: 44px; padding: 0 13px; font-size: 12px;
  transition: background .18s var(--ease);
}
.fchip:hover { background: var(--shell); }

.allshown { text-align: center; color: var(--muted); font-size: 13px; margin-top: 28px; }
#sentinel { height: 1px; }
.empty { border: 1px solid var(--rule); padding: 24px; display: flex; flex-direction: column; gap: 10px; align-items: flex-start; }
/* The empty state quotes the shopper's own query back, so it is the one
   paragraph on the page whose content is arbitrary. A single long word has
   nothing to break on and pushed the phone into a sideways scroll — app.js
   clips the echo at 60 characters, and this wraps whatever is left of it. */
.empty p { margin: 0; overflow-wrap: anywhere; }
.empty__cats { display: flex; gap: 8px; flex-wrap: wrap; }

/* ---------- product page ---------- */
.pdp {
  display: grid; gap: 24px 48px; padding: 16px 0 32px;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 320px), 1fr));
}
/* pan-y so a horizontal swipe through the gallery is not stolen by the
   page scroll, exactly as on the hero */
.pdp__stage { position: relative; aspect-ratio: 1; background: var(--page); touch-action: pan-y; }
.pdp__img { inset: 6%; }
.pdp__wm { width: 16px; height: 25px; right: 12px; bottom: 12px; }
.pdp__thumbs { display: flex; gap: 8px; margin-top: 10px; flex-wrap: wrap; }
.pdp__thumb {
  position: relative; width: 58px; height: 58px; border: 1px solid var(--rule);
  background: var(--page); cursor: pointer; padding: 0;
  transition: border-color .18s var(--ease);
}
.pdp__thumb:hover { border-color: var(--ink); }
/* thicken with an inset ring, not border-width — a 2px border shrinks the
   content box and nudged the thumbnail a pixel on every click */
.pdp__thumb[aria-current="true"] { border-color: var(--ink); box-shadow: inset 0 0 0 1px var(--ink); }
.pdp__brand { margin-bottom: 6px; }
.pdp__title { font-size: clamp(20px, 3.4vw, 26px); margin-bottom: 10px; }
.pdp__price { font-size: 22px; font-weight: 600; margin-bottom: 4px; }
.pdp__tax { font-size: 12px; color: var(--muted); margin-bottom: 18px; }
.pdp__vol { font-size: 14px; margin-bottom: 16px; }
.pdp__oos { border: 1px solid var(--rule); padding: 16px; margin: 18px 0 20px; }
.pdp__oos p { margin: 0 0 12px; font-size: 13.5px; }
.pdp__oosrow { display: flex; gap: 8px; }
.pdp__oosrow .input { flex: 1; min-height: 48px; }
/* the same box for one empty VOLUME (r23) — two lines and no form, so the
   last of them does not need the 12px that separates a sentence from an input */
.pdp__oos--size p:last-child { margin-bottom: 0; }
.sizes { display: flex; gap: 8px; flex-wrap: wrap; }
.size {
  min-height: 46px; padding: 0 16px; border: 1px solid var(--ink);
  cursor: pointer; background: transparent; color: var(--ink); font-size: 14px;
  transition: background .18s var(--ease), color .18s var(--ease);
}
.size:hover { background: var(--shell); }
.size[aria-current="true"] { background: var(--ink); color: var(--paper); }
/* per-size stock (r23): a volume somebody counted down to zero. Greyed and
   struck through rather than removed — the shopper who came for the 500 мл
   has to see that it exists and is empty, not wonder whether this shop sells
   it at all. Still 46px tall and still pressable (variantPicker says why): an
   inert button on a phone reads as a broken one, so it stays tappable and the
   buy row it selects says what is wrong. `opacity` is deliberately not used —
   it would take the label under the 4.5:1 contrast the rest of the shop
   holds, and this is text a shopper has to be able to read. */
.size--out { border-color: var(--rule); color: var(--muted); text-decoration: line-through; }
.size--out:hover { background: transparent; }
.size--out[aria-current="true"] { background: var(--shell); color: var(--muted); }
/* …and the same news on a basket line whose volume has gone since (r23). Two
   classes on purpose: .cline__parts is declared further down this file and
   paints itself --muted, so a single .cline__gone here would lose the cascade
   on source order and the warning would read as one more grey caption. */
.cline__parts.cline__gone { color: var(--error); }
.pdp__buy { display: flex; gap: 12px; align-items: center; margin: 18px 0; }
.pdp__add { flex: 1; }
.pdp__alt { text-align: center; margin-bottom: 20px; }
.pdp__ship { font-size: 13px; color: var(--muted); border-top: 1px solid var(--rule); padding-top: 14px; }

.stepper { display: inline-flex; border: 1px solid var(--ink); flex-shrink: 0; }
.stepper button { width: 46px; height: 48px; border: none; background: none; cursor: pointer; font-size: 16px; transition: background .18s var(--ease); }
.stepper button:hover { background: var(--hover); }
/* The «−» side stops at qty 1 — removal only happens through the explicit
   «Убрать» control. aria-disabled (not the disabled attribute) so the button
   stays reachable by Tab; pointer-events blocks the click, the qty handler
   in app.js still guards the state change itself for a keyboard activation. */
.stepper button[aria-disabled="true"] { opacity: .35; cursor: not-allowed; pointer-events: none; }
.stepper > span { width: 42px; height: 48px; display: flex; align-items: center; justify-content: center; }
.stepper--sm button { width: 44px; height: 44px; font-size: 14px; }
.stepper--sm > span { width: 34px; height: 44px; font-size: 13px; }

/* ---------- accordions ---------- */
.acc { border-bottom: 1px solid var(--rule); }
.acc summary {
  cursor: pointer; padding: 15px 26px 15px 0; font-size: 14px; font-weight: 500;
  list-style: none; position: relative;
}
.acc summary::-webkit-details-marker { display: none; }
.acc summary::after {
  content: "+"; position: absolute; right: 4px; top: 50%; transform: translateY(-50%);
  font-size: 18px; line-height: 1; color: var(--muted);
}
.acc[open] summary::after { content: "−"; }
.acc__body { font-size: 13.5px; color: rgba(28, 26, 0, .75); padding-bottom: 15px; }

/* ---------- account ---------- */
.acct__top { display: flex; justify-content: space-between; align-items: baseline; }
/* Every field of the profile form saves itself (app.js acctQueue) — there
   is no Save button and no bar: the bar that used to follow the page to the
   bottom was covered by the phone's keyboard exactly while a field was
   being typed into (Dim, 12.09.2026). The line under a control says where
   its own save stands — «Сохраняем…», «Сохранено ✓» for a moment, the
   refusal's words — and is painted in place while the shopper types on. It
   keeps its height while empty, so a line coming and going never moves the
   field below it under a finger. */
.field--st { margin-bottom: 0; }
.acctst { min-height: 16px; line-height: 16px; font-size: 12.5px; color: var(--muted); margin: 3px 0 6px; }
.acctst--ok { color: var(--ok); }
.acctst--err { color: var(--error); }
.rowcard { border: 1px solid var(--rule); padding: 14px; display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
.rowcard__id { font-weight: 600; }
.rowcard__code { font-size: 13px; letter-spacing: .08em; }
.rowcard__act { margin-left: auto; }
/* features: «Скачать подарочную карту (PDF)» on the order that bought one.
   Its own full-width line under the order's facts — a card is a thing to do,
   not another fact, and several cards on one order stack rather than fighting
   the row for space. */
.rowcard__gifts { flex-basis: 100%; display: flex; flex-direction: column; align-items: flex-start; gap: 4px; }
/* returns: «Хочу вернуть заказ» — the tick, and the line it turns into once
   it has been ticked. Its own full-width row under the order's facts, for the
   same reason the gift-card links have one: it is a thing to do, not another
   fact about the order, and on a phone it must not fight the chips for space. */
.rowcard__ret { flex-basis: 100%; }
.rowcard__retdone { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
.rowcard__retnote { flex-basis: 100%; font-size: 12.5px; }
/* loyalty: what a refund did to the order's points — its own line under the
   order's facts, like the two above (acctOrderRow, 26.09.2026). */
.rowcard__pts { flex-basis: 100%; }
/* «Мои заказы», 28.09.2026 (Dim): the parcel in words, the order behind a
   tap, the receipt on the documents line (acctParcelHTML, acctOrderMoreHTML).
   The row's own idiom — each a full-width line of its own — and, inside the
   opened row, the checkout summary's rows (.cosum__line, .cosum__row,
   .cosum__row--tot): an order reads the way its basket did. No new colour —
   ink for what matters, --muted for the rest, the hairlines already here. */
.rowcard__parcel { flex-basis: 100%; display: flex; flex-direction: column; gap: 2px; }
.rowcard__pstate { color: var(--ink); font-weight: 500; }
.rowcard__phint { font-size: 12.5px; }
/* The accordions' opener (.acc summary) as a real <button>: 14px/500, a «+»
   that turns «−», a hairline above, the 44px tap floor. */
.rowcard__more {
  flex-basis: 100%; position: relative; display: flex; align-items: center;
  min-height: 44px; padding: 0 26px 0 0;
  background: none; border: none; border-top: 1px solid var(--rule-soft); border-radius: 0;
  font: inherit; font-size: 14px; font-weight: 500; color: var(--ink); text-align: left; cursor: pointer;
}
.rowcard__more::after {
  content: "+"; position: absolute; right: 4px; top: 50%; transform: translateY(-50%);
  font-size: 18px; line-height: 1; color: var(--muted); transition: color .18s var(--ease);
}
.rowcard__more:hover::after { color: var(--ink); }
.rowcard__more[aria-expanded="true"]::after { content: "−"; }
/* opening fades in like a checkout step (stepIn) — opacity only, nothing reflows twice */
.rowcard__det { flex-basis: 100%; display: flex; flex-direction: column; gap: 10px; padding-bottom: 2px; animation: stepIn .28s var(--ease); }
.rowcard__det[hidden] { display: none; }
.rowcard__det .cosum__line { align-items: flex-start; }
.rowcard__det .cosum__row { gap: 12px; }
.rowcard__sub { display: block; font-size: 12.5px; color: var(--muted); }
.rowcard__dnote { margin: -6px 0 0; font-size: 12.5px; }
.rowcard__pay { text-align: right; }
@media (prefers-reduced-motion: reduce) { .rowcard__det { animation: none; } }

/* ---------- checkout ---------- */
.cohdr {
  position: sticky; top: 0; z-index: var(--z-nav);
  background: var(--page); border-bottom: 1px solid var(--rule);
}
.cohdr .wrap--co { display: flex; align-items: center; gap: 14px; padding-block: 12px; }
/* the admin header spans the panes' own full-bleed column so the logo lines
   up with the left nav, not with a phantom 1020px checkout column */
/* 22px = the side pane's 12px padding + the nav buttons' 10px inset, so the
   logo sits flush over the section labels below it */
.cohdr--adm .cohdr__row { display: flex; align-items: center; gap: 14px; padding: 12px 22px; max-width: 1400px; margin: 0 auto; }
@media (max-width: 560px) { .cohdr--adm .cohdr__row > .link { margin-left: auto; } }
.cohdr__t {
  margin-left: auto; margin-right: auto;
  font-family: Oswald, sans-serif; font-size: 11.5px; letter-spacing: .18em; text-transform: uppercase;
  /* the checkout's is an <h1> now (the page's one heading) — drawn exactly
     as the span it used to be, not with the heading defaults */
  font-weight: 400; line-height: 1.55;
}
@media (max-width: 560px) {
  /* off the phone's narrow row, but still the page's heading for a screen
     reader — visually hidden, not display:none */
  .cohdr__t {
    position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
    overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
  }
  .cohdr .wrap--co > .link { margin-left: auto; }
}

/* language pills in the checkout header — the shopper may realise mid-payment
   that the form is in the wrong language, and the full header is hidden here */
.cohdr__langs { display: flex; gap: 2px; }
.cohdr__lang {
  border: 1px solid transparent; background: none; cursor: pointer;
  padding: 4px 7px; min-height: 32px; color: var(--muted);
  font-family: Oswald, sans-serif; font-size: 11px; font-weight: 600; letter-spacing: .08em;
}
.cohdr__lang[aria-current="true"] { color: var(--ink); border-color: var(--ink); }
.cosum__rm { font-size: 12px; color: var(--muted); margin-left: 6px; }

/* ---------- cart upsell ---------- */
.upsell { margin-top: 14px; border-top: 1px solid var(--rule); padding-top: 12px; }
.upsell__t {
  font-family: Oswald, sans-serif; font-size: 11px; letter-spacing: .12em;
  text-transform: uppercase; color: var(--muted); margin-bottom: 8px;
}
.upsell__row { display: flex; gap: 9px; align-items: center; padding: 5px 0; }
.upsell__ph {
  width: 40px; height: 40px; flex: 0 0 40px; position: relative;
  background-size: contain; background-position: center; background-repeat: no-repeat;
}
.upsell__nm {
  flex: 1; min-width: 0; border: none; background: none; cursor: pointer;
  font: inherit; font-size: 12.5px; line-height: 1.3; text-align: left; padding: 0;
  overflow-wrap: anywhere;
}
.upsell__row .btn--sm { min-height: 36px; min-width: 36px; padding: 0 10px; }

/* ---------- reviews ---------- */
.revs__avg { font-family: Oswald, sans-serif; font-size: 15px; letter-spacing: .04em; margin-bottom: 12px; }
.rev { padding: 10px 0; border-top: 1px solid var(--rule); }
.rev__head { display: flex; gap: 10px; align-items: baseline; font-size: 13px; }
.rev__stars { color: var(--ink); letter-spacing: 2px; font-size: 12px; }
.rev__t { margin: 5px 0 0; font-size: 13.5px; line-height: 1.5; }

/* ---------- shop assistant chat (demo) ---------- */
.sbot__fab {
  position: fixed; right: 16px; bottom: calc(72px + env(safe-area-inset-bottom, 0px));
  z-index: 60; width: 52px; height: 52px; border-radius: 50%;
  background: var(--ink); color: #fff; border: none; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  box-shadow: 0 6px 20px rgba(28, 26, 0, .28);
}
.sbot__fab svg { width: 24px; height: 24px; }
@media (min-width: 768px) { .sbot__fab { bottom: 20px; } }
/* the product page and checkout park a sticky buy bar above the bottom nav
   on phones — the bubble sits above it instead of on the button */
@media (max-width: 767px) {
  body[data-screen="product"] .sbot__fab,
  body[data-screen="checkout"] .sbot__fab { bottom: calc(140px + env(safe-area-inset-bottom, 0px)); }
  body[data-screen="product"] .sbot__panel,
  body[data-screen="checkout"] .sbot__panel { bottom: calc(200px + env(safe-area-inset-bottom, 0px)); }
}
.sbot__panel {
  position: fixed; right: 12px; bottom: calc(132px + env(safe-area-inset-bottom, 0px));
  z-index: 61; width: min(360px, calc(100vw - 24px)); max-height: min(540px, 72vh);
  background: var(--paper, #fff); border: 1px solid var(--ink);
  display: flex; flex-direction: column; box-shadow: 0 14px 44px rgba(28, 26, 0, .25);
}
/* display:flex above beats the hidden attribute's UA display:none — without
   this the "closed" panel stayed on screen, empty, and ✕ looked broken */
.sbot__panel[hidden] { display: none; }
/* a drawer owns the screen on the phone — the chat bubble was sitting on
   top of the cart's «Итого» */
body.is-locked .sbot { display: none; }
@media (min-width: 768px) { .sbot__panel { bottom: 84px; } }
.sbot__head {
  display: flex; align-items: baseline; gap: 8px; padding: 12px 14px;
  border-bottom: 1px solid var(--rule);
  font-family: Oswald, sans-serif; letter-spacing: .06em; text-transform: uppercase; font-size: 13px;
}
.sbot__hint { font-family: "Golos Text", sans-serif; text-transform: none; letter-spacing: 0; font-size: 11px; color: var(--muted); flex: 1; }
.sbot__x { border: none; background: none; cursor: pointer; font-size: 14px; padding: 4px; min-width: 32px; min-height: 32px; }
.sbot__log { overflow-y: auto; padding: 12px; display: flex; flex-direction: column; gap: 10px; flex: 1; min-height: 140px; }
.sbot__msg { max-width: 92%; padding: 9px 12px; font-size: 13.5px; line-height: 1.45; }
.sbot__msg--bot { background: var(--shell, #edeae1); align-self: flex-start; }
.sbot__msg--me { background: var(--ink); color: #fff; align-self: flex-end; }
/* the article the assistant points at (chat.js withBlogLinks): the shop's own
   ink, underlined, never the browser's blue inside the bubble */
.sbot__msg a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
.sbot__prod { display: flex; gap: 9px; align-items: center; margin-top: 10px; }
.sbot__ph {
  width: 44px; height: 44px; flex: 0 0 44px;
  background-size: contain; background-position: center; background-repeat: no-repeat;
  background-color: #fff; border: 1px solid var(--rule);
}
.sbot__pn { flex: 1; font-size: 12px; line-height: 1.3; min-width: 0; }
.sbot__pp { display: block; font-weight: 600; margin-top: 2px; }
.sbot__pact { display: flex; flex-direction: column; gap: 4px; }
.sbot__mini {
  border: 1px solid var(--ink); background: var(--ink); color: #fff;
  font-size: 11px; padding: 5px 8px; cursor: pointer; white-space: nowrap;
}
.sbot__mini--ghost { background: none; color: var(--ink); }
.sbot__all { margin-top: 10px; width: 100%; padding: 9px; }
.sbot__chips { display: flex; gap: 6px; flex-wrap: wrap; padding: 0 12px 10px; }
.sbot__chip {
  border: 1px solid var(--rule); background: none; cursor: pointer;
  font-size: 12px; padding: 6px 10px; min-height: 32px;
}
.sbot__form { display: flex; border-top: 1px solid var(--rule); }
.sbot__form input { flex: 1; border: none; padding: 12px 14px; font: inherit; font-size: 14px; min-width: 0; outline-offset: -2px; }
.sbot__send { border: none; background: var(--ink); color: #fff; width: 48px; cursor: pointer; font-size: 16px; }
/* «Развернуть» in the panel's header (chat.js setWide): the column becomes a
   ~720-px, 80-vh window for reading product rows side by side, remembered
   for the session. Behind the same 768-px line as the rest — a phone never
   loads the assistant at all, and a window dragged narrow keeps the column. */
.sbot__wide {
  align-self: center; display: inline-flex; align-items: center; gap: 6px;
  min-height: 32px; padding: 0 10px; border: 1px solid var(--rule); background: none; cursor: pointer;
  font-family: "Golos Text", sans-serif; font-size: 12px; letter-spacing: 0; text-transform: none; color: var(--ink);
}
.sbot__wide:hover { border-color: var(--ink); }
.sbot__wide svg { width: 14px; height: 14px; }
@media (min-width: 768px) {
  .sbot--wide .sbot__panel { width: min(720px, calc(100vw - 24px)); height: 80vh; max-height: 80vh; }
  .sbot--wide .sbot__msg { max-width: 76%; font-size: 14.5px; }
  .sbot--wide .sbot__ph { width: 56px; height: 56px; flex-basis: 56px; }
  .sbot--wide .sbot__pn { font-size: 13px; }
}
/* the reminder bubble beside the button (chat.js, NUDGE_* at its top). It is
   a polite live region that sits in the page EMPTY — no box, no tail — so a
   screen reader announces the text when it goes in and focus stays put. */
.sbot__nudge {
  position: fixed; right: 80px; bottom: 22px; z-index: 60;
  display: flex; align-items: center; gap: 2px; max-width: min(300px, calc(100vw - 110px));
  padding: 6px 6px 6px 14px; background: var(--paper, #fff); border: 1px solid var(--ink);
  box-shadow: 0 10px 28px rgba(28, 26, 0, .18); animation: sbotNudge .32s ease-out;
}
.sbot__nudge:empty { padding: 0; border: 0; box-shadow: none; animation: none; pointer-events: none; }
.sbot__nudge::after {
  content: ""; position: absolute; right: -7px; top: 50%; width: 11px; height: 11px;
  background: var(--paper, #fff); border-top: 1px solid var(--ink); border-right: 1px solid var(--ink);
  transform: translateY(-50%) rotate(45deg);
}
.sbot__nudge:empty::after { display: none; }
.sbot__nudge-go {
  border: none; background: none; cursor: pointer; padding: 6px 4px 6px 0; text-align: left;
  font: inherit; font-size: 13.5px; line-height: 1.35; color: var(--ink); text-underline-offset: 3px;
}
.sbot__nudge-go:hover { text-decoration: underline; }
.sbot__nudge-x { border: none; background: none; cursor: pointer; font-size: 13px; min-width: 32px; min-height: 32px; color: var(--muted); flex: none; }
.sbot__nudge-x:hover { color: var(--ink); }
@keyframes sbotNudge { from { opacity: 0; transform: translateY(6px); } }
@media (prefers-reduced-motion: reduce) { .sbot__nudge { animation: none; } }

/* minmax(0,…) not 1fr: a grid item's automatic minimum size is its
   min-content width, and the nowrap step summary ("изменить" + the chosen
   value) pushed the whole column past the viewport. */
.co { display: grid; gap: 24px; padding-top: 20px; grid-template-columns: minmax(0, 1fr); }
.co > * { min-width: 0; }
/* Phone: the collapsed «Ваш заказ» belongs above the form, not below three
   steps and a reassurance list where it falls off the bottom of the page. */
@media (max-width: 767px) { .co__sum { order: -1; } }
/* No `align-items: start` — that collapses the summary column to its content
   height, leaving `position: sticky` inside it nothing to travel through.
   Two columns start at the same width where `wide()` flips, so the summary is
   never force-opened while still stacked under the form. */
@media (min-width: 768px) { .co { grid-template-columns: minmax(0, 1fr) 360px; gap: 40px; } }
@media (min-width: 1000px) { .co { grid-template-columns: minmax(0, 1fr) 380px; gap: 46px; } }

.costep { border-bottom: 1px solid var(--rule); }
.costep__head {
  width: 100%; min-width: 0; display: flex; align-items: center; gap: 12px;
  background: none; border: none; cursor: pointer; text-align: left;
  padding: 16px 0; font: inherit;
}
.costep__n {
  width: 26px; height: 26px; flex-shrink: 0;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid var(--ink); font-family: Oswald, sans-serif; font-size: 12px;
}
.costep.is-open .costep__n { background: var(--ink); color: var(--paper); }
.costep__n--done { background: var(--ok); border-color: var(--ok); color: #fff; }
.costep__t {
  font-family: Oswald, sans-serif; font-size: 13px; letter-spacing: .18em; text-transform: uppercase;
  color: var(--muted); flex-shrink: 0;
}
.costep.is-open .costep__t { color: var(--ink); }
.costep__v {
  margin-left: auto; min-width: 0; font-size: 13px; color: var(--muted); text-align: right;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.costep__edit { font-size: 12px; color: var(--muted); border-bottom: 1px solid var(--rule); flex-shrink: 0; transition: color .18s var(--ease), border-color .18s var(--ease); }
.costep__head:hover .costep__edit { color: var(--ink); border-bottom-color: var(--ink); }
.costep__body { padding-bottom: 24px; animation: stepIn .28s var(--ease); }
/* UX fix 7, real root cause: an ANIMATED transform on an ancestor makes it a
   containing block for position:fixed descendants for as long as the
   animation runs (CSS Transforms spec) — .psheet (the parcel-machine sheet)
   is one, nested inside .costep__body two levels down. With transform in
   this keyframe, opening the picker on step 2 re-renders .costep__body fresh
   (every render() replaces the whole step body) with stepIn restarting, and
   .psheet spent its first ~280ms positioned against that box instead of the
   viewport — hence "jumps from left to centre" the moment stepIn ended and
   .psheet snapped to its real position. Fading alone leaves nothing here for
   position:fixed to key off. */
@keyframes stepIn { from { opacity: 0; } }
.co__zip { display: grid; grid-template-columns: 118px 1fr; gap: 12px; }
/* the account's «Курьер до двери» address — the checkout's boxes (acctAddrHTML) */
.acct__addr { margin-top: 8px; }
.cosrc { font-size: 11.5px; color: var(--muted); margin: 0 0 16px; line-height: 1.45; }
.cotrust {
  list-style: none; margin: 26px 0 0; padding: 0;
  display: grid; gap: 8px; font-size: 12.5px; color: var(--muted);
}
.cotrust li { padding-left: 18px; position: relative; }
.cotrust li::before {
  content: ""; position: absolute; left: 0; top: 7px; width: 8px; height: 4px;
  border-left: 1.5px solid var(--ok); border-bottom: 1.5px solid var(--ok);
  transform: rotate(-45deg);
}

.cosum { border: 1px solid var(--rule); }
@media (min-width: 768px) { .cosum { position: sticky; top: 84px; } }
.cosum__head {
  list-style: none; cursor: pointer; display: flex; align-items: center; gap: 12px;
  padding: 15px 34px 15px 16px; position: relative;
}
.cosum__head::-webkit-details-marker { display: none; }
.cosum__head::after {
  content: "+"; position: absolute; right: 14px; top: 50%; transform: translateY(-50%);
  font-size: 18px; color: var(--muted);
}
.cosum[open] .cosum__head::after { content: "−"; }
.cosum__tot { margin-left: auto; font-weight: 600; }
.cosum__body { padding: 0 16px 16px; display: flex; flex-direction: column; gap: 12px; }
.cosum__line { display: flex; gap: 11px; align-items: center; }
.cosum__ph { position: relative; width: 46px; height: 46px; flex-shrink: 0; }
.cosum__nm { flex: 1; font-size: 13px; }
.cosum__pr { font-size: 13px; font-weight: 600; }
.cosum__promo { display: flex; gap: 8px; border-top: 1px solid var(--rule); padding-top: 12px; }
.cosum__promo .input { flex: 1; min-height: 44px; font-size: 15px; }
.cosum__row { display: flex; justify-content: space-between; font-size: 13px; }
.cosum__row--rule { border-top: 1px solid var(--rule); padding-top: 12px; }
.cosum__row--tot { border-top: 1px solid var(--ink); padding-top: 12px; font-size: 16px; font-weight: 600; }
.cosum__legal { font-size: 11px; color: var(--muted); margin: 0; }

/* The sticky bar already carries Оплатить on the phone; a second identical
   button inside the summary read as two different actions. */
@media (max-width: 767px) { .co__pay { display: none; } }

/* ---------- the cabinet's points ledger ----------
   The admin panel is entirely its own design system now (public/shop2/admin.css,
   `.adm2` / `adm-*`) and borrows nothing from here. The four rules below wear
   the panel's old prefix for history's sake but style exactly one thing: the
   «Мои баллы» history rows on the account screen (loyaltyRowHTML). */

.adm__sub { display: block; font-size: 11.5px; color: var(--muted); }

.adm__list { border: 1px solid var(--rule); margin-top: 6px; }
.adm__row {
  display: flex; align-items: center; gap: 12px; padding: 10px 14px;
  border-bottom: 1px solid var(--rule-soft); font-size: 13.5px; flex-wrap: wrap;
}
.adm__row:last-child { border-bottom: none; }
.adm__nm { flex: 1 1 200px; min-width: 0; }

/* ---------- socials ----------
   Bare marks, no boxes. They sit in the contact column's own text rhythm —
   flush with the phone number, one row height like the links above them, so
   the 44px touch target is bled outwards rather than pushing the column. */
.socials { display: flex; gap: 10px; margin: 6px 0 0; }
/* Left-aligned with the phone number above: the glyph starts at the column
   edge and the 44px touch target is grown downwards and to the right, not
   centred — centring pushed the first icon 10px out of the column. */
.social {
  display: inline-flex; align-items: center; justify-content: flex-start;
  width: 30px; height: 40px; color: var(--ink); opacity: .8;
  transition: opacity .18s var(--ease), transform .18s var(--ease);
}
.social:hover { opacity: 1; transform: translateY(-1px); }
.social svg { width: 20px; height: 20px; }

/* ---------- sticky mobile action bar ---------- */
.stickybar {
  position: fixed; left: 0; right: 0; z-index: var(--z-bar);
  bottom: calc(var(--bottomnav-h) + env(safe-area-inset-bottom));
  background: var(--page); border-top: 1px solid var(--ink);
  padding: 10px var(--gutter);
  display: flex; align-items: center; gap: 12px;
  min-height: var(--bar-h);
}
.stickybar .btn { flex: 1; }
.stickybar__sum { font-weight: 600; }
.stickybar__tot { display: flex; flex-direction: column; line-height: 1.2; flex-shrink: 0; }
.stickybar__tot span:first-child { font-size: 11px; color: var(--muted); }
.stickybar__tot .num { font-weight: 600; font-size: 16px; }
@media (min-width: 768px) { .stickybar { display: none; } }

/* ---------- footer ---------- */
.ftr {
  border-top: 2px solid var(--ink); margin-top: var(--s6);
  padding-bottom: calc(var(--bottomnav-h) + env(safe-area-inset-bottom) + 16px);
}
/* Not zero: two round buttons are parked in the bottom corners of every
   screen — the assistant on the right (.sbot__fab, bottom 20px, 52px tall)
   and «Оставить комментарий» on the left — and the footer's last row puts
   «Админка» in the right corner and the © line in the left one. With no
   padding the page simply ends there, so scrolled to the bottom the bubble
   sat on the link. Ренат, 20.09.2026, on the iPad: «админка поправить».
   80px is the fab's own 20 + 52 and room to see the link is not under it. */
@media (min-width: 768px) { .ftr { padding-bottom: 80px; } }
.ftr__cols {
  display: grid; gap: 22px 28px; padding: var(--s5) 0;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 190px), 1fr));
  font-size: 13px; color: rgba(28, 26, 0, .75);
}
.ftr__h { display: block; font-family: Oswald, sans-serif; font-size: 11px; letter-spacing: .18em; text-transform: uppercase; color: var(--ink); margin-bottom: 7px; }
/* stacked links with no gap: 26px rows sharing edges is a mis-tap generator */
.ftr__cols a { display: block; padding: 9px 0; }
.ftr__cols a:hover { text-decoration: underline; text-underline-offset: 3px; }
.ftr__bottom {
  display: flex; justify-content: space-between; align-items: center; gap: 14px; flex-wrap: wrap;
  border-top: 1px solid var(--rule); padding: 16px 0 24px; font-size: 12px; color: var(--muted);
}
.ftr__sig { display: inline-flex; gap: 9px; align-items: center; white-space: nowrap; }
.ftr__mark { height: 18px; width: auto; opacity: .75; flex-shrink: 0; }

/* Card marks are the real artwork, so they carry their own colour and their
   own aspect ratio. A fixed-height tile keeps the row optically even. */
/* No tile borders. The Apple Pay mark ships inside its own rounded frame, so
   any box we draw makes it the odd one out. The viewBoxes are tight-cropped
   to the artwork (see paylogos.js), so these heights are what actually
   renders — each mark sized to match the others' optical weight, not their
   canvas. */
.paylogos { display: inline-flex; gap: 12px; align-items: center; flex-wrap: wrap; }
.paylogos__item {
  height: 26px; display: inline-flex; align-items: center; justify-content: center;
}
.paylogos__item svg { max-height: 100%; width: auto; }
.paylogos__item--bank { color: var(--ink); }
.paylogos__item--visa svg, .paylogos__item--mastercard svg { height: 26px; }
/* both wallet wordmarks are bare now — matched by cap height, not canvas */
.paylogos__item--applepay svg { height: 16px; }
.paylogos__item--gpay svg { height: 14px; }
.paylogos__item--bank svg { height: 18px; }
/* the wallet rows in checkout reuse the marks at a smaller scale */
.opt__logos svg[aria-label="Google Pay"] { height: 12px; }
.opt__logos svg[aria-label="Apple Pay"] { height: 14px; }

/* ---------- misc ---------- */
.crumbs { font-size: 12.5px; color: var(--muted); padding-top: 16px; }
.crumbs button { border: none; background: none; padding: 0; cursor: pointer; color: inherit; }
.crumbs button:hover { text-decoration: underline; }

.spinner {
  width: 22px; height: 22px; border: 2px solid var(--rule); border-top-color: var(--ink);
  border-radius: 50%; animation: spin .7s linear infinite; margin: 26px auto;
}
/* Reserve the row but only spin while a fetch is actually in flight — a
   permanent spinner under the grid reads as a stall. */
.spinner[data-on="0"] { visibility: hidden; }
@keyframes spin { to { transform: rotate(360deg); } }

.toast {
  position: fixed; left: 50%; transform: translateX(-50%);
  bottom: calc(var(--bottomnav-h) + 20px + env(safe-area-inset-bottom));
  z-index: var(--z-toast); background: var(--ink); color: var(--paper);
  width: min(430px, 92vw); padding: 12px 14px; display: flex; gap: 12px; align-items: center;
  animation: toastUp .35s var(--ease); font-size: 13px;
}
.toast > span { flex: 1; }
.toast__x { color: var(--paper); min-width: 44px; min-height: 44px; padding: 4px; }
@keyframes toastUp { from { opacity: 0; transform: translate(-50%, 12px); } }
@media (min-width: 768px) { .toast { bottom: 24px; } }

.done__tick { color: var(--ok); margin: 0 auto 12px; width: 46px; }
.done__tick svg { width: 46px; height: 46px; stroke-width: 1.6; }

/* ---------- intro ---------- */
.intro {
  position: fixed; inset: 0; z-index: var(--z-intro); background: var(--page);
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 18px;
  cursor: pointer; border: none; width: 100%; color: inherit; font: inherit;
}
.intro svg { height: 92px; width: auto; }
.intro__word { font-family: Oswald, sans-serif; font-size: 15px; text-transform: uppercase; letter-spacing: .1em; animation: spread .9s var(--ease) .9s both; }
@keyframes spread { from { letter-spacing: .1em; opacity: 0; } to { letter-spacing: .42em; opacity: 1; } }
.intro--out { animation: fadeOut .5s ease forwards; }
@keyframes fadeOut { to { opacity: 0; visibility: hidden; } }
.tw-draw { fill: none; stroke: var(--ink); stroke-width: 2; }
.intro .tw-draw { stroke-dasharray: 1; stroke-dashoffset: 1; animation: draw 1.3s var(--ease-inout) .1s forwards; }
.intro .tw-fill { opacity: 0; animation: fillIn .6s ease 1.15s forwards; }
/* Both start hidden INSIDE the keyframe, so the rule above can rest visible. */
@keyframes draw { from { stroke-dashoffset: 1; } to { stroke-dashoffset: 0; } }
@keyframes fillIn { 0%, 66% { opacity: 0; } to { opacity: 1; } }

/* ---------- feedback widget ----------
   Must clear the fixed bottom nav and never sit above the sticky action bars
   or drawers — it was covering the cart total and the right edge of ОПЛАТИТЬ. */
/* Where this thing sits has now bitten twice, so the reasoning is written
   down. It is fixed-position, so it always covers *something*; the job is to
   pick the column that carries no controls.
     Phone: the product grid is two columns and every «В корзину» link is
   left-aligned in its column, so the LEFT edge is the worst place — that is
   what put the bubble on top of the first card's add link. Right edge it is.
     Desktop: the checkout summary and its total live in the right column, so
   the right edge is the worst place there. Left edge it is. */
#rempire-feedback > button[title="Оставить комментарий"] {
  bottom: calc(var(--bottomnav-h) + 12px + env(safe-area-inset-bottom)) !important;
  right: 12px !important; left: auto !important;
  z-index: 25 !important;
}
/* `:has()` carries the specificity of its argument, so this must stay inside
   the phone query or it outranks the desktop reset — and `.stickybar` is
   `display: none` on a desktop but still in the DOM, so it would match there. */
@media (max-width: 767px) {
  body:has(.stickybar) #rempire-feedback > button[title="Оставить комментарий"] {
    bottom: calc(var(--bottomnav-h) + var(--bar-h) + 12px + env(safe-area-inset-bottom)) !important;
  }
}
@media (min-width: 768px) {
  #rempire-feedback > button[title="Оставить комментарий"] {
    bottom: 16px !important; left: 16px !important; right: auto !important; z-index: 25 !important;
  }
}
body:has(.drawer) #rempire-feedback { display: none; }

/* ---------- reduced motion ---------- */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
  }
}

/* cover fill last: merch is a model shot, it fills its frame */
.ph--cover { inset: 0; background-size: cover; }
/* media: a product with no photograph yet — the owner has just created it, or
   its one photo row carries no usable url. The shop's own mark instead of an
   empty square (media(), app.js): a name and a price over nothing reads as a
   broken page, the mark reads as «no photograph yet». */
.ph--mark { display: flex; align-items: center; justify-content: center; }
.ph__mark { width: 46%; height: 62%; color: var(--ink); opacity: .18; }

/* ---------- share ----------
   This used to sit under «Другие способы оплаты» wearing the same .link dress:
   two identical centred links 8px apart, one of which opens checkout — and at
   37px it was under the 44px touch floor. Sharing is metadata about the
   product, not a step in buying it, so it moved off the CTA stack and onto the
   brand line, typeset in that line's register so the row reads as one byline
   rather than a control bolted above the title. */
.pdp__idrow { display: flex; align-items: center; gap: 16px; margin-bottom: 6px; }
.pdp__idrow .pdp__brand { margin-bottom: 0; }
.pdp__share {
  margin-left: auto; position: relative;
  border: none; background: none; padding: 0; cursor: pointer;
  display: inline-flex; align-items: center; gap: 6px;
  font: inherit; font-size: 10px; letter-spacing: .14em; text-transform: uppercase;
  color: var(--muted); white-space: nowrap;
  transition: color .18s var(--ease);
}
.pdp__share:hover { color: var(--ink); }
.pdp__share svg { width: 14px; height: 14px; stroke-width: 1.6; }
/* 46px of tap without 46px of row: a real height here would push the title
   down the column, so the hit area is grown past the visual bounds instead.
   Grown further down than up — below is the title, which is not interactive,
   while above is the thumbnail strip, which needs its 8px of clearance. */
.pdp__share::after { content: ""; position: absolute; inset: -12px -10px -18px -12px; }


/* ================= v2 — Renat's feedback 31.08 ================= */

/* #1 + #6 — the header follows the hero. Over the hero it wears the hero's
   shell ground and whitens under the cursor (na-kd); scrolled past, or on any
   other screen, it is plain white. The ::before bleeds the same background
   one viewport upward: iOS Safari's collapsing address bar exposes a strip
   above the sticky header mid-gesture, and with the bleed that strip shows
   header colour instead of reading as a hole. */
.hdr { transition: background .25s var(--ease); }
.hdr::before {
  content: ""; position: absolute; left: 0; right: 0; bottom: 100%; height: 140px;
  background: inherit; pointer-events: none;
}
/* The beige-over-hero and its hover-whitening are a desktop pointer story;
   on a phone there is no hover to reveal the white, so the header simply
   stays white there — the Safari bleed then masks the gap in white too. */
@media (min-width: 768px) {
  html[data-tint] .hdr { background: var(--shell); border-bottom-color: var(--shell-dark); }
  html[data-tint] .hdr:hover { background: var(--page); }
}

/* #4 — brand strip under the hero, one scrollable line, D-urban register:
   quiet grey wordmarks that ink up under the cursor. */
.brandstrip { border-bottom: 1px solid var(--rule); background: var(--page); }
/* the urban prototype's line, copied: Oswald 600 · 19px · .08em · quiet grey,
   28px gaps, native scrollbar left visible so a desktop knows the row moves */
.brandstrip__row {
  display: flex; align-items: center; gap: 28px;
  max-width: var(--maxw); margin-inline: auto;
  padding: 10px var(--gutter) 12px;
  overflow-x: auto; -webkit-overflow-scrolling: touch;
  scrollbar-width: thin; scrollbar-color: var(--shell-dark) transparent;
}
.brandstrip__row::-webkit-scrollbar { height: 6px; }
.brandstrip__row::-webkit-scrollbar-thumb { background: var(--shell-dark); border-radius: 3px; }
.brandstrip__row::-webkit-scrollbar-track { background: transparent; }
.brandstrip__it {
  border: none; background: none; padding: 2px 0; cursor: pointer; flex: 0 0 auto;
  font-family: Oswald, sans-serif; font-weight: 600; font-size: 19px; letter-spacing: .08em;
  /* was rgba(28, 26, 0, .35) — flattened to #b0afa6 on the strip's white
     background, a 2.2:1 contrast ratio that failed WCAG AA (needs 4.5:1).
     The design system's own muted-text token reads as the same quiet grey
     and is already proven to pass elsewhere on this page (~5.4:1 on white). */
  text-transform: uppercase; color: var(--muted); white-space: nowrap;
  transition: color .18s var(--ease);
}
.brandstrip__it:hover { color: var(--ink); }
/* socials share the © line — truly centred. margin:auto in the old flex row
   centred them between the neighbours, and © is wider than «Админка», so the
   icons sat visibly off the page's axis. Equal 1fr side columns pin the
   middle cell to the actual centre regardless of what the sides weigh. */
.ftr__bottom { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; }
.ftr__bottom .ftr__sig { justify-self: start; }
.socials--bottom { display: inline-flex; gap: 14px; justify-self: center; }
@media (max-width: 560px) {
  .ftr__bottom { grid-template-columns: 1fr; gap: 12px; justify-items: center; }
  .ftr__bottom .ftr__sig { justify-self: center; }
}

/* #2 — subcategory chips: a second scrollable row, never a dropdown */
.subcats {
  display: flex; gap: 8px;
  position: sticky; top: var(--hdrh, 56px); z-index: 20;
  background: var(--page);
  margin: 2px calc(-1 * var(--gutter)) 16px;
  padding: 10px var(--gutter);
  border-bottom: 1px solid var(--rule);
  overflow-x: auto; scrollbar-width: none; -webkit-overflow-scrolling: touch;
}
.subcats::-webkit-scrollbar { display: none; }
.scchip {
  flex: 0 0 auto; min-height: 40px; padding: 0 14px;
  border: 1px solid var(--rule); background: var(--page); cursor: pointer;
  font-family: inherit; font-size: 13px; color: var(--ink); white-space: nowrap;
  transition: border-color .18s var(--ease), background .18s var(--ease), color .18s var(--ease);
}
.scchip .num { font-size: 11.5px; color: var(--muted); }
.scchip:hover { border-color: var(--ink); }
.scchip[aria-current="true"] { background: var(--ink); border-color: var(--ink); color: var(--paper); }
.scchip[aria-current="true"] .num { color: rgba(255, 255, 255, .65); }

/* #5 — footer folded into short rows */
.ftr__accs { border-top: 1px solid var(--rule); }
.ftr__acc { border-bottom: 1px solid var(--rule); }
/* the last row's underline would sit right on the © line's top border and
   read as one thick rule — the © border is the closing line, so the last
   accordion gives its own up */
.ftr__acc:last-child { border-bottom: none; }
.ftr__acc summary {
  cursor: pointer; list-style: none; position: relative;
  padding: 13px 26px 13px 0;
  font-family: Oswald, sans-serif; font-size: 12px; letter-spacing: .14em;
  text-transform: uppercase;
}
.ftr__acc summary::-webkit-details-marker { display: none; }
.ftr__acc summary::after {
  content: "+"; position: absolute; right: 2px; top: 50%; transform: translateY(-50%);
  font-size: 16px; color: var(--muted);
}
.ftr__acc[open] summary::after { content: "−"; }
.ftr__acc > div { padding: 0 0 14px; font-size: 13.5px; color: var(--muted); line-height: 1.6; }
.ftr__acc a { color: inherit; }
.ftr__acc a:hover { text-decoration: underline; text-underline-offset: 3px; }
.ftr__acc .socials { display: inline-flex; gap: 10px; margin-left: 12px; vertical-align: middle; }
.ftr__pay { display: block; margin-top: 8px; }
/* two columns of accordions where there is room; one on a phone */
@media (min-width: 768px) {
  .ftr__accs { display: grid; grid-template-columns: 1fr 1fr; gap: 0 44px; border-top: none; }
  .ftr__acc:nth-child(1), .ftr__acc:nth-child(2) { border-top: 1px solid var(--rule); }
}


/* ---------- v2 mobile: give the screen back ---------- */
@media (max-width: 767px) {
  /* The header search duplicated the bottom nav's Поиск tab, which opens a
     search screen with its own field — so the always-on bar was pure cost:
     a full row of every viewport. Gone; search lives in the bottom nav. */
  .hdr__searchwrap { display: none; }

  /* Scroll down → the whole header (promo, row, categories) slides away and
     the subcat chips take the top edge; scroll up → it returns. will-change
     keeps the header on its own compositor layer so the slide never repaints
     the page under it. */
  .hdr { transition: transform .22s var(--ease), background .25s var(--ease); will-change: transform; }
  html[data-hidenav] .hdr { transform: translateY(-100%); }
  html[data-hidenav] .subcats { top: 0; }

  /* Anything the browser scrolls into view — focus(), an anchor, a script —
     lands below the header's return path and above the bottom nav, never
     under either. The header comes back on any scroll-up and slides over
     the top edge for 220ms; a tap target parked exactly there (WebKit aligns
     scrollIntoView to the nearest edge where Chromium centres) took the
     mousedown and lost the mouseup to the arriving header — «В корзину»
     silently did nothing on --project=mobile-safari. --hdrh is measured by
     measureHdr() in app.js; the fallback is the header's usual height. */
  html { scroll-padding: calc(var(--hdrh, 160px) + 8px) 0 calc(var(--bottomnav-h) + 8px); }
}

/* ---------- fluidity ---------- */
/* no 350ms double-tap ambiguity on anything tappable */
button, a, summary, label { touch-action: manipulation; }
/* a horizontal strip finishing its scroll must not hand the momentum to the
   page (or to iOS's back-swipe) */
.brandstrip__row, .subcats, .hdr__nav { overscroll-behavior-x: contain; }
/* long grids: let the browser skip layout and paint of offscreen cards —
   the single biggest win for scroll on a phone. Chrome/Firefox only; older
   engines simply ignore it, and WebKit is kept out on purpose. Safari's
   content-visibility (18 through 26, seen on Playwright's WebKit 26.5) is not
   safe under a finger: it renders even on-screen cards lazily, so a tap that
   lands right after a scroll sits on a row still being swapped from the 340px
   placeholder to its real ~280px and ends up on the section behind the card;
   and a card whose button just lost focus (WebKit blurs a focused button on
   the next mousedown) is skipped for one frame, so the mouseup of that very
   tap finds nothing — the size listbox would not reopen and a product title
   next to it did not open the product. Both showed up in e2e/catalogue.spec.ts
   and e2e/home.spec.ts on --project=mobile-safari. -webkit-nbsp-mode is a
   WebKit-only property, i.e. the second clause is a Safari gate. */
@supports (content-visibility: auto) and (not (-webkit-nbsp-mode: space)) {
  .card { content-visibility: auto; contain-intrinsic-size: auto 340px; }
}


/* harvested description + legal page typography */
.acc__rich p, .legal p { margin: 0 0 10px; }
.acc__rich ul, .legal ul, .legal ol { margin: 0 0 12px; padding-left: 20px; }
.acc__rich li, .legal li { margin: 4px 0; }
.acc__rich h1, .acc__rich h2, .acc__rich h3, .acc__rich h4 { font-size: 14px; margin: 14px 0 6px; font-family: Oswald, sans-serif; letter-spacing: .08em; text-transform: uppercase; }
.legal h1, .legal h2, .legal h3, .legal h4 { font-size: 15px; margin: 20px 0 8px; font-family: Oswald, sans-serif; letter-spacing: .08em; text-transform: uppercase; }
.legal { font-size: 14.5px; line-height: 1.65; }
.legal table { width: 100%; border-collapse: collapse; margin: 12px 0; font-size: 13.5px; }
.legal td, .legal th { border: 1px solid var(--rule); padding: 6px 9px; text-align: left; }
.legal a { color: var(--ink); }
/* returns: the shop's own paragraph under the policy text on /info/returns/
   (returnsAskHTML). A hairline above it, because it is this shop speaking and
   not the harvested policy — the same way «Полные условия доставки» is set
   apart from the page that carries it. */
.legal--ask { margin-top: 22px; padding-top: 16px; border-top: 1px solid var(--rule); }

/* ---------- «Доставка и оплата» — the customer page (deliveryPageHTML) ----
   The same voice as the rest of the shop: Oswald section titles, Golos body,
   hairlines instead of boxes. The price table is wider than a phone, so it
   scrolls inside its own strip — edge to edge, the gutter pulled in — and
   the page itself never scrolls sideways. Prerendered and live pages share
   this markup, so the rules live here and nowhere else. */
.dlv__h2 { display: block; margin: 30px 0 12px; }
.dlv__scroll {
  overflow-x: auto; -webkit-overflow-scrolling: touch;
  margin: 0 calc(-1 * var(--gutter)); padding: 0 var(--gutter);
  scrollbar-width: thin;
}
.dlv__scroll:focus-visible { outline: 2px solid var(--ink); outline-offset: -2px; }
.dlv__table { width: 100%; min-width: 560px; border-collapse: collapse; font-size: 14.5px; line-height: 1.4; }
.dlv__table th, .dlv__table td {
  padding: 11px 14px 11px 0; border-bottom: 1px solid var(--rule-soft); text-align: left; vertical-align: top;
}
.dlv__table th:last-child, .dlv__table td:last-child { padding-right: 0; }
.dlv__table thead th {
  font-family: Oswald, sans-serif; font-weight: 500; font-size: 11.5px; letter-spacing: .14em;
  text-transform: uppercase; color: var(--muted); border-bottom-color: var(--rule); white-space: nowrap;
}
.dlv__table tbody th { font-weight: 500; }
.dlv__carriers { display: block; font-size: 12.5px; font-weight: 400; color: var(--muted); margin-top: 2px; }
.dlv__note { margin: 10px 0 0; font-size: 13px; color: var(--muted); line-height: 1.55; }
.dlv__pickup { margin-top: 18px; padding: 14px 16px; background: var(--paper); border: 1px solid var(--rule-soft); font-size: 14.5px; }
.dlv__pickup p { margin: 0; }
.dlv__pickup .dlv__addr { margin-top: 4px; }
.dlv__pickup .dlv__hours { margin-top: 6px; font-size: 13.5px; color: var(--muted); }
.dlv__times { margin: 0; }
.dlv__times > div {
  display: grid; grid-template-columns: minmax(120px, 1fr) 2fr; gap: 12px;
  padding: 9px 0; border-bottom: 1px solid var(--rule-soft); font-size: 14.5px;
}
.dlv__times dt { font-weight: 500; }
.dlv__times dd { margin: 0; }
.dlv__pay { list-style: none; margin: 0; padding: 0; }
.dlv__pay li { display: flex; gap: 16px; align-items: center; padding: 12px 0; border-bottom: 1px solid var(--rule-soft); }
.dlv__paylogo { flex: 0 0 76px; display: flex; gap: 6px; align-items: center; flex-wrap: wrap; color: var(--ink); }
.dlv__paylogo svg { height: 22px; width: auto; max-width: 76px; }
.dlv__paylogo svg[aria-label="Apple Pay"] { height: 15px; }
.dlv__paylogo svg[aria-label="Google Pay"] { height: 13px; }
.dlv__paylogo .bank__logo { height: 18px; }
.dlv__paytxt { display: flex; flex-direction: column; gap: 2px; font-size: 14.5px; min-width: 0; }
.dlv__paytxt span { font-size: 13px; color: var(--muted); line-height: 1.45; }
.dlv__contact p { margin: 0 0 10px; font-size: 14.5px; }
.dlv__contact a { color: var(--ink); }
.dlv__legal { margin-top: 32px; border-top: 1px solid var(--rule); border-bottom: 1px solid var(--rule); }
.dlv__legal summary {
  cursor: pointer; list-style: none; position: relative; min-height: 48px; display: flex; align-items: center;
  padding: 12px 26px 12px 0; font-family: Oswald, sans-serif; font-size: 12px; letter-spacing: .14em; text-transform: uppercase;
}
.dlv__legal summary::-webkit-details-marker { display: none; }
.dlv__legal summary::after {
  content: "+"; position: absolute; right: 2px; top: 50%; transform: translateY(-50%);
  font-size: 16px; color: var(--muted);
}
.dlv__legal[open] summary::after { content: "−"; }
.dlv__legal > .legal { padding-bottom: 16px; }
@media (min-width: 768px) {
  .dlv__scroll { margin: 0; padding: 0; }
}
.ftr__acc .link { font-size: inherit; color: inherit; border-bottom-color: var(--rule); }
.ftr__acc .link:hover { border-bottom-color: var(--ink); }

/* ---------- checkout: carrier chips, parcel-machine picker ----------
   The machine list is 400+ rows long in Estonia alone, so it is a searchable
   sheet rather than a <select>: a bottom sheet on a phone (thumb reach, and it
   can be swiped away by tapping the scrim), a centred panel on a desktop.
   Every target here is at least 44px tall. */
/* UX fix 6: the method list (.optlist: Пакомат / Курьер / Самовывоз) used to
   run straight into whatever chooser followed it — the carrier chips, or the
   pakomat field itself when a country has only one live carrier and the chips
   never render — with zero space between the last row and it. */
.optlist + .carriers, .optlist + .field { margin-top: 14px; }
.carriers { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 14px; }
.carrier {
  display: inline-flex; align-items: center; gap: 7px;
  min-height: 44px; padding: 10px 16px;
  border: 1px solid var(--rule); background: transparent;
  font: inherit; font-size: 13.5px; cursor: pointer;
  transition: background .15s var(--ease), border-color .15s var(--ease);
}
.carrier:hover { background: var(--hover); }
/* Carrier colour marks — own brand colours, drawn when there is no real logo:
   without Montonio keys, and for any carrier its /carriers list has no logoUrl
   for. The real mark comes from Montonio itself (GET /api/shipping/carriers/,
   Dim 08.09.2026) and lands as an <img class="carrier__logo"> in the chip,
   which also carries .carrier--logo so this dot steps aside for it. */
.carrier::before {
  content: ""; flex-shrink: 0; width: 10px; height: 10px; border-radius: 50%;
  background: var(--muted);
}
/* height as briefed, width left to the image's own ratio — same rule as
   .bank__logo. A 404 or a timeout removes the image (the "error" listener near
   the bottom of app.js) and the carrier's name, which is beside it rather than
   inside it, stays exactly where it was. */
.carrier__logo { height: 20px; width: auto; display: block; }
.carrier--logo::before { display: none; }
.carrier[data-carrier="omniva"]::before { background: #f47a20; }
.carrier[data-carrier="dpd"]::before { background: #dc0032; }
.carrier[data-carrier="smartpost"]::before { background: linear-gradient(135deg, #ffcf00 50%, #111 50%); }
.carrier[data-carrier="unisend"]::before { background: #7dd957; }
/* Nova Post: its red over its dark, split down the middle. A flat #da291c
   would be the third warm circle in an Estonian row that already has DPD's
   #dc0032 and Omniva's #f47a20 — three reds at 10 px is no cue at all — and
   the split reads apart from SmartPosti's diagonal too. The colour is never
   the only cue in any case: the carrier's name is beside every circle.
   Venipak's #1e4b9d lived here until 14.09.2026; the shop does not offer it,
   so a chip for it is never drawn. */
.carrier[data-carrier="novapost"]::before { background: linear-gradient(90deg, #da291c 50%, #1a1a1a 50%); }
/* The same fill had already destroyed the same kind of mark on the bank tiles
   at «Оплата», the step after this one — see the note above
   .bank[aria-current="true"], which this rule is a verbatim copy of; the two
   are one decision, and this is the half that was missed. Here it was Unisend
   (Renat, 14.09.2026): its logo is black artwork on nothing, so the picked
   chip went completely black — and the first carrier in the list is picked
   before the shopper has touched anything, so the step opened with a black
   hole already in it. The ink ring over the chip's own light
   ground marks the pick instead, and every mark Montonio sends, black or
   coloured, stays readable on it. The colour dots above are the no-logo half
   of the same list and are untouched. */
.carrier[aria-current="true"] {
  border-color: var(--ink); background: var(--hover); color: var(--ink);
  box-shadow: inset 0 0 0 1px var(--ink);
}
/* ---- carrier cards (22.09.2026) — the step Montonio's calculator has ----
   Under the chosen delivery type, one full-width row per carrier: its mark,
   its name, Nova Post's «без возврата», and its own price on the right. The
   pick is the same ink ring as the chips above and the floor the same 44px —
   52 here, because a price and a tag now share the row with the name. The
   tag is quiet on purpose: no returns is a fact to read, not an alarm. */
.carriers__h { margin: 16px 0 8px; font-size: 13px; color: var(--muted); }
.carriers--cards { display: grid; grid-template-columns: minmax(0, 1fr); gap: 8px; }
.carrier--card { display: flex; width: 100%; min-height: 52px; padding: 10px 14px; text-align: left; }
.carrier__name { font-size: 14px; }
.carrier__tag {
  font-size: 12px; color: var(--muted); white-space: nowrap;
  border: 1px solid var(--rule); padding: 1px 6px;
}
.carrier__price { margin-left: auto; padding-left: 12px; font-weight: 600; font-size: 14px; white-space: nowrap; }
/* The price is the one thing on the card that must never leave it
   (23.09.2026). Measured at 280 px: SmartPosti's mark is 91 px wide at 20 px
   high, and with «SmartPosti» beside it the price ran 3 px past the card's
   edge; Nova Post with «без возврата» ran 10 px past — at larger system text
   sizes the same happens on an ordinary 360 px phone. So the price keeps its
   width, the mark gives way first (scaled, never cropped — object-fit), and
   the Nova Post tag may take two lines. Nothing changes where there is room. */
.carrier--card .carrier__logo { flex: 0 1 auto; min-width: 28px; object-fit: contain; object-position: left center; }
.carrier--card .carrier__tag { min-width: 0; white-space: normal; }
.carrier--card .carrier__price { flex: none; }
.carriers__note { margin: -6px 0 14px; font-size: 13px; line-height: 1.5; color: var(--muted); }

.pointbtn {
  display: grid; grid-template-columns: 1fr auto; align-items: center;
  gap: 2px 12px; width: 100%; min-height: 56px; padding: 10px 12px;
  border: 1px solid rgba(28, 26, 0, .35); background: transparent;
  font: inherit; text-align: left; cursor: pointer;
}
.pointbtn:hover { background: var(--hover); }
.pointbtn--set { border-color: var(--ink); }
.pointbtn[aria-invalid="true"] { border-color: var(--error); }
.pointbtn__nm { grid-column: 1; font-size: 14.5px; }
.pointbtn__ad { grid-column: 1; font-size: 12.5px; color: var(--muted); }
.pointbtn__go { grid-column: 2; grid-row: 1 / span 2; font-size: 12px; color: var(--muted); text-decoration: underline; }

.psheet {
  position: fixed; z-index: var(--z-drawer); background: var(--page);
  display: flex; flex-direction: column;
  left: 0; right: 0; bottom: 0; max-height: 82vh;
  border-top: 1px solid var(--ink);
  animation: sheetUp .28s var(--ease);
}
@keyframes sheetUp { from { transform: translateY(100%); } }
@media (min-width: 768px) {
  .psheet {
    left: 50%; right: auto; top: 50%; bottom: auto;
    transform: translate(-50%, -50%);
    width: min(520px, 92vw); max-height: 76vh;
    border: 1px solid var(--ink);
    /* UX fix 7: the shared `fade` keyframes only ever touch opacity, so the
       static translate(-50%,-50%) above should already hold it centred for
       every frame — but the plain fade still read as a left-to-centre jump
       on open. psheetIn keyframes the SAME translate at both ends explicitly
       (only scale differs), so the panel cannot move horizontally by
       construction, whatever produced the jump. */
    animation: psheetIn .2s ease;
  }
}
@keyframes psheetIn { from { opacity: 0; transform: translate(-50%, -50%) scale(.96); } }
.psheet__head { display: flex; align-items: center; justify-content: space-between; padding: 14px 18px; border-bottom: 1px solid var(--rule); }
.psheet__search { display: flex; align-items: center; gap: 8px; padding: 12px 18px; border-bottom: 1px solid var(--rule); }
.psheet__search .input { flex: 1; margin: 0; }
/* UX fix 8: the Карта/Kaart/Map toggle next to the search box */
.psheet__maptoggle {
  flex-shrink: 0; min-height: 44px; padding: 0 14px;
  border: 1px solid var(--rule); background: transparent;
  font: inherit; font-size: 13px; cursor: pointer;
  transition: background .15s var(--ease), border-color .15s var(--ease);
}
.psheet__maptoggle:hover { background: var(--hover); }
.psheet__maptoggle[aria-pressed="true"] { border-color: var(--ink); background: var(--ink); color: var(--page); }
.psheet__list { flex: 1; overflow: auto; -webkit-overflow-scrolling: touch; padding-bottom: env(safe-area-inset-bottom); }
.psheet__msg { padding: 22px 18px; margin: 0; font-size: 13.5px; }
/* Leaflet needs its container to carry a real computed height — flex:1 inside
   .psheet's column gives it exactly the room .psheet__list would have had. */
.psheet__map { flex: 1; min-height: 0; }
.psheet__maphint { padding: 8px 18px; margin: 0; font-size: 12px; color: var(--muted); border-top: 1px solid var(--rule); }
/* a phone wants the whole screen to pan and zoom a map; the fixed-height
   bottom sheet that suits a short list of rows is cramped for it */
@media (max-width: 767px) { .psheet--map { max-height: 100vh; height: 100vh; } }
/* .psheet itself only ever carries max-height — the LIST view's few hundred
   rows push its auto/content height up against that cap on their own, which
   is what lets .psheet__list's flex:1 fill the rest. #pointmap starts empty
   (Leaflet paints into it, but contributes no intrinsic height of its own),
   so without an explicit height here the sheet never grows past its header
   and search row and the map has no box to fill — an explicit height, not
   just a cap, is what flex:1 needs to have anything to grow into. */
@media (min-width: 768px) { .psheet--map { height: 76vh; } }

/* «Удалить аккаунт» (acctDelHTML, app.js): the parcel sheet's frame around
   the drawers' own body and foot — a few sentences, one field, a red button
   and «Отмена». No list, so the sheet is as tall as its words; the body
   scrolls when a phone's keyboard leaves it less room than that. The motion
   is the sheet's (sheetUp / psheetIn), and reduced motion stills it with the
   rest of the shop (the block at the end of this file). */
.psheet--confirm .drawer__body p { margin: 0 0 10px; font-size: 14px; }
.psheet--confirm .drawer__body .field { margin: 16px 0 0; }
.psheet--confirm .acctdel__orders { font-size: 15px; }
/* the refusal line is there from the start (role="alert" is announced when
   its text arrives) and takes no room while it is empty */
.acctdel__err:empty { margin: 0; }
/* The one red button in the shop: an act that cannot be undone. --error is
   functional-only (docs/design/REMPIRE_DESIGN_SYSTEM.md), and this is its
   function; disabled, it fades like every other .btn until the e-mail matches. */
.btn--danger { background: var(--error); border-color: var(--error); color: var(--paper); }

/* «Данные и аккаунт» — the last block of «Кабинет», quiet: a hint, the
   export as the shop's ghost button, and the deletion as a plain .link like
   «Выйти» — secondary, but saying exactly what it does. */
.acct__privtxt { margin: 0 0 12px; }
.acct__privdel { margin-top: 24px; margin-bottom: 8px; }
/* 44 px to tap without 44 px of underline: the hit area grows past the text */
.acct__delbtn { position: relative; }
.acct__delbtn::after { content: ""; position: absolute; inset: -14px -10px; }
.prow {
  display: block; width: 100%; min-height: 56px;
  padding: 10px 18px; border: none; border-bottom: 1px solid var(--rule-soft);
  background: transparent; font: inherit; text-align: left; cursor: pointer;
}
.prow:hover { background: var(--hover); }
.prow[aria-current="true"] { background: var(--shell); }
.prow__nm { display: block; font-size: 14.5px; }
.prow__ad { display: block; font-size: 12.5px; color: var(--muted); }

/* receipt states — paid, failed, still waiting on the bank */
.done__num { margin: 0 0 10px; font-size: 14px; }
.done__tick--bad, .done__tick--wait {
  display: grid; place-items: center;
  font-size: 30px; line-height: 1; font-family: Oswald, sans-serif;
}
.done__tick--bad { color: var(--error); }
.done__tick--wait { color: var(--muted); }
/* features: «Скачать подарочную карту (PDF)» under a paid receipt — one link
   per card the order bought, stacked so several never crowd the row. */
.done__gifts { display: flex; flex-direction: column; gap: 8px; margin: 0 0 18px; }
/* the failed receipt's two buttons: «Оплатить ещё раз» over «Вернуться в магазин» */
.done__acts { display: flex; flex-direction: column; gap: 8px; }
.done__gift { display: inline-flex; justify-content: center; gap: 8px; align-items: baseline; }
.done__gift .num { font-size: 13px; opacity: .7; }

/* ---------- «Оплата не прошла» / «Заказ не оплачен» ----------
   The one receipt that is not a receipt: the order still owes money, and the
   screen carries the checkout's payment picker so it can be paid from here.
   A radio list, bank chips and a form's two buttons lay themselves out from
   the left whatever the container says, so the three lines above them used to
   float in the middle over a block that did not — «text alignment is wrong»
   (Ренат, 17.09.2026). One left edge for the whole column instead, which is
   the edge the checkout, the basket and every other screen already start
   from. The receipts that really are receipts keep their centred setting:
   they have nothing to fill in. */
.done--unpaid { text-align: left; }
.done--unpaid .done__tick { margin-left: 0; }
.done__lead { margin: 0 0 20px; }
/* The chips used to sit directly on the top edge of «Оплатить ещё раз» —
   `.banks` has a top margin and nothing had a bottom one, so the last row of
   bank tiles and the button shared a border. The picker owns its own space
   now, at every width and with any number of banks. */
.done__pay { margin-bottom: 22px; }
/* Montonio's list is still in flight at the first paint, so the row is not
   drawn at all until it lands (doneBanksHTML in app.js). This is the box it
   holds open in the meantime — one chip tall, so the button below it does not
   move when the real tiles arrive. */
.banks--wait { min-height: 44px; align-items: center; }
.banks--wait .muted { font-size: 13px; }

/* ==========================================================================
   FEATURES — наборы, подарочная карта, отзывы, видео
   Appended by the features agent. Everything below belongs to those four and
   is namespaced (.bstack/.bitem, .gift*, .rev*, .pvideo) so it cannot reach
   the rest of the shop.
   ========================================================================== */

/* ---------- наборы: the photo stack ---------- */
/* No new artwork for a set: three product photos, overlapped, on the same
   square the product cards use. The stage is the card's own .card__media (or
   .pdp__stage), which is already position:relative. */
.bstack { position: absolute; inset: 0; display: block; }
.bstack__i {
  position: absolute; background-size: contain; background-position: center;
  background-repeat: no-repeat; transition: transform .5s var(--ease);
}
.bstack__i--0 { inset: 8% 34% 20% 4%; }
.bstack__i--1 { inset: 4% 16% 12% 22%; }
.bstack__i--2 { inset: 10% 2% 8% 42%; }
@media (hover: hover) {
  .card--bundle:hover .bstack__i--0 { transform: translateX(-4%) scale(1.03); }
  .card--bundle:hover .bstack__i--2 { transform: translateX(4%) scale(1.03); }
}
.bstack--big .bstack__i--0 { inset: 10% 36% 20% 2%; }
.bstack--big .bstack__i--1 { inset: 5% 18% 12% 20%; }
.bstack--big .bstack__i--2 { inset: 12% 0 8% 42%; }
.bstack--line .bstack__i--0 { inset: 6% 40% 14% 0; }
.bstack--line .bstack__i--1 { inset: 2% 20% 8% 20%; }
.bstack--line .bstack__i--2 { inset: 8% 0 4% 40%; }
/* two-item set: last, so it wins over the size modifiers above */
.bstack--n2 .bstack__i--0 { inset: 7% 32% 16% 5%; }
.bstack--n2 .bstack__i--1 { inset: 10% 5% 8% 32%; }

.bbadge {
  position: absolute; left: 0; top: 0; z-index: 1;
  background: var(--ink); color: var(--page);
  font-family: Oswald, sans-serif; font-size: 10.5px; letter-spacing: .1em;
  padding: 3px 7px;
}
.bwas { color: var(--muted); font-weight: 400; text-decoration: line-through; margin-left: 5px; }
/* UX fix 2: on the catalogue page the set grid is nested INSIDE that page's
   own <section class="sec">, right under the toolbar, so it keeps no extra
   top gap of its own there — hence the descendant selector, not a bare
   .sec--bundles. On the home page the same bundleGridHTML() output is a
   top-level sibling of the two product rails (each a plain .sec), so it falls
   through to the ordinary .sec padding-top and now keeps the same rhythm
   between "Новинки" and "Наборы" as between the two rails above it. */
.sec .sec--bundles { padding-top: 0; }

.bdesc { font-size: 14.5px; line-height: 1.6; margin: 14px 0 4px; }
/* No rule of its own: «Что внутри» above it is a .sec__head, which already
   draws a full-width line under the heading. The two together read as a thick
   black rule with a grey one stuck to it (Renat, 13.09.2026). */
.bitems { margin-bottom: 20px; }
/* …and no rule under the rows either, for the same reason one turn later:
   «Что внутри» already draws a line, and the grey one under every part
   repeated it three more times — the last of them separating the list from
   the «В корзину» button and so separating nothing (Renat, 18.09.2026). The
   photo and the 20px between rows are what keep the parts apart. Removed
   here rather than from `--rule-soft`, which half the shop uses: `.bitem`
   is built in one place only (screenBundle), so this is the set page and
   nothing else. */
.bitem {
  display: flex; align-items: center; gap: 12px;
  padding: 10px 0;
}
.bitem__ph { position: relative; width: 52px; height: 52px; flex-shrink: 0; }
.bitem__nm {
  flex: 1; min-width: 0; text-align: left; font: inherit; font-size: 13.5px;
  background: none; border: none; padding: 8px 0; cursor: pointer; color: inherit;
  border-bottom: 1px solid transparent;
}
.bitem__nm:hover { border-bottom-color: var(--rule); }
/* «нет в наличии» on the one part that is missing. flex-shrink:0 like the
   price beside it: the row is already tight on a 375px phone, and a chip
   allowed to shrink wraps its own two words. */
.bitem__out { flex-shrink: 0; }
.bitem__pr { font-size: 13.5px; font-weight: 600; flex-shrink: 0; }

/* ---------- подарочная карта ---------- */
.gifttile {
  display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
  border: 1px solid var(--ink); padding: 16px; margin-bottom: 28px;
}
.gifttile__art {
  width: 54px; height: 54px; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  background: var(--shell);
}
.gifttile__mark { width: 22px; height: 34px; color: var(--ink); }
.gifttile__txt { flex: 1 1 220px; min-width: 0; }
.gifttile__txt .muted { margin: 4px 0 0; }
/* The home page's gift-card block: the tile already draws its own frame and
   carries the gap to what follows, so the section only needs the rhythm above
   it — .sec's own bottom padding would double that gap. */
.sec--gift { padding-bottom: 0; }
/* Same tile in the cabinet, between «Мои заказы» and «Мои данные» — it needs
   the gap a .sec__head--sub heading would have given it. */
.acct__gift { margin-top: 32px; }

.gamts { display: flex; gap: 10px; flex-wrap: wrap; margin: 4px 0 20px; }
.gamt {
  min-height: 48px; min-width: 84px; padding: 0 18px;
  border: 1px solid var(--rule); background: var(--page); color: inherit;
  font: inherit; font-size: 15px; cursor: pointer;
  transition: border-color .18s var(--ease), background .18s var(--ease);
}
.gamt:hover { border-color: var(--ink); }
.gamt[aria-current="true"] { border-color: var(--ink); background: var(--ink); color: var(--page); }

/* the gift-card line in the cart has no photo — the mark stands in for one */
.giftph {
  position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  background: var(--shell);
}
.giftph__mark { width: 40%; height: 55%; color: var(--ink); }

/* what a set or a gift card adds under its name in the cart / the summary */
.cline__parts {
  display: block; font-size: 11.5px; color: var(--muted);
  line-height: 1.45; margin: -3px 0 7px;
}
.cosum__nm .cline__parts { margin: 3px 0 0; }
.cosum__row--note { margin-top: -6px; }

/* ---------- отзывы ---------- */
.revs--db { margin-bottom: 4px; }
.rev--db .rev__head { flex-wrap: wrap; }
.revbadge {
  font-family: Oswald, sans-serif; font-size: 9.5px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--ok); border: 1px solid currentColor;
  padding: 1px 6px; white-space: nowrap;
}
.revadd { border-top: 1px solid var(--rule); padding-top: 16px; margin-top: 4px; }
.revadd--done p { margin: 0 0 8px; font-size: 14px; }
.revadd--open .field:first-of-type { margin-top: 10px; }
/* Whose review this will be. One muted line under «Публикуем после
   проверки»; the address inside it is one unbreakable token, so it wraps
   instead of widening the page on a phone. */
.revadd__who { font-size: 12.5px; margin: 4px 0 0; overflow-wrap: anywhere; }
.revstars { display: flex; align-items: center; gap: 2px; margin: 2px 0 16px; }
.revstar {
  /* ≥44px targets: a five-star row is the single easiest thing to mis-tap */
  min-width: 44px; min-height: 44px; padding: 0;
  background: none; border: none; cursor: pointer;
  font-size: 26px; line-height: 1; color: var(--rule);
  transition: color .15s var(--ease), transform .15s var(--ease);
}
.revstar:hover { transform: scale(1.08); }
.revstar[aria-checked="true"] { color: var(--ink); }
.revstars .muted { margin-left: 8px; font-size: 12.5px; }
/* honeypot: present for a bot, gone for a human and for a screen reader */
.revhp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

/* ---------- видео ---------- */
.pvideo { margin: 8px 0 32px; }
.pvideo__frame {
  position: relative; display: block; width: 100%; aspect-ratio: 16 / 9;
  background: var(--shell); border: none; padding: 0; overflow: hidden;
}
.pvideo__frame iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
.pvideo__play { cursor: pointer; }
.pvideo__play img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.pvideo__blank { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; }
.pvideo__mark { width: 32px; height: 50px; color: var(--muted); }
.pvideo__btn {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  width: 64px; height: 64px; border-radius: 50%;
  background: rgba(28, 26, 0, .82); color: #fff;
  display: flex; align-items: center; justify-content: center;
  font-size: 22px; padding-left: 4px;
  transition: transform .2s var(--ease), background .2s var(--ease);
}
.pvideo__play:hover .pvideo__btn { transform: translate(-50%, -50%) scale(1.07); background: var(--ink); }
/* media: an Instagram reel is a tall card, not a 16:9 player — the ratio here
   is the embed page's own, so it neither letterboxes nor clips. */
.pvideo--ig .pvideo__frame { aspect-ratio: 400 / 640; max-width: 420px; }
.pvideo__alt { margin: 8px 0 0; font-size: 13px; }
/* media: an uploaded file has whatever shape the phone recorded, so it keeps
   its own — no fixed box to letterbox a portrait clip into. */
.pvideo__file { display: block; width: 100%; max-height: 70vh; background: var(--shell); }
@media (min-width: 900px) { .pvideo { max-width: 620px; } }

/* The set page parks the same sticky buy bar as a product page. The chat
   bubble's own rule (further up, chat.js's block) only knows about
   data-screen product/checkout, so the set screen needs the same lift —
   added here rather than by editing that block. */
@media (max-width: 767px) {
  body[data-screen="bundle"] .sbot__fab { bottom: calc(140px + env(safe-area-inset-bottom, 0px)); }
  body[data-screen="bundle"] .sbot__panel { bottom: calc(200px + env(safe-area-inset-bottom, 0px)); }
}

/* «Отзывов пока нет» sits where the review list would be, so it needs the
   same air above the form as a list of reviews would have had. */
.revs__none { margin: 0 0 14px; font-size: 13.5px; }

/* ---------- blog: the listing and one article ----------------------------
   Cards are text-heavy (a title and an excerpt, not just a price), so the
   grid gets wider tracks than the product grid's — narrower ones read fine
   for a bottle and a price, not for two sentences. */
.blog__grid { grid-template-columns: repeat(auto-fill, minmax(min(100%, 240px), 1fr)); }
@media (min-width: 1100px) { .sec .blog__grid { grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); } }
/* A tile is a link and has to read as one: the pointer, and on hover the
   title underlines and the cover lifts a touch — quiet, like the product
   cards' own hover, not a box appearing around it. */
.blog__tile { text-decoration: none; color: inherit; cursor: pointer; }
/* The line between articles. It used to come from .card::after, which every
   blog tile inherited by also carrying the class; that line went when the
   product cards lost theirs (owner, 09.09.2026), and an article has no
   «В корзину» underneath it to close it off in the way a product now does.
   So it is the tile's own rule. The last one has nothing below it to
   separate from — on a phone the grid is a single column, where :last-child
   is exactly the bottom row, and that is the screen this is read on. */
.blog__tile { position: relative; padding-bottom: 6px; }
.blog__tile::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0;
  border-top: 1px solid rgba(28, 26, 0, .13); pointer-events: none;
}
.blog__grid > li:last-child .blog__tile::after { display: none; }
.blog__tile .blog__tileimg { transition: transform .4s var(--ease), translate .4s var(--ease); }
@media (hover: hover) {
  .blog__tile:hover .blog__tiletitle, .blog__tile:hover .pre__nm { text-decoration: underline; text-underline-offset: 3px; }
  .blog__tile:hover .blog__tileimg { transform: translateY(-2px); }
  /* A cover the owner zoomed carries its zoom as an inline `transform:
     scale(…)` (blogCoverBgStyle in app.js), which outranks the rule above —
     so its lift goes through `translate`, which stacks with a transform
     instead of replacing it. */
  .blog__tile:hover .blog__tileimg[style*="scale("] { translate: 0 -2px; }
}
/* A zoomed cover is its frame scaled up around the owner's point and cut
   back to its own box by a clip-path. The clip settles what is painted, not
   what is laid out: the scaled box still counts as overflow, and at 3× on a
   phone that is two screens' width of it — a page that slides sideways under
   the thumb wherever body's own overflow-x does not hold. So the tile, or the
   article, that holds a zoomed cover clips sideways itself. `clip`, not
   `hidden`: no scroll container, and the tile's lift (vertical) is untouched.
   Only where a cover is zoomed — every other tile and article is as it was. */
.blog__tile:has(> [style*="scale("]), .blog__post:has(> .blog__cover[style*="scale("]) { overflow-x: clip; }
.blog__tile:focus-visible .blog__tiletitle { text-decoration: underline; text-underline-offset: 3px; }
/* contain, not cover: most covers are catalogue photos (square, on white) and
   cover-cropping a 1:1 photo into a 1200/630 box zoomed into the label. A real
   1200×630 cover fills the box either way. */
.blog__tileimg {
  display: block; aspect-ratio: 1200 / 630; background: var(--page) center / contain no-repeat;
  margin-bottom: 9px;
}
.blog__tileimg--none { display: flex; align-items: center; justify-content: center; }
.blog__mark { width: 34px; height: auto; opacity: .3; }
.blog__tilebody { display: flex; flex-direction: column; gap: 4px; flex: 1; }
.blog__date { font-size: 12px; }
.blog__tiletitle { font-family: Oswald, sans-serif; font-size: 15px; font-weight: 500; line-height: 1.3; }
.blog__tilebody p { margin: 0; font-size: 13.5px; color: var(--muted); line-height: 1.5; overflow-wrap: anywhere; }
.blog__tags { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 2px; }
.blog__tags .chip { font-size: 10px; }
.blog__more { text-align: center; margin: 26px 0 6px; }

.blog__post { padding-top: 14px; }
.blog__cover {
  display: block; width: 100%; aspect-ratio: 1200 / 630; background: var(--page) center / contain no-repeat;
  margin-bottom: 18px;
}
.blog__meta { display: flex; align-items: center; gap: 16px; margin: 6px 0 14px; font-size: 12.5px; color: var(--muted); }
.blog__meta .link { display: inline-flex; align-items: center; gap: 6px; }
.blog__meta svg { width: 16px; height: 16px; }
.blog__post > .blog__tags { margin-bottom: 10px; }
/* The article page: breadcrumbs in the ordinary .wrap, where every other
   page keeps them; the article itself a reading column in the middle; the
   products it names and the other articles below it at the full width, on
   the catalogue's own grid — a card there is as wide as on a category page,
   so its foot row (price · size · «В корзину») never runs out of room. */
.blog__read { max-width: 760px; margin-inline: auto; }
.blog__h2 { font-size: 13px; letter-spacing: .18em; }
/* The article body: .acc__rich already carries the shop's rich-text rules
   (headings, lists, links — the same ones a product description uses);
   this only caps the measure and gives images and quotes their own room. */
.blog__body { max-width: 68ch; }
.blog__body img { display: block; max-width: 100%; height: auto; margin: 18px 0; }
/* ---- a picture inside the text: how wide it stands, and on which side ----
   Four presets, written by the panel onto the picture's own <figure> as
   `data-fig` (BLOG_FIG in app.js; FIG_VALUES in src/lib/blog.ts is the
   server's copy of the same four words).

   A <figure> with no `data-fig` is matched by nothing below, deliberately:
   that is every picture in every article written before the presets existed,
   and it keeps exactly the rendering it has today — the browser's own figure
   margins, and `.blog__body img` above. Nothing here can move an old article,
   because nothing here can select one.

   Half width is a float, and has to be: only a float makes the paragraphs
   that FOLLOW the picture run down beside it, which is what «текст рядом»
   means — a flex or a grid row could put one paragraph there and no more.
   The two costs of a float are paid on the spot: headings, quotes and
   product cards clear it, so a new section never opens halfway down a
   picture, and the body closes its own floats, so a tall picture at the end
   cannot push into the products shelf below.

   On a phone there is no «beside». 48 % of a 343 px column is 165 px of
   picture and about twenty characters of text per line, which is not
   reading. Both halves go full width under the shop's own phone breakpoint;
   «маленькая» stays small, being the one preset that still means something
   at 375 px. */
.blog__body figure[data-fig] { margin: 18px 0; }
.blog__body figure[data-fig] img { width: 100%; margin: 0; }
.blog__body figure[data-fig="half-left"],
.blog__body figure[data-fig="half-right"] { width: 48%; }
.blog__body figure[data-fig="half-left"] { float: left; margin: 6px 22px 10px 0; }
.blog__body figure[data-fig="half-right"] { float: right; margin: 6px 0 10px 22px; }
.blog__body figure[data-fig="small"] { width: 50%; max-width: 260px; }
.blog__body h2, .blog__body h3, .blog__body blockquote, .blog__body .blog__prod { clear: both; }
.blog__body::after { content: ""; display: block; clear: both; }
@media (max-width: 767px) {
  .blog__body figure[data-fig="half-left"],
  .blog__body figure[data-fig="half-right"] { float: none; width: 100%; margin: 18px 0; }
  .blog__body figure[data-fig="small"] { width: 62%; max-width: 190px; }
}
.blog__body blockquote {
  margin: 18px 0; padding: 2px 0 2px 16px; border-left: 3px solid var(--rule); color: var(--muted);
}
/* A product named inside the text: the «Товар» button leaves an <a
   data-product> marker, and blogBodyHTML() in app.js swaps it for this. It
   is a <button>, so it is valid where the marker stood — inside a sentence's
   own <p> — which a full product card (a <div> of buttons) would not be. */
.blog__prod {
  display: flex; align-items: center; gap: 12px; width: fit-content; max-width: 100%;
  margin: 16px 0; padding: 8px 16px 8px 8px; text-align: left;
  border: 1px solid var(--rule); background: var(--page); color: inherit; cursor: pointer;
}
.blog__prod:hover { border-color: var(--ink); }
.blog__prod:hover .blog__prodname { text-decoration: underline; text-underline-offset: 3px; }
/* .ph is absolute inside a sized frame everywhere else in the shop; here the
   photo IS the frame, so it is put back in flow at a fixed size. */
.blog__prodimg { position: relative; inset: auto; width: 46px; height: 46px; flex: none; background-size: contain; }
.blog__prodtx { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.blog__prodname { font-size: 13.5px; line-height: 1.35; overflow-wrap: anywhere; }
.blog__prodprice { font-size: 13px; color: var(--muted); }
@media (max-width: 767px) {
  .blog__tiletitle { font-size: 14px; }
  .blog__prod { width: 100%; }
}
/* While the API really is still answering (a cold miss — nothing embedded
   in the page, nothing in the tab yet): the shapes of what is coming, grey
   blocks where the cover, the date, the title and the text will be, instead
   of a «…». The blocks breathe slowly; not at all for reduced motion. */
.blog__sk, .blog__skimg, .blog__skline { pointer-events: none; }
.blog__skimg { display: block; aspect-ratio: 1200 / 630; background: rgba(28, 26, 0, .06); margin-bottom: 9px; }
.blog__skimg--cover { width: 100%; margin-bottom: 18px; }   /* where the article's cover will be */
.blog__skline { display: block; height: 11px; margin: 7px 0; background: rgba(28, 26, 0, .08); }
.blog__skline--t { height: 15px; margin: 4px 0 6px; }
.blog__skline--h { height: 26px; width: 70%; margin: 0 0 22px; }
.blog__body.blog__sk { min-height: 240px; }
.blog__skimg, .blog__skline { animation: blogsk 1.3s ease-in-out infinite; }
@keyframes blogsk { 50% { opacity: .45; } }
@media (prefers-reduced-motion: reduce) { .blog__skimg, .blog__skline { animation: none; } }

/* ---------- админка: «Блог» ------------------------------------------------
   The editor itself is `adm-` markup since the phase-3 redesign
   (public/shop2/admin.css: .adm-title-in, .adm-drop, .adm-tools, .adm-canvas).
   What is left here is the one piece that is neither: the sheet the «Ссылка»,
   «Картинка» and «Товар» buttons open, which is drawn by blogToolSheet() out
   of the same `.input`/`.btn` controls the rest of the shop uses. */
.admblog__tool:empty { display: none; }
.admblog__tool {
  border: 1px solid var(--rule); padding: 10px 12px; margin-bottom: 8px; background: var(--hover);
}
.admblog__toolrow { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.admblog__toolrow .input { flex: 1 1 200px; min-width: 0; }

/* ==========================================================================
   07.09.2026 — the three pieces of chrome Dim asked for: the «page not
   found» screen, the checkout's skip link and the consent banner. Nothing
   here reaches an existing selector; each block is namespaced (.nf, .skip,
   .cbanner) so it can be read, moved or removed on its own.
   ========================================================================== */

/* ---------- 404 ----------
   Centred like the receipt, because it is the same shape of page: one fact,
   one sentence, the ways out. The number is drawn as the receipt's tick is —
   big, quiet, decorative — and is aria-hidden, since the <h1> already says
   what happened in words. */
.nf { text-align: center; }
.nf__code {
  margin: 0 0 6px;
  font-family: Oswald, sans-serif; font-size: 46px; line-height: 1;
  letter-spacing: .06em; color: var(--muted);
}
.nf .sec__intro { margin-inline: auto; }
.nf__acts { display: flex; flex-direction: column; gap: 8px; align-items: stretch; }
@media (min-width: 561px) {
  .nf__acts { flex-direction: row; justify-content: center; }
  .nf__acts .btn { min-width: 200px; }
}

/* ---------- the checkout's skip link ----------
   Off the top of the screen until it takes focus, then the first thing a
   keyboard shopper sees — over the sticky header, which is why it is fixed
   and above --z-bar. Not .vh: a 1×1 clipped box cannot be seen once focused,
   and being seen is the whole job. The transition is dropped for anyone who
   asked for less motion; the link still arrives, it simply does not slide. */
.skip {
  position: fixed; left: 10px; top: 10px; z-index: calc(var(--z-toast) + 1);
  transform: translateY(calc(-100% - 14px));
  background: var(--ink); color: var(--page);
  font-family: Oswald, sans-serif; font-size: 11.5px; letter-spacing: .14em; text-transform: uppercase;
  padding: 12px 18px; border: 0; white-space: nowrap;
  transition: transform .18s var(--ease);
}
.skip:focus { transform: none; }
.skip:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) { .skip { transition: none; } }

/* ---------- the consent banner ----------
   A bar, not a modal: nothing about it is urgent enough to take the shop
   away from someone who came to buy something, and a scrim over a first
   visit is the pattern everybody has learnt to resent. It rides low on the
   page in the shop's own ink-on-paper, sized to its own text.

   On a phone it sits ABOVE the bottom nav — the nav is fixed at bottom:0 and
   is how a shopper moves around, so covering it would be worse than the
   banner itself; the same sum the toast uses. It is never drawn on the
   checkout or the receipt at all (consentShown() in app.js), so it cannot
   come between anybody and «Оплатить». */
.cbanner {
  /* Above the chat bubble (z 60) and its panel (61), which are optional
     chrome next to a one-time question — but hidden outright while a drawer
     owns the screen, exactly as .sbot is, so it can never float over an open
     cart. */
  position: fixed; left: 0; right: 0; z-index: 62;
  bottom: calc(var(--bottomnav-h) + env(safe-area-inset-bottom));
  display: flex; justify-content: center;
  padding: 0 var(--gutter) 10px;
  pointer-events: none;   /* the page stays clickable either side of the box */
  animation: cbUp .3s var(--ease);
}
.cbanner__box {
  pointer-events: auto;
  width: min(720px, 100%);
  background: var(--ink); color: var(--paper);
  padding: 14px 16px 12px;
  box-shadow: 0 8px 30px rgba(28, 26, 0, .28);
}
.cbanner__t {
  margin: 0 0 6px;
  font-family: Oswald, sans-serif; font-size: 11.5px; letter-spacing: .18em; text-transform: uppercase;
}
.cbanner__x { margin: 0 0 6px; font-size: 12.5px; line-height: 1.5; opacity: .82; }
.cbanner__acts { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 10px; }
/* the buttons are the shop's own, inverted onto the ink ground */
.cbanner .btn { background: var(--paper); color: var(--ink); border-color: var(--paper); }
.cbanner .btn--ghost { background: none; color: var(--paper); }
.cbanner .link { color: var(--paper); opacity: .8; margin-left: auto; }
.cbanner .link:hover { opacity: 1; }
@keyframes cbUp { from { opacity: 0; transform: translateY(14px); } }
@media (prefers-reduced-motion: reduce) { .cbanner { animation: none; } }
@media (min-width: 768px) { .cbanner { bottom: 0; padding-bottom: 16px; } }
body.is-locked .cbanner { display: none; }
@media (max-width: 560px) {
  /* the box must not eat the screen it is standing on: tighter everywhere,
     and the policy link drops to its own line under the two buttons */
  .cbanner__box { padding: 11px 13px 10px; }
  .cbanner__t { margin-bottom: 4px; }
  .cbanner__x { font-size: 12px; line-height: 1.45; margin-bottom: 4px; }
  .cbanner__acts { margin-top: 8px; gap: 6px; }
  .cbanner .link { margin-left: 0; }
}

