.basket-overlay {
  position: fixed;
  inset: 0;
  z-index: 100;
  background: transparent;
  overflow: hidden;
  pointer-events: none;
}

.basket-overlay.is-open {
  pointer-events: auto;
}

.basket-overlay::before {
  content: '';
  position: fixed;
  inset: 0;
  background: var(--overlay);
  opacity: 0;
  transition: opacity 320ms cubic-bezier(0.32, 0.72, 0, 1);
  pointer-events: none;
  z-index: -1;
}

.basket-overlay.is-open::before {
  opacity: 1;
}

/* ── Layer map (outer → inner) ───────────────────────────────────────────────
   .basket-overlay    fixed full-screen backdrop + click-catcher
   .basket-sheet      the bottom sheet that slides up/down (owns the transform);
                      a flex column, NOT itself scrollable
     .basket-drag-zone  fixed 56px strip holding the grab handle — never scrolls
     .basket-scroll     the ONE element that actually scrolls (overflow-y:auto)
   Naming rule: only .basket-scroll scrolls; .basket-sheet only slides. ------- */
.basket-sheet {
  position: absolute;
  top: 92px;
  right: 0;
  bottom: 0;
  left: 0;
  display: flex;
  flex-direction: column;
  background: var(--surface-0);
  border-radius: var(--radius-md) var(--radius-md) 0 0;

  transform: translateY(100%);
  transition: transform 320ms cubic-bezier(0.32, 0.72, 0, 1);
}

.basket-drag-zone {
  flex-shrink: 0;
  height: 56px;
  position: relative;
}

.basket-drag-zone::before {
  content: '';
  width: 36px;
  height: 4px;
  background: var(--handle);
  border-radius: 2px;
  position: absolute;
  top: 14px;
  left: 50%;
  transform: translateX(-50%);
}

.basket-scroll {
  flex: 1;
  /* Without min-height:0 a flex child defaults to min-height:auto (its
     content's natural size), which overrides flex:1 and stops it from ever
     shrinking to fit — overflow-y:auto would then never see anything to
     scroll. */
  min-height: 0;
  overflow-y: auto;
  box-sizing: border-box;
  padding: 0 16px 32px;
  display: flex;
  flex-direction: column;
}

/* Scroll shadow at the boundary with .basket-drag-zone. Zero-height sticky
   anchor (doesn't push "Your order" down) + an absolutely-positioned ::before
   that paints the actual gradient — pointer-events:none so it never blocks
   taps on content scrolled underneath it. Only visible once basket.js's
   scroll listener adds .is-scrolled (there's nothing to shadow at scrollTop 0). */
.basket-scroll-fade {
  position: sticky;
  top: 0;
  height: 0;
  z-index: 2;
}

.basket-scroll-fade::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 32px;
  pointer-events: none;
  /* Neutral black muddies a cool dark palette — it desaturates whatever's
     underneath toward gray instead of reading as depth. The fix is a shade of
     the SHEET'S OWN background (#11141C), not an unrelated neutral: same hue,
     just darker, exactly how the reference example darkens its own #171B29
     down to #141825. #0F1219 here isn't a guess — it applies that same
     example's per-channel darkening ratio (~0.887×) to our background, which
     independently checks out to a near-identical ~23% relative-luminance drop
     (theirs is ~25%) — see scratchpad/apca-check.mjs. Solid color (not
     translucent) at the peak, like the reference: a wash of transparent black
     would still mix in neutral gray at whatever opacity; a same-hue solid
     doesn't. APCA's Lc metric floors to 0 for tone shifts this small (it's
     tuned for text-legibility thresholds, not sub-threshold dark-on-dark
     nuance) — raw relative luminance is what's actually informative here. */
  /* Solid stop = var(--scroll-fade) (#0F1219); the transparent stop MUST stay
     the same hue at alpha 0 (rgba(15,18,25,0)), NOT `transparent` — a black
     transparent would muddy the fade (see the note above). */
  background: linear-gradient(180deg, var(--scroll-fade) 0%, rgba(15, 18, 25, 0) 100%);
  opacity: 0;
  transition: opacity 200ms ease;
}

.basket-scroll.is-scrolled .basket-scroll-fade::before {
  opacity: 1;
}

.basket-overlay.is-open .basket-sheet {
  /* Resting position is translateY(0) — the 92px gap comes from `top: 92px`. */
  transform: translateY(0);
}

.basket-close {
  position: fixed;
  top: 104px;
  right: 16px;
  z-index: 1;
  width: 28px;
  height: 28px;
  border-radius: 14px;
  background: var(--surface-2);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: transform 120ms cubic-bezier(0.23, 1, 0.32, 1), opacity 320ms cubic-bezier(0.32, 0.72, 0, 1);
  opacity: 0;
  pointer-events: none;
}

.basket-overlay.is-open .basket-close {
  opacity: 1;
  pointer-events: auto;
}

.basket-close:active {
  transform: scale(0.9);
}

.basket-section-title {
  align-self: stretch;
  padding: 20px 12px 12px;
  opacity: 0.6;
  color: var(--text-primary);
  font-size: var(--font-caption);
  font-family: var(--font-family);
  font-weight: var(--fw-regular);
  text-transform: uppercase;
  line-height: var(--lh-caption);
}

.basket-list {
  display: flex;
  flex-direction: column;
  /* Row spacing is margin-bottom (not gap) so a removed row can collapse its own
     spacing too — a 0-height flex item would still reserve the gap on both sides
     and leave a visible snap. */
}

/* Clip/positioning container. The visible card is .basket-row-surface, which
   slides left over the .basket-row-delete revealed underneath. */
.basket-row {
  align-self: stretch;
  position: relative;
  margin-bottom: 8px;
  border-radius: var(--radius-md);
  overflow: hidden;   /* clips the delete layer + surface to the rounded corners */
  box-sizing: border-box;
}

.basket-row-surface {
  position: relative;
  z-index: 1;
  padding: 8px 16px 8px 8px;
  background: var(--surface-1);
  outline: 1px var(--border-subtle) solid;
  outline-offset: -1px;
  border-radius: var(--radius-md);
  display: flex;
  align-items: center;
  gap: 16px;
  box-sizing: border-box;
  will-change: transform;
  transform: translateX(0);
  /* Let vertical drags scroll the list natively; horizontal swipe is handled in
     JS (which preventDefaults once it locks the x-axis). */
  touch-action: pan-y;
}

/* The revealed zone underneath the surface is just the row's own background —
   the red only lives on the circular button itself (see below), so nothing
   colored shows until the circle has grown enough to read as "delete". */
.basket-row-delete {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  z-index: 0;
  width: 84px;
  border: none;
  margin: 0;
  background: transparent;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}

/* Apple-style circular delete affordance: starts small at rest (scale 0.4,
   set here) and grows toward full size as the surface is dragged — the
   growth itself is driven inline by attachSwipe() in basket.js (finger-
   tracked 1:1 during the drag, animated only on release/settle), so this
   base scale is just the closed resting state. No label — a growing circle
   doesn't have room for "Delete" text and doesn't need it. */
.basket-row-delete-circle {
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background: var(--danger);
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  transform: scale(0.4);
  transition: filter 120ms ease;
}

.basket-row-delete:active .basket-row-delete-circle {
  filter: brightness(0.9);
}

.basket-row-image {
  width: 80px;
  height: 80px;
  flex-shrink: 0;
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  background-repeat: no-repeat;
}

.basket-row-info {
  flex: 1;
  min-width: 0;
  align-self: stretch;
  display: flex;
  flex-direction: column;
  justify-content: center;
  /* No flex gap — spacing lives on the children so the collapsible breakdown
     line reserves zero space when hidden (a flex gap would keep a 4px slot). */
}

.basket-row-name {
  color: var(--text-primary);
  font-size: var(--font-heading);
  font-family: var(--font-family);
  font-weight: var(--fw-semibold);
  line-height: var(--lh-heading);
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}

.basket-row-price {
  margin-top: 4px; /* was the row-info flex gap */
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 6px;
  color: var(--text-secondary);
  font-size: var(--font-body);
  font-family: var(--font-family);
  font-weight: var(--fw-regular);
  line-height: var(--lh-body);
}

/* Per-unit breakdown under the line total, shown only at qty > 1. Muted meta:
   --text-tertiary is Lc −52 on the row surface — legibly below the total.
   Collapsible via grid-template-rows 0fr→1fr so it glides open/closed and the
   price glides with it instead of jumping (maga-motion: ease-out, <300ms). */
.basket-row-unit {
  display: grid;
  grid-template-rows: 0fr;
  margin-top: 0;
  opacity: 0;
  transition:
    grid-template-rows 260ms var(--ease-out),
    margin-top 260ms var(--ease-out),
    opacity 200ms ease;
}

.basket-row-unit.is-shown {
  grid-template-rows: 1fr;
  margin-top: 4px;
  opacity: 1;
}

.basket-row-unit-text {
  overflow: hidden; /* clipped by the collapsing grid row */
  min-height: 0;
  color: var(--text-tertiary);
  font-size: var(--font-caption);
  font-family: var(--font-family);
  font-weight: var(--fw-regular);
  line-height: var(--lh-caption);
}

.basket-row-stepper {
  flex-shrink: 0;
  align-self: center;
  /* Bigger than the grid card's stepper — this is the primary control in
     each cart row, with a full 80px-tall row to work with. */
  height: 40px;
  padding: 10px 14px;
  gap: 12px;
  border-radius: 20px;
}

.add-button.basket-row-stepper .counter-text {
  font-size: var(--font-heading);
  line-height: var(--lh-body);
}

.basket-row-stepper .icon-plus,
.basket-row-stepper .icon-minus {
  width: 20px;
  height: 20px;
}

.basket-row-stepper .icon-plus::before,
.basket-row-stepper .icon-minus::before {
  width: 20px;
  height: 20px;
}

/* ── Promo code ───────────────────────────────────────────────────────────
   Sits between the item list and the totals footer — same 12px extra inset
   as .basket-section-title/.basket-footer, so it lines up with their text. */
.basket-promo {
  align-self: stretch;
  padding: 4px 12px 8px;
}

.basket-promo-field {
  display: flex;
  gap: 8px;
}

.basket-promo-input {
  flex: 1;
  min-width: 0;
  height: 44px;
  padding: 0 16px;
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-sm);
  background: var(--surface-1);
  color: var(--text-primary);
  font-family: var(--font-family);
  font-size: var(--font-body);
  font-weight: var(--fw-regular);
  outline: none;
  transition: border-color 160ms ease;
}

.basket-promo-input::placeholder {
  color: var(--text-muted);
}

.basket-promo-input:focus {
  border-color: var(--accent);
}

.basket-promo.has-error .basket-promo-input {
  border-color: var(--danger);
}

/* Same tinted-accent recipe as .basket-toast-undo (#C2CDFF on accent 14% —
   already APCA-verified there, Lc −70 on this same dark surface). */
.basket-promo-apply {
  flex-shrink: 0;
  height: 44px;
  padding: 0 20px;
  border: none;
  border-radius: var(--radius-sm);
  background: rgba(96, 128, 255, 0.14);
  color: #C2CDFF;
  font-family: var(--font-family);
  font-size: var(--font-body);
  font-weight: var(--fw-semibold);
  cursor: pointer;
  transition: transform 160ms var(--ease-out), background-color 160ms ease;
}

.basket-promo-apply:active {
  transform: scale(0.96);
  background: var(--tab-tint-18);
}

/* Validation round-trip to the Worker in flight — the JS already ignores
   repeat taps (promoCheckInFlight); this just makes the wait visible. */
.basket-promo.is-checking .basket-promo-apply {
  opacity: 0.5;
  pointer-events: none;
}

.basket-promo-message {
  margin-top: 6px;
  min-height: 16px;
  color: var(--danger);
  font-size: var(--font-caption);
  font-family: var(--font-family);
  line-height: var(--lh-caption);
}

/* Applied state: the input+button swap for a chip showing the active code. */
.basket-promo-chip {
  display: none;
  align-items: center;
  justify-content: space-between;
  height: 44px;
  padding: 0 8px 0 16px;
  border-radius: var(--radius-sm);
  background: rgba(96, 128, 255, 0.14);
}

.basket-promo.is-applied .basket-promo-field {
  display: none;
}

.basket-promo.is-applied .basket-promo-message {
  display: none;
}

.basket-promo.is-applied .basket-promo-chip {
  display: flex;
}

.basket-promo-chip-text {
  color: #C2CDFF;
  font-family: var(--font-family);
  font-size: var(--font-body);
  font-weight: var(--fw-regular);
}

/* The code itself (basket.js wraps it in a <b>) is the one emphasized part —
   "Promocode" is just the label. */
.basket-promo-chip-text b {
  font-weight: var(--fw-semibold);
}

.basket-promo-remove {
  flex-shrink: 0;
  width: 28px;
  height: 28px;
  border: none;
  border-radius: 14px;
  background: transparent;
  color: #C2CDFF;
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
  transition: transform 120ms var(--ease-out), background-color 160ms ease;
}

.basket-promo-remove:active {
  transform: scale(0.9);
  background: var(--tab-tint-18);
}

.basket-footer {
  align-self: stretch;
  padding: 20px 12px 12px;
  display: flex;
  flex-direction: column;
}

.basket-footer-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

/* Subtotal/discount rows only exist once a promo code is applied — hidden by
   default so an undiscounted cart looks exactly like it did before this
   feature (a single Total row). */
.basket-footer-subtotal,
.basket-footer-discount {
  display: none;
  margin-bottom: 6px;
}

.basket-footer-subtotal.is-shown,
.basket-footer-discount.is-shown {
  display: flex;
}

.basket-footer-sub-label,
.basket-footer-sub-value {
  color: var(--text-muted);
  font-size: var(--font-body);
  font-family: var(--font-family);
  font-weight: var(--fw-regular);
  line-height: var(--lh-body);
}

.basket-footer-discount-value {
  color: var(--danger);
}

.basket-footer-text {
  color: var(--text-primary);
  font-size: var(--font-heading);
  font-family: var(--font-family);
  font-weight: var(--fw-semibold);
  line-height: var(--lh-heading);
}

.basket-suggestions {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 179px;
  gap: 12px;
  overflow-x: auto;
  scrollbar-width: none;
  -ms-overflow-style: none;
  /* Horizontal carousel inside a vertical flex-column scroller: without this it
     collapses to ~0 height (an overflow container gets min-height:0 and shrinks
     under its cards), so the cards spill out and can't be scrolled to. Pin its
     height to the cards. */
  flex-shrink: 0;
  /* Full-bleed like .product-grid: the parent .basket-scroll pads 16px on each
     side, which would clip the carousel 16px short of the screen edge (cards
     look "cut off"). Cancel that padding with negative margins so the SCROLL
     viewport reaches the sheet edge, then re-add 16px as internal padding so the
     first/last card still line up with the 16px content gutter — cards now bleed
     smoothly off the edge instead of being clipped early. */
  margin: 0 -16px;
  padding: 0 16px 12px;
}

.basket-suggestions::-webkit-scrollbar {
  display: none;
}

/* ── Undo toast ───────────────────────────────────────────────────────────────
   Fixed bottom bar (like the order bar) shown after a swipe-delete. Sits on
   <body>, above the overlay, so it survives the sheet closing on last item. */
.basket-toast {
  position: fixed;
  left: 16px;
  right: 16px;
  bottom: 24px;
  z-index: 200;
  padding: 12px 12px 12px 16px;
  background: var(--surface-2);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-order);

  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;

  /* Enters from just below, like it slid up from the bottom edge (spatial). */
  opacity: 0;
  transform: translateY(120%);
  pointer-events: none;
  transition: transform 320ms var(--ease-drawer), opacity 320ms var(--ease-drawer), bottom 220ms var(--ease-out);
}

.basket-toast.is-visible {
  opacity: 1;
  transform: translateY(0);
  pointer-events: auto;
}

/* The order bar sits at bottom:24px too (56px tall) — collide when the sheet
   is closed and the cart still has items. basket.js toggles this class (see
   repositionToast()) to clear it: 24 (bar's own offset) + 56 (bar height) +
   12 (gap) = 92. */
.basket-toast.is-above-order-bar {
  bottom: 92px;
}

.basket-toast-left {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
}

.basket-toast-timer-wrap {
  position: relative;
  flex-shrink: 0;
  width: 22px;
  height: 22px;
}

/* Countdown ring direction, verified programmatically (not by eye — small
   partial-arc rings are easy to misjudge on a compressed screenshot, learned
   the hard way). Method: isPointInStroke() at 12 sample angles around the
   ring, comparing this transform's visible/gap pattern at each angle θ
   against the previous transform's pattern at (180−θ) — a 12/12 match
   confirms this is its exact top-to-bottom mirror, independent of exactly
   which clock position the gap starts at.
   Drains smoothly (linear, in basket.js) — the numeral overlay is what
   carries the once-per-second "clock tick" identity, not the ring itself. */
.basket-toast-timer {
  transform: scaleY(-1) rotate(-90deg) scaleX(-1);
}

.basket-toast-timer-track {
  fill: none;
  stroke: rgba(255, 255, 255, 0.12);
  stroke-width: 2;
}

.basket-toast-timer-fill {
  fill: none;
  stroke: var(--accent);
  stroke-width: 2;
  stroke-linecap: round;
}

.basket-toast-timer-num {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text-primary);
  font-family: var(--font-family);
  font-size: 11px;
  font-weight: var(--fw-semibold);
  line-height: 1;
  font-variant-numeric: tabular-nums; /* digit width doesn't shift the ring as it ticks 5→1 */
}

.basket-toast-text {
  color: var(--text-primary);
  font-family: var(--font-family);
  font-size: var(--font-body);
  font-weight: var(--fw-regular);
  line-height: var(--lh-body);
}

/* Pill button, same tinted-accent language as the active +/- stepper — but a
   SMALLER radius than the free-floating stepper pill. Nested-radius rule
   (maga-spacing-hierarchy): inner = outer − padding = 16 (.basket-toast) − 12
   (its padding) = 4. Reusing the stepper's full-pill radius here would give
   this button the SAME curvature as its container, which collapses the
   corner instead of reading as concentric. */
.basket-toast-undo {
  flex-shrink: 0;
  height: 32px;
  padding: 8px 14px;
  border: none;
  border-radius: var(--radius-xs);
  background: rgba(96, 128, 255, 0.14); /* --accent (#6080FF) at 14% */
  /* Text composited on this bg (over --surface-2 → ≈#323951): plain --accent
     (#6080FF) was Lc −33 (Non-Text); #8FA5FF was still only −48 (Large Text —
     fails at this button's actual 15px/590, that band needs 24px+/700).
     #C2CDFF → Lc −70 (Content Text), passes. maga-contrast. */
  color: #C2CDFF;
  font-family: var(--font-family);
  font-size: var(--font-body);
  font-weight: var(--fw-semibold);
  line-height: var(--lh-caption);
  cursor: pointer;
  transition: transform 160ms var(--ease-out), background-color 160ms ease;
}

.basket-toast-undo:active {
  transform: scale(0.94);
  background: var(--tab-tint-18);
}

/* Оформление заказа. Кнопка появляется только у клубов с checkoutUrl —
   см. buildFooter() в basket.js. */
.basket-checkout {
  width: 100%;
  min-height: 52px;
  margin-top: 16px;
  padding: 0 20px;
  border: 0;
  border-radius: var(--radius-sm);
  background: var(--accent);
  color: var(--text-on-accent);
  font-family: var(--font-family);
  font-size: var(--font-cta);
  font-weight: var(--fw-semibold);
  cursor: pointer;
  transition: transform .16s var(--ease-out), filter .18s ease;
}
.basket-checkout:active { transform: scale(.98); }
@media (hover: hover) and (pointer: fine) {
  .basket-checkout:hover { filter: brightness(1.08); }
}
