/* =============================================================================
   LONDON SPOT: components.css
   Every component in design.md §6, mobile-first, all states.  [FOUNDATION owns]
   -----------------------------------------------------------------------------
   Requires base.css (tokens, .icon, .dot, focus ring, reduced motion).
   States shipped on every interactive component:
   hover · active · focus-visible · disabled · loading · empty · error.
   The state colours are design.md §2's table; every hover is inside
   `@media (hover:hover)`, and press is the touch answer.

      0  The touch contract (shared)
      1  Buttons + icon buttons
      2  Badges and the count badge
      3  Chips
      4  Product card, and the missing-photo device
      5  Section heading
      6  Inputs, selects, checkbox/radio, quantity stepper
      7  Hotline (the red help panel)
      8  Breadcrumbs
      9  Pagination
     10  Toast
     11  Skeleton
     12  Empty state
     13  Rail (horizontal scroll-snap)
     14  Disclosure (accordion)
     15  Segmented control
     16  Overlay scrim
     17  Reduced motion
   ========================================================================== */


/* ── 0. THE TOUCH CONTRACT ─────────────────────────────────────────────────
   Two properties every control in this file needs and neither of which any
   of them can be trusted to get by default.

   `touch-action:manipulation` drops the 300ms click delay a mobile browser
   holds a tap for while it waits to see whether a second tap is coming. On
   a shop that is 300ms added to every add-to-cart, every filter chip and
   every step of checkout, and the double-tap-to-zoom it buys back is not a
   gesture anyone performs on a 44px pill.

   The tap highlight is set on purpose rather than left to the platform.
   Every control here already draws its own press state (a darker fill, a
   scale, a wash), and the platform's grey-blue rectangle lands on top of
   that with the wrong shape: it ignores border-radius on some builds and
   flashes a colour from no palette on this site. Removing it is only
   honest because the replacement is already drawn. `transparent` is the
   value, never `rgba(0,0,0,0)`, so nothing here carries a literal.

   layout.css sets the same pair on the shell's own classes. This is the
   component half of it, and between the two every tappable thing on the
   site is covered.
   ---------------------------------------------------------------------- */
.btn,.iconbtn,.chip--filter,.chip--sug,.chip__x,
.input,.textarea,.select,.opt,
.qty__btn,.qty__in,.pager__i,
.acc__trigger,.sechead__all,.rail-nav__b,.seg__b,
.toast__act,.toast__x,.crumbs a,.link{
  touch-action:manipulation;
  -webkit-tap-highlight-color:transparent;
}


/* HOVER IS GATED ON `@media (hover:hover)` THROUGHOUT THIS FILE.
   A touch screen fires :hover on tap and then leaves it stuck until the
   next tap lands somewhere else, so on a phone every button a shopper
   pressed kept its hover fill, and on the catalog's filter row a chip that
   had merely been tapped looked exactly like the ones that are selected.
   The card block already gated its own hover for this reason; these are the
   shared controls doing the same. Nothing about a pointer's experience
   changes, and the rules stay where they were, so the cascade does not
   move: a media block has no specificity of its own.
   Press and focus are NOT gated. A touch device has a press, and a
   keyboard can drive a touch device.                                       */

/* ── 1. BUTTONS ────────────────────────────────────────────────────────────
   44px everywhere, 48px for hero/checkout, 40px compact ONLY where the
   pointer is fine. Red is the only action fill on the site: one primary
   per section.
   ---------------------------------------------------------------------- */
.btn{
  position:relative;
  display:inline-flex; align-items:center; justify-content:center;
  gap:var(--s-2);
  height:44px; padding-inline:var(--s-6);    /* horizontal padding = 2× vertical */
  /* THE CONTROL RUNG. --r-sm is where .input, .select, .textarea and
     .iconbtn already sit; the pill was the one control standing off the
     system's own radius ladder (control 10 -> card 14 -> panel 20), which
     is why a .btn next to a field never quite agreed with it. The controls
     that touch a button follow it here: .hsearch__box, .qty and the two
     CTA skeletons. Chips, badges and counters stay --r-pill on purpose,
     because a chip IS a pill. See .chip and .cx-fbtn__n.                 */
  border-radius:var(--r-sm);
  font-family:var(--f-ui);
  font-size:var(--t-body); font-weight:600; line-height:var(--lh-ui);
  white-space:nowrap; text-align:center;
  text-decoration:none;                      /* .btn is worn by <a> as often as <button> */
  cursor:pointer; user-select:none;
  transition:background-color var(--d-hover) var(--ease),
             color            var(--d-hover) var(--ease),
             box-shadow       var(--d-hover) var(--ease),
             transform        var(--d-press) var(--ease-press);
}
/* The focus ring gets its --ring-off of air. Flush against the edge it
   reads as a border rather than a ring.                                   */
.btn:focus-visible{ outline-offset:var(--ring-off) }
/* The glyph is 20px in a button and 18px in .btn--sm, whatever size class
   the caller wrote on it, and it pulls in its own leading (design.md §6). */
.btn .icon{ width:20px; height:20px; --sw:2; margin-block:-2px }
.btn--sm .icon{ width:18px; height:18px }

/* variants --------------------------------------------------------------
   The primary used to throw a red halo: three CTA shadow tokens, rest, hover
   and press, all rgba(205,68,78,...) at a 14px to 20px blur. That is a glow,
   and RULES bans glow. It rises on the neutral ladder instead: --sh-1 at
   rest, --sh-2 on hover, flat and scale(.97) on press. Nothing is lost,
   because the fill was always doing the work: --red to --red-700 to
   --red-800 is a visible three step ramp and it stays exactly as it was.  */
.btn--primary{ background:var(--red); color:var(--on-fill); box-shadow:var(--sh-1) }
@media (hover:hover){
  .btn--primary:hover{ background:var(--red-700); box-shadow:var(--sh-2) }
}
/* PRESS IS A SCALE, NOT A NUDGE. translateY(1px) moves the edge by one
   device pixel and nothing else; at 44px tall that is below the threshold
   where a shopper registers the control answered. scale() takes the label
   and the icon down with the pill, so the whole object reacts.          */
.btn--primary:active{ background:var(--red-800); transform:scale(.97);
                      box-shadow:none }

.btn--dark{ background:var(--slate-800); color:var(--on-fill) }
@media (hover:hover){
  .btn--dark:hover{ background:var(--slate-700) }
}
.btn--dark:active{ background:var(--slate-900); transform:scale(.97) }

/* The secondary stands in a row with fields (the filter button beside the
   sort select), so it takes the field's edge: one --bw-1 line of --bd-3,
   3.37:1 on --paper. It was a 2px --bd-2 ring (1.44:1), twice the weight
   and a third of the contrast of the select beside it (audit C7). Hover
   firms the edge to --ink-3, as a field's does.                           */
.btn--ghost{ background:transparent; color:var(--ink);
             box-shadow:inset 0 0 0 var(--bw-1) var(--bd-3) }
@media (hover:hover){
  .btn--ghost:hover{ background:var(--paper); color:var(--ink-strong);
                     box-shadow:inset 0 0 0 var(--bw-1) var(--ink-3) }
}
.btn--ghost:active{ background:var(--page-2); transform:scale(.97) }

.btn--quiet{ background:transparent; color:var(--ink-2); padding-inline:var(--s-3) }
@media (hover:hover){
  .btn--quiet:hover{ background:var(--page-2); color:var(--ink) }
}
.btn--quiet:active{ background:var(--bd); transform:scale(.97) }

/* sizes ------------------------------------------------------------------
   The compact step is a POINTER question, not a width question. A 1024px
   viewport is a desktop on a laptop and a tablet held in landscape, and on
   the second one a 40px pill breaks the 44px floor. `hover:hover` plus
   `pointer:fine` is the pair that actually means "a mouse is driving this",
   so the compact height lands there and nowhere else: a touch device keeps
   44px at every width it can reach.                                        */
.btn--lg{ height:48px; padding-inline:var(--s-8) }
.btn--sm{ height:44px; padding-inline:var(--s-4); font-size:var(--t-sm) }
@media (min-width:1024px) and (hover:hover) and (pointer:fine){
  .btn--sm{ height:40px }                              /* compact desktop toolbar */
}
/* A label never clips and never ellipsises (design.md §6). A full-width
   button is the one that may wrap its label to a second line: the height
   grows from its step instead of cutting the words (the filter sheet's
   apply label lost 24px at 320, audit C3). An inline button may not wrap;
   its label is shortened in the locale file instead.                      */
.btn--block{
  width:100%; display:flex;
  height:auto; min-height:44px; padding-block:var(--s-2);
  white-space:normal;
}
.btn--block.btn--lg{ min-height:48px }

/* A call button's two labels (design.md §6 "Button", §14.14): below 768px
   it says „დარეკვა" alone, where the tel: link dials, and from 768px it
   carries the number. The button holds both, <span class="l-short"> and
   <span class="l-long">, and this one rule shows one of them wherever a
   call button stands: the product page, the wishlist's call block, the
   empty aisle and the contact page. It was written twice (catalog.css
   .cx-tel, product.css .buybox__call) and contact had neither, so its
   button read the number at every width (review S3). */
.btn .l-long{ display:none }
@media (min-width:768px){
  .btn .l-short{ display:none }
  .btn .l-long{ display:inline }
}

/* disabled ---------------------------------------------------------------
   The opacity is --o-disabled (.55), and the number is measured rather than
   chosen. A disabled control is exempt from WCAG 1.4.3, but a label nobody
   can read is a label nobody can read, so this system holds its disabled
   text to the 3:1 floor it holds every other boundary to. Composited over
   --page: --ink at .45 is 2.83:1 and at .55 it is 3.78:1; --on-fill on a
   --slate-800 fill at .45 is 2.43:1 and at .55 it is 3.07:1. .55 is the
   first step on the ladder where every variant clears, and it still reads
   unmistakably as off.

   --ink-2 cannot survive any fade at all (2.47:1 at .55), so the quiet
   button's disabled label borrows --ink. Fading a word that was already the
   secondary tier twice is what put it under the floor.                     */
.btn:disabled,
.btn[aria-disabled="true"]{
  opacity:var(--o-disabled); cursor:not-allowed; box-shadow:none; transform:none;
}
.btn--quiet:disabled,
.btn--quiet[aria-disabled="true"]{ color:var(--ink) }
/* The primary is the one button that cannot be disabled by opacity. At .45
   the red fill composites to #E4A6AC and its own white label to #FBFBFC:
   1.96:1, unreadable, and this is the checkout button. So it drops the fill
   and the elevation entirely for a neutral pill: --ink-2 on --bd is 5.8:1.
   --ink-3 was the softer choice but only reaches 4.0:1 on --bd.            */
.btn--primary:disabled,
.btn--primary[aria-disabled="true"]{
  opacity:1;
  background:var(--bd); color:var(--ink-2);
  box-shadow:none;
}
/* Hover is locked per variant, and the lock is a hover rule like any other,
   so it is gated with them. `background-color:inherit` used to do this job
   and it was a bug: inherit takes the PARENT's background, so a disabled
   .btn--dark inside a white card lost its fill on hover and the white label
   vanished into the card.                                                  */
@media (hover:hover){
  .btn:disabled:hover,.btn[aria-disabled="true"]:hover{
    box-shadow:none; transform:none;
  }
  .btn--dark:disabled:hover,.btn--dark[aria-disabled="true"]:hover{
    background:var(--slate-800); color:var(--on-fill);
  }
  .btn--ghost:disabled:hover,.btn--ghost[aria-disabled="true"]:hover{
    background:transparent; color:var(--ink); box-shadow:inset 0 0 0 var(--bw-1) var(--bd-3);
  }
  .btn--quiet:disabled:hover,.btn--quiet[aria-disabled="true"]:hover{
    background:transparent; color:var(--ink);
  }
  .btn--primary:disabled:hover,
  .btn--primary[aria-disabled="true"]:hover{ background-color:var(--bd) }
}

/* Loading -----------------------------------------------------------------
   THE SPINNER IS GONE. It was an 18px ring turning on `animation:ls-spin
   .7s linear infinite`, and RULES rule 9 caps every animation at 300ms with
   no exemption for a loop, exactly as it capped the skeleton's shimmer. A
   ring that completed a turn inside 300ms would not read as waiting, it
   would read as broken, so there is no compliant version of it and it is
   deleted rather than shortened.

   What replaces it is the state itself. The button dims, the cursor says
   the machine is busy, and pointer-events stop a second submit; the WORDS
   are what say what is happening, and they are the caller's job:

     THE JS CONTRACT. On the same tick as `data-loading`, set
     `aria-busy="true"` and swap the label to „იტვირთება…", then put both
     back. cart.js already sets aria-busy; auth.js:1091, home.js:529 and
     product.js:708 set the attribute alone and still need the label. The
     copy cannot live here, because a string a person reads belongs in the
     JS string tables with every other one, not in a stylesheet.

   The fade is --o-busy (.8), not the disabled .55: a loading button is still
   the live control the shopper just pressed. Measured on the primary, which
   is the one that carries white on a fill: --on-fill on --red at .8 over
   --page composites to 3.40:1, clear of the 3:1 floor. It is also declared
   after the disabled block on purpose, because cart.js sets `disabled` and
   `data-loading` together and loading is the truer of the two states.      */
.btn[data-loading]{
  opacity:var(--o-busy); cursor:progress; pointer-events:none;
  box-shadow:none; transform:none;
}

/* Icon button (design.md §6): a glyph and no visible word. 44x44 at every
   width, no compact step, because the target is all it has. Always an
   aria-label, never revealed by hover.

   TWO SHAPES AND NO MORE (six looks shipped, audit C23):
     .iconbtn          a square on --r-sm, transparent, in bars, toolbars,
                       rows and panels, and the card's heart;
     .iconbtn--float   a circle with its own --paper disc and --sh-float, and
                       only where it floats over a photograph (the zoom disc,
                       the lightbox's arrows and close).
   A toggle (aria-pressed) changes its glyph and never its name: the heart is
   `heart` at rest and `heart-fill` saved, in --red-700 (a state, §2). */
.iconbtn{
  display:inline-grid; place-items:center;
  width:44px; height:44px; flex:none;
  border-radius:var(--r-sm);
  color:var(--ink-2); background:transparent;
  transition:background-color var(--d-hover) var(--ease),
             color            var(--d-hover) var(--ease),
             transform        var(--d-press) var(--ease-press);
}
.iconbtn:focus-visible{ outline-offset:var(--ring-off) }
@media (hover:hover){
  .iconbtn:hover{ background:var(--page-2); color:var(--ink) }
}
.iconbtn:active{ background:var(--bd); transform:scale(.97) }
.iconbtn[data-wish][aria-pressed="true"]{ color:var(--red-700) }
/* Same measured floor as the button above: the glyph is a graphical object
   and 3:1 is its floor, so the fade stops at --o-disabled and the icon takes
   --ink on the way down (--ink-2 at .55 is 2.47:1 on --page, --ink 3.78:1). */
.iconbtn:disabled,
.iconbtn[aria-disabled="true"]{
  opacity:var(--o-disabled); cursor:not-allowed; background:transparent; color:var(--ink);
}
@media (hover:hover){
  .iconbtn:disabled:hover,
  .iconbtn[aria-disabled="true"]:hover{ background:transparent; color:var(--ink) }
}

.iconbtn--float{
  border-radius:var(--r-pill);
  background:var(--paper); box-shadow:var(--sh-float);
}
@media (hover:hover){
  .iconbtn--float:hover{ background:var(--page-2); color:var(--ink) }
}
.iconbtn--float:active{ background:var(--bd) }
.iconbtn--float:disabled,
.iconbtn--float[aria-disabled="true"]{ background:var(--paper) }
@media (hover:hover){
  .iconbtn--float:disabled:hover,
  .iconbtn--float[aria-disabled="true"]:hover{ background:var(--paper) }
}
/* The circle alone, for the lightbox close until the product page moves it
   to .iconbtn--float. Kept so that markup does not lose its shape. */
.iconbtn--round{ border-radius:var(--r-pill) }


/* ── 2. BADGES ─────────────────────────────────────────────────────────────
   Badges mark a product (design.md §6 "Badge"). --t-xs is Latin and
   numerals only (§3), so a badge that carries Georgian is .badge--ka: the
   Georgian floor on a 24px pill. A condition badge keeps that size on an
   English page too, so the two layouts match.

   THE CONDITION BADGES. 4,907 of 5,742 units are outlet (grade 1) or need
   repair (grade 2), and none of them said so on a card (audit C1, p0).
     .badge--outlet  grade 1, „აუთლეტი": --page-2 with --ink (15.06:1) and a
                     --bw-1 --bd-2 inset edge, so the pill keeps its shape
                     on a white photograph;
     .badge--repair  grade 2, „შესაკეთებელი": --slate-800 with --on-fill
                     (11.01:1).
   Never red (it would read as a sale), never green (a promise), never
   --warn: the words carry the meaning and two neutral fills tell the grades
   apart. Grade 0 carries no badge. The condition comes first when a discount
   joins it. .badge--new stays in the kit and renders nowhere: the flag it
   showed marks the manifest lots, not the state of the item (OD2).
   ---------------------------------------------------------------------- */
.badge{
  display:inline-flex; align-items:center; gap:var(--s-1);
  height:22px; padding-inline:var(--s-2);
  border-radius:var(--r-pill);
  font-size:var(--t-xs); line-height:1; font-weight:600;
  white-space:nowrap;
}
.badge--ka{
  height:24px; padding-inline:var(--s-2);
  font-size:var(--t-small-ka); line-height:var(--lh-ui); font-weight:500;
}
.badge--outlet{
  background:var(--page-2); color:var(--ink);
  box-shadow:inset 0 0 0 var(--bw-1) var(--bd-2);
}
.badge--repair{ background:var(--slate-800); color:var(--on-fill) }
.badge--new  { background:var(--slate-800); color:var(--on-fill) }
.badge--sale { background:var(--red);  color:var(--on-fill); font-family:var(--f-disp); font-weight:700 }

/* THE COUNT BADGE (design.md §6): one class for the header action, the
   bottom-bar item and the filter button, which carried three sizes of one
   idea (18px, 17px and 20px, audit C24). The count role: --t-xs, 700,
   tabular, at least 18px square, --red with --on-fill (a state, §2).
   The number is painted from data-n through ::after, so it never enters the
   control's accessible name ahead of its label (WCAG 2.5.3); core.js
   writes data-n on every [data-count-glyph] and caps it at „99+".
     .count          inline, as on the filter button: no ring;
     .count--icon    on an icon: the icon's top inline end, with a --bw-2
                     --paper ring that cuts it from the glyph. Its host
                     needs position:relative.                               */
.count{
  display:inline-grid; place-items:center; flex:none;
  min-width:18px; height:18px; padding-inline:var(--s-1);
  border-radius:var(--r-pill);
  background:var(--red); color:var(--on-fill);
  font-family:var(--f-disp); font-size:var(--t-xs); font-weight:700; line-height:1;
  font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
}
.count[data-n]::after{ content:attr(data-n) }
.count--icon{
  position:absolute; top:-6px; inset-inline-start:50%;
  box-shadow:0 0 0 var(--bw-2) var(--paper);
}


/* ── 3. CHIPS ────────────────────────────────────────────────────────────
   A chip's label is Georgian, so it is 14px and never smaller (--t-sm is
   the same 14px as --t-small-ka; the chip reaches for the type step, not
   the Georgian-floor alias, because a Latin brand name wears the same
   pill). The LINE HEIGHT is the part that had to move: at `line-height:1`
   the line box is exactly 14px tall and .chip__t clips to it, and of the
   33 everyday mkhedruli letters the eight that descend (ჟ ღ ყ ც ძ წ ჭ ჯ)
   hang below that box. „ღვინის ჭიქა" lost the tails of two letters inside
   a pill that had 14px of empty room above and below them. --lh-small-ka
   (1.45) gives the line box 20.3px, which is what the glyphs actually
   occupy, and the pill grows from 28 to 32px to hold it: on the 4px
   ladder, and still the quietest thing in any row it sits in.
   ---------------------------------------------------------------------- */
.chip{
  display:inline-flex; align-items:center; gap:var(--s-2);
  max-width:100%; min-width:0;
  height:var(--s-8); padding-inline:var(--s-3);
  border-radius:var(--r-pill);
  background:var(--page-2); color:var(--ink-2);
  font-size:var(--t-sm); line-height:var(--lh-small-ka);
}
/* A chip's label is never capped by a fixed width (design.md §6): the pill
   is at most as wide as its row, and the ellipsis is only the fallback for a
   row narrower than the name (the 26ch cap cut „სამზარეულო და საოჯახო
   ნივთები" even on a 1,080px row, audits C25, K14). */
.chip__t{ min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; line-height:var(--lh-small-ka) }
/* A count inside a chip (a price band's) follows its label by the chip's
   --s-2 gap and never touches it; a chip that holds one may wrap the count
   under the label rather than squeeze it („₾51-150" ran into „1 560" at
   360, audit K3). */
.chip__n{
  flex:none;
  font-family:var(--f-disp); font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
}
.chip:has(> .chip__n){ flex-wrap:wrap; row-gap:0; height:auto; min-height:var(--s-8) }

/* Filter chip: 44px on touch, 40px where the pointer is fine.
   A filter chip is a control, so RULES's 44px floor is the floor, and it is
   gated on the pointer rather than on the viewport for the same reason
   .btn--sm is: a 1024px tablet in landscape is a touch device.
   The WHOLE CHIP is the remove control on the active-filter row (catalog.js
   renders a <button> with the label and an aria-hidden × inside it), so the
   44px target is the pill, not the glyph: the × has no separate hit area to
   grow and must not acquire one, or a row of filters would carry two
   controls per chip that both do the same thing.                           */
/* The toggle (a price band, a facet choice) and the suggestion (a link to
   an aisle, in empty states and in search, .chip--sug) share one look: a
   quiet control on --paper with a --bw-1 --bd-2 edge. The old one and a half
   pixel edge rendered as 1px beside a 2px ring (audit C7). */
.chip--filter,
.chip--sug{
  height:44px; padding-inline:var(--s-4);
  background:var(--paper); color:var(--ink);
  border:var(--bw-1) solid var(--bd-2);
  cursor:pointer;
  transition:background-color var(--d-hover) var(--ease),
             border-color     var(--d-hover) var(--ease),
             color            var(--d-hover) var(--ease);
}
.chip--filter:has(> .chip__n),
.chip--sug:has(> .chip__n){ height:auto; min-height:44px }
@media (min-width:1024px) and (hover:hover) and (pointer:fine){
  .chip--filter,.chip--sug{ height:40px }
  .chip--filter:has(> .chip__n){ height:auto; min-height:40px }
}
@media (hover:hover){
  .chip--filter:hover,
  .chip--sug:hover{ background:var(--paper-2); border-color:var(--ink-strong); color:var(--ink-strong) }
}
.chip--filter:active,
.chip--sug:active{ background:var(--page-2) }
/* --o-disabled, the same measured step as the button: --ink at .45 on
   --paper is 2.87:1 and at .55 it is 3.83:1. The label AND the count take
   --ink on the way down: an --ink-3 count at .55 was 2.2:1 (audit A18).    */
.chip--filter:disabled,
.chip--filter[aria-disabled="true"]{ opacity:var(--o-disabled); cursor:not-allowed; color:var(--ink) }
.chip--filter:disabled .chip__n,
.chip--filter[aria-disabled="true"] .chip__n{ color:var(--ink) }
@media (hover:hover){
  .chip--filter:disabled:hover,
  .chip--filter[aria-disabled="true"]:hover{
    background:var(--paper); border-color:var(--bd-2); color:var(--ink);
  }
}

/* selected filter chip: dark-surface fill, white text, × to clear */
.chip--filter.is-selected,
.chip--filter[aria-pressed="true"]{
  background:var(--slate-800); border-color:var(--slate-800); color:var(--on-fill);
}
@media (hover:hover){
  .chip--filter.is-selected:hover,
  .chip--filter[aria-pressed="true"]:hover{ background:var(--slate-700); border-color:var(--slate-700) }
}
.chip--filter.is-selected:active,
.chip--filter[aria-pressed="true"]:active{ background:var(--slate-900); border-color:var(--slate-900) }

/* the × sits inside the chip, so it inherits the chip's full 44px height.
   The negative end margin cancels the chip's own inline padding so the ×
   reaches the pill's rounded end, otherwise its hover wash stops short and
   floats. The base value cancels .chip's 12px.                              */
.chip__x{
  display:grid; place-items:center;
  width:var(--s-8); align-self:stretch;
  margin-inline-end:calc(var(--s-3) * -1);
  border-radius:0 var(--r-pill) var(--r-pill) 0;
  color:currentColor;
}
/* Size modifier: the × on a chip whose inline padding is var(--s-4) (16px),
   i.e. every .chip--filter, carrying a 16px glyph (design.md §5: 14px text →
   16px icon). One value moves: the end margin has 16px to cancel rather than
   the base chip's 12, and 32px of box is already the right width for a 16px
   glyph with 8px either side, so the class only says the offset.
   catalog.css carried this as a local `.cx-chips .chip__x{margin-inline-end:
   -16px}`. This is where that fix belongs, and the local override can go.   */
.chip__x--16{
  margin-inline-end:calc(var(--s-4) * -1);
}
@media (hover:hover){
  .chip__x:hover{ background:var(--scrim) }
  .chip--filter:not(.is-selected) .chip__x:hover{ background:var(--page-2) }
}


/* ── 4. PRODUCT CARD: the catalog build's card ─────────────────────────────
   A photo, a name, a price and a heart (design.md §6 "Product card"). The
   catalog build has no add to cart: a card has one action, saving the item,
   and one destination, the product page, where the call is. The 56px row a
   lone heart stood in on every card is gone (audits C4, L15, H8, K26): the
   heart sits at the end of the price row.

   TWO LINKS, ONE TAB STOP. The photo well is a link and the title is a link,
   both to the same product. The title's link is the one in the tab order and
   the one a screen reader reads; the photo's is `aria-hidden` with
   `tabindex="-1"`, so a keyboard gets one stop per card and a pointer gets
   the whole card. Each link stretches over its own half of the card, so
   there is no overlay sitting on a link and no dead strip between them. The
   condition and the price are in the title link's accessible name as a
   visually hidden suffix (§12): a condition only the photograph shows is not
   stated.

   ONE CONTAINER. `card` is the whole card: the title steps from --t-sm to
   --t-body at 200px of card, the body gutter at 250px, and a card wider than
   the band's --card-max can only be a list row, where the well keeps its
   square. `cardbody` stays for the storefront's add control.

   The photograph never animates.
   ---------------------------------------------------------------------- */
.card{
  container:card / inline-size;
  position:relative; isolation:isolate;
  display:flex; flex-direction:column;
  min-width:0;
  background:var(--paper);
  border:var(--bw-1) solid var(--bd);
  border-radius:var(--r);
  overflow:hidden;
  transition:transform    var(--d-hover) var(--ease),
             box-shadow   var(--d-hover) var(--ease),
             border-color var(--d-hover) var(--ease),
             background-color var(--d-press) var(--ease-out);
}

/* The square well. `min-height:0` and the minmax(0,1fr) tracks are load-
   bearing: without them a portrait photo's min-content height wins over
   aspect-ratio and the card grows to the height of the JPEG. The photograph
   is inset by 8% of the well, at least --s-2, so its framing holds at every
   width (a fixed 16px gave the product 78% of the well at 320px and 84% at
   1440px, audit I7). The LINK is the well's one in-flow child and fills its
   content box; it is a grid item, never `position:absolute`.

   THE PHOTO STATES (§15.3), on data-photo, set by core.js:
     wait   loading: the plain --page-2 well, no glyph and no words, with
            aria-busy on the well while the photograph is being fetched. The
            same for eager and lazy cards (the kiosk used to stand in the
            well of every lazy card, audit I4);
     ok     the photograph on the white --paper well;
     none   missing or failed: the missing-photo device below.            */
.card__media{
  position:relative;
  aspect-ratio:1;
  min-height:0;
  overflow:hidden;
  background:var(--paper);
  padding:max(var(--s-2), 8%);
  display:grid;
  grid-template-rows:minmax(0,1fr);
  grid-template-columns:minmax(0,1fr);
  transition:background-color var(--d-hover) var(--ease);
}
.card__media[data-photo="wait"],
.card__media[data-photo="none"]{ background:var(--page-2) }
.card__medialink{
  grid-area:1 / 1;
  min-width:0; min-height:0;
  display:grid;
  grid-template-rows:minmax(0,1fr);
  grid-template-columns:minmax(0,1fr);
  place-items:center;
}
.card__img{
  width:100%; height:100%;
  object-fit:contain; object-position:center;
}
/* A lazy card's placeholder source is a transparent pixel, so nothing but the
   well's own fill shows while it waits. */
.card__media[data-photo="none"] .card__img{ display:none }

/* The badges: top-left on the photograph, stacked --s-1 apart, the
   condition first. They are aria-hidden: the condition is in the title
   link's name, and read twice it would be noise. z-index 2 inside the card's
   isolation. */
.card__badges{
  position:absolute; inset-block-start:var(--s-2); inset-inline-start:var(--s-2);
  z-index:2;
  display:flex; flex-direction:column; align-items:flex-start; gap:var(--s-1);
  max-width:calc(100% - var(--s-4));
  pointer-events:none;
}
/* A condition is never cut. A card is not drawn under --card-min (136px,
   §14.3), where „შესაკეთებელი" fits with room; a card that still is (a brand
   panel before it joins the grid table) keeps every letter by pulling the
   badges into the corner and the pill's padding to --s-1. */
@container card (max-width:135.98px){
  .card__badges{ inset-block-start:var(--s-1); inset-inline-start:var(--s-1); max-width:calc(100% - var(--s-2)) }
  .card__badges .badge{ padding-inline:var(--s-1) }
}

/* ── the missing-photo device (§6) ────────────────────────────────────────
   What an image slot shows when a product has no photograph or its
   photograph failed: the slot's own --page-2 fill, the photo-off glyph at
   28px (--sw 1.75) in --ink-3 (4.88:1 on --page-2) and, --s-2 under it, the
   words „ფოტო ჯერ არ არის" in the label role, --ink-2, centred. The words
   are a locale string, never part of an image. In a slot of 64px or less
   (search rows, the call bar, thumbnails, minis) .nophoto--sm draws the
   glyph alone, at 20px. It replaces the grey K6 kiosk, which said nothing
   about what was missing (audits I1, C13; OD10 default).
   Built by LS.photoOff() in core.js; never zoomable.                      */
.nophoto{
  grid-area:1 / 1;
  display:flex; flex-direction:column; align-items:center; justify-content:center;
  gap:var(--s-2);
  min-width:0; min-height:0; width:100%; height:100%;
  padding:var(--s-2);
  color:var(--ink-3);
  text-align:center;
}
.nophoto .icon{ width:28px; height:28px; --sw:1.75 }
.nophoto__t{
  max-width:100%;
  font-size:var(--t-small-ka); line-height:var(--lh-small-ka); font-weight:500;
  color:var(--ink-2);
  text-wrap:balance; overflow-wrap:break-word;
}
.nophoto--sm{ padding:0 }
.nophoto--sm .icon{ width:20px; height:20px; --sw:2 }

/* The aisle (.card__cat): one line, the meta role, ellipsised. Inside a
   category page it names the subcategory (LS.card's `aisle:'sub'`), because
   the page's h1 and crumbs already name the category (audits K27, D33). No
   dot and no colour of its own: a category is told apart by its name. */
.card__cat{
  font-size:var(--t-small-ka); line-height:var(--lh-small-ka);
  font-weight:400; color:var(--ink-3);
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
  min-width:0;
}

/* The body reads top down in the order a shopper reads a shelf: which aisle
   is this, what is it, what does it cost, and the one thing you can do about
   it. --card-pad is the body's inline gutter; it steps up with the card, not
   with the viewport. The bottom padding is small because the price row is
   44px and carries its own air around the figure. */
.card__body{
  --card-pad:var(--s-3);
  container:cardbody / inline-size;
  position:relative;
  display:flex; flex-direction:column;
  padding:var(--s-3) var(--card-pad) var(--s-1);
  flex:1; min-width:0;
}
@container card (min-width:250px){
  .card__body{ --card-pad:var(--s-4) }
}

/* The title: the name role, a two-line clamp with the second line reserved
   so the prices of a row line up. --t-sm under 200px of card and --t-body
   from 200px (a container query, so a grid row never mixes the two), 500,
   --lh-name. No balancing: a balanced clamp would give words away. */
.card__title{
  margin-top:var(--s-1);
  font-size:var(--t-sm); line-height:var(--lh-name); font-weight:500;
  letter-spacing:0; color:var(--ink);
  text-wrap:wrap;
  min-height:calc(var(--lh-name) * 2em);
  -webkit-line-clamp:2;
}
@container card (min-width:200px){
  .card__title{ font-size:var(--t-body) }
}
/* THE LINK IS THE CLAMPED BOX, not an inline run inside one. Inline, its
   box went on through every hidden line of a long title (up to six, 100px
   under the words a shopper sees), so a focused title "sat" under the
   bottom bar while its visible lines were clear of it, and scrolling a
   focused card into view measured the wrong box. The clamp count is
   inherited from .card__title, so a view that sets it there (the list
   row's one line) still decides it. */
.card__link{
  color:inherit;
  display:-webkit-box; -webkit-box-orient:vertical;
  -webkit-line-clamp:inherit;
  overflow:hidden; overflow-wrap:break-word;
}
/* The title's link stretches over the body, the photo's link over the well:
   two real links, no overlay on top of a link. The stretched area is the
   link's own pseudo-element, positioned on .card__body, so the clamp above
   does not clip it, and it makes the whole body a 44px target on touch. */
.card__link::after{
  content:""; position:absolute; z-index:1;
  inset:0;
}
@media (hover:hover){
  .card__link:hover{ color:var(--ink-link) }
}

/* THE PRICE ROW: the price at the start, the heart at the end, one row,
   44px, held to the bottom of the card by `margin-top:auto`, so the prices
   of a row line up whether a title runs to one line or two. The price is the
   price role (--t-num, 700, --ink-strong, tabular). With a discount, what it
   costs now comes first in --red-700, then the old price struck through in
   --ink-3; the pair wraps inside its own box and never pushes the heart. */
.card__price{
  display:flex; align-items:center; gap:var(--s-2);
  min-height:44px; min-width:0;
  margin-top:auto;
}
.card__prices{
  display:flex; align-items:baseline; flex-wrap:wrap; column-gap:var(--s-2);
  min-width:0;
}
.card .price{ font-size:var(--t-num); line-height:var(--lh-num) }
/* THE FOOT OF THE BAND. A price has the card container's width (the card
   inside its border) less 56px before it reaches the heart's 44px target:
   the body's 12px gutter before the figure, the target after it. The
   widest price on file, four figures with tetri, is 93px in Georgian
   („₾3 399,99") and 94.2px in English („₾3,399.99") at --t-num, so it
   needs 151px of card; narrower, it ran into the glyph (138px cards on a
   320px phone, 136px at --card-min; S-1). Under 153px of card (2px kept
   for rounding) the price takes --t-num-sm: 75.4px at most, clear of the
   target at --card-min with 14px of air to the glyph. One step for every
   card of a width, so a grid row never mixes two sizes; the row stays
   44px and the card's height does not move. A five figure price would
   need this measured again. */
@container card (max-width:152.98px){
  .card .price{ font-size:var(--t-num-sm) }
}
/* A reduced price is the one number on this card that is not the price.
   --red-700, never --red: 5.78:1 on --page and 6.18:1 on --paper. */
.card .price--off{ color:var(--red-700) }

/* THE HEART (.card__wish on .iconbtn): a bare icon button, a 44x44 target
   with a 20px glyph, transparent at rest in --ink-2. Its end margin cancels
   its own padding, so the glyph stands on the body's text edge (§14.2).
   Saved, it is heart-fill in --red-700 with aria-pressed="true" and the same
   accessible name. z-index 3 inside the card's isolation holds it above the
   title link's stretched area. Hover (pointer only) a --page-2 wash, press
   --bd and scale(.97): .iconbtn's own states. */
.card__wish{
  position:relative; z-index:3;
  margin-inline-start:auto;
  margin-inline-end:calc((44px - 20px) / -2);
}
.card__wish > .icon{ width:20px; height:20px; --sw:2 }
/* Its ring is drawn inside (§2: inside anything that clips). The card
   clips, and the heart's target ends on the card's inner edge under 250px
   of card and 4px from it (and from the foot) above: the outside ring
   reaches 5px and lost its end side and its foot (S-2). One rule at every
   width, so the heart's ring never changes shape with the card. */
.card__wish:focus-visible{ outline-offset:var(--ring-in) }

/* The list row (.catgrid--list, from the width its toggle appears at): a
   card wider than the band's --card-max can only be a row, and there the
   photo well keeps its square instead of stretching to the row's height
   (it measured 160 by 219, audit L30). The row is as tall as its well. */
@container card (min-width:400px){
  .card:not(.card--lead) > .card__media{ align-self:flex-start }
}

/* pointer hover: the card lifts; press (touch) is --paper-2 */
@media (hover:hover) and (pointer:fine){
  .card:hover{ transform:translateY(-2px); box-shadow:var(--sh-2); border-color:var(--bd-2) }
}
@media (hover:none){
  .card:active{ background:var(--paper-2) }
}
@supports selector(:has(*)){
  .card:has(.card__link:active),
  .card:has(.card__medialink:active){ background:var(--paper-2) }
}

/* the focus ring belongs to the whole card, not the two words of the title */
@supports selector(:has(*)){
  .card__link:focus-visible{ outline:none }
  .card:has(.card__link:focus-visible){
    outline:var(--ring) solid var(--focus); outline-offset:var(--ring-off);
  }
}

/* ── storefront only: the add control ────────────────────────────────────
   LS.card(p, {add:true}) renders a foot with the add button. No catalog page
   asks for it (design.md §6); it is kept so the storefront phase starts from
   a working card. Its in-cart receipt is a --bw-1 --ok edge and --ok text. */
.card__foot{
  display:flex; align-items:center; flex-wrap:wrap;
  gap:var(--s-2);
  padding-block:var(--s-2) var(--s-2);
}
.card__add{
  position:relative; z-index:3;
  flex:1 1 auto; min-width:0;
  padding-inline:var(--s-2); gap:var(--s-1);
}
@media (hover:none){ .card__add{ height:44px } }
@container cardbody (min-width:190px){
  .card__add{ padding-inline:var(--s-4); gap:var(--s-2) }
}
@container cardbody (min-width:340px){
  .card__add{ flex:0 0 auto; min-width:200px }
}
@media (hover:hover) and (pointer:fine){
  .card:hover .card__add:not(:disabled):not(.is-in){
    background:var(--red); color:var(--on-fill); box-shadow:none;
  }
  .card:hover .card__add:not(:disabled):not(.is-in):hover{ background:var(--red-700) }
}
.card__add.is-in{ box-shadow:inset 0 0 0 var(--bw-1) var(--ok); color:var(--ok) }
@media (hover:hover){
  .card__add.is-in:hover{ box-shadow:inset 0 0 0 var(--bw-1) var(--ok); color:var(--ok);
                          background:var(--paper-2) }
}

/* ── .card--lead: the head of a grid ───────────────────────────────────────
   Same card, two columns and two rows of one. It stays in the kit with no
   user (design.md §6): a ranking is read across a row of equals. Below
   768px it is an ordinary card.                                           */
@media (min-width:768px){
  .card--lead{ grid-column:span 2; grid-row:span 2 }
  .card--lead .card__media{ aspect-ratio:auto; flex:1 1 auto; min-height:220px }
  .card--lead .card__body{ padding-top:var(--s-4) }
  .card--lead .card__title{
    font-size:var(--t-h3); line-height:var(--lh-h3); font-weight:600;
    min-height:calc(var(--lh-h3) * 2em);
  }
  .card--lead .price{ font-size:var(--t-num-lg) }
}


/* ── 5. SECTION HEADING: the words alone ──────────────────────────────────
   The h2, then the "see all" link at the far edge of the same row. No mark,
   no rule, no eyebrow in front of it (design.md §6 "Section heading").
   ---------------------------------------------------------------------- */
/* One shape at every width. The link label is short (at most about twelve
   characters), so title and link share the row at 320px; if a title still
   pushes the link, the row wraps and the link starts its own line on the
   gutter, never centred (audits M31, H14).                                */
/* MEASURED ON INK (design.md §4, §6). The row is the title's line box: it is
   pulled up by the air over its first ink and gives back the air under its
   last (--words-top, --words-foot, base.css), so the words stand the
   section's own gap under the block above and --gap-head over the content.
   The see-all link keeps its 44px target but its air falls outside the row
   (below), so a row with a link and a row without are one height. From
   1024px the rail arrows' 44px frames are the row's first and last ink, and
   nothing is pulled. The product page's related heading stood 30.8px from
   its words to its cards at 390 against home's 20 (verify-home 1 and 4). */
.sechead{
  display:flex; align-items:center; flex-wrap:wrap;
  gap:var(--s-2) var(--s-4);
  margin-top:calc(var(--t-h2) * var(--words-top) * -1);
  margin-bottom:calc(var(--gap-head) - var(--t-h2) * var(--words-foot));
}
.sechead > .sechead__all{ margin-block:calc((var(--t-sm) * var(--lh-ui) - 44px) / 2) }
@media (min-width:1024px){
  .sechead:has(> .rail-nav){ margin-block:0 var(--gap-head) }
}
.sechead__t{
  min-width:0; flex:0 1 auto;
  margin-inline-end:auto;
  font-size:var(--t-h2); line-height:var(--lh-h2);
  font-weight:700; letter-spacing:var(--ls-heading); color:var(--ink);
  /* Georgian compounds are long and a two line section title that leaves one
     word alone on line two reads as a mistake. Balance evens the lines; where
     it is unsupported the heading simply wraps as before.                   */
  text-wrap:balance;
}
/* A rail's prev and next pair sits between the title and the link. */
.sechead .rail-nav{ margin-inline-start:auto }
.sechead__all{
  flex:none;
  display:inline-flex; align-items:center; gap:var(--s-2);
  min-height:44px; padding-inline:var(--s-3);
  /* The negative margins cancel the link's own 12px of inline padding so the
     LABEL, not the hit area, sits on the gutter. Both ends, not just the end:
     the row wraps on a phone, and a wrapped link starts its own line, where a
     cancelled end margin does nothing and an uncancelled start padding
     indents the words 12px past every other word in the column. Where the row
     does not wrap the start margin costs nothing, because the link is placed
     from its end edge by .sechead__t's auto margin.                         */
  margin-inline:calc(var(--s-3) * -1);
  border-radius:var(--r-sm);
  font-size:var(--t-sm); line-height:var(--lh-ui); font-weight:600; color:var(--ink-2);
  transition:color var(--d-hover) var(--ease), background-color var(--d-hover) var(--ease);
}
/* Hover paints the label itself, so it is red as TEXT and takes --red-700:
   on the --page-2 wash that hover lays down, --red is 4.1:1 and --red-700
   is 5.4:1. Press already used --red-800 (6.7:1) and is left alone.        */
@media (hover:hover){
  .sechead__all:hover{ color:var(--red-700); background:var(--page-2) }
}
.sechead__all:active{ color:var(--red-800) }
.sechead__all .icon{ transition:transform var(--d-hover) var(--ease) }
@media (hover:hover){
  .sechead__all:hover .icon{ transform:translateX(var(--s-1)) }
}
/* The link beside the title instead of at the far edge of the row. On a
   full width band the auto margin on .sechead__t pairs "see all" with the
   right hand end of the section it opens, which is where a shopper who has
   just finished scanning that section is looking. Inside a panel whose top
   row holds nothing else, the same rule strands the link a thousand pixels
   from the words it refers to, so the panel gets this variant: heading,
   then link, both at the left edge where the reading starts. The label
   keeps its own 12px of padding and only the START margin is dropped, so
   the two sit a clear 28px apart rather than the 4px the cancelled margin
   would leave.                                                             */
.sechead--lead{ justify-content:flex-start }
.sechead--lead .sechead__t{ margin-inline-end:0 }
.sechead--lead .sechead__all{ margin-inline-start:0 }

/* on the dark surface */
.sechead--on-dark .sechead__t{ color:var(--on-fill) }
.sechead--on-dark .sechead__all{ color:var(--on-dark-2) }
@media (hover:hover){
  .sechead--on-dark .sechead__all:hover{ color:var(--on-fill); background:var(--scrim) }
}


/* ── 6. FORM CONTROLS ──────────────────────────────────────────────────────
   Label always above the field, never a placeholder substitute.
   16px text everywhere (iOS zooms below that). 44px mobile / 40px desktop.
   Errors are border + icon + text, wired with aria-invalid + aria-describedby.
   ---------------------------------------------------------------------- */
/* Fieldset and legend, reset once and shared. A <fieldset> ships with a 2px
   groove border, asymmetric padding and a `min-width:min-content` that no
   author style can override on the element itself, which is why a fieldset
   wrapped around a flex or grid row refuses to shrink below its widest
   child and pushes a 390px form sideways. `min-width:0` is the load-bearing
   line. A <legend> brings its own inline padding and a float-like flow.
   Element selectors on purpose: three pages already carry .ckfs, .revform__f
   and .auth__cellsw over the top of this, and a class here would outrank
   them for no reason.                                                      */
fieldset{ margin:0; padding:0; border:0; min-width:0 }
legend{ padding:0; display:block; width:100% }

.field{ display:flex; flex-direction:column; gap:var(--s-2); min-width:0 }
.field__label{
  font-size:var(--t-sm); line-height:var(--lh-sm);
  font-weight:500; color:var(--ink-2);
}
/* the asterisk is a word, not a fill. --red is 4.6:1 on --paper, --red-700
   is 6.2:1, and this one is 14px so it gets no large-text allowance.       */
.field__label .req{ color:var(--red-700) }

/* The resting border is --bd-3, not --bd-2. A field's boundary is the only
   thing that says where the field is, and WCAG 1.4.11 asks a control boundary
   for 3:1 against what it sits on. --bd-2 gave 1.44:1 on --paper, which is a
   suggestion of an edge rather than an edge. --bd-3 is 3.37:1 on the field's
   own white fill and 3.15:1 on the page around it, so the box is findable at
   rest, before anyone hovers or focuses anything. See base.css.            */
.input,.textarea,.select{
  width:100%; min-height:44px;
  /* 8px, not 10: on the 4px ladder, and the box does not change size,
     because 8 + 20.8 (16px at --lh-ui) + 8 + 2 of border is 38.8 and
     min-height is still what decides. 12px would push the field past 44px
     and break the one row height every form on the site is built on.     */
  padding:var(--s-2) var(--s-3);
  background:var(--paper); color:var(--ink);
  /* --bw-1 of --bd-3: the old one and a half pixels rendered as 1px (C7) */
  border:var(--bw-1) solid var(--bd-3);
  border-radius:var(--r-sm);
  /* 16px and never smaller: iOS zooms the whole page when a field under
     16px takes focus, and the zoom does not come back on blur.            */
  font-family:var(--f-ui); font-size:var(--t-body); line-height:var(--lh-ui);
  transition:border-color var(--d-hover) var(--ease),
             background-color var(--d-hover) var(--ease);
}
/* the compact field is a mouse's field, same as .btn--sm above */
@media (min-width:1024px) and (hover:hover) and (pointer:fine){
  .input,.select{ min-height:40px }
}
.textarea{ min-height:112px; resize:vertical; line-height:var(--lh-sm) }

.input::placeholder,.textarea::placeholder{ color:var(--ink-3); opacity:1 }
@media (hover:hover){
  .input:hover:not(:disabled),
  .textarea:hover:not(:disabled),
  .select:hover:not(:disabled){ border-color:var(--ink-3) }
}
/* Focus turns the edge --focus and draws the ring. A TEXT FIELD SHOWS THE
   RING ON EVERY FOCUS, a tap included: Chrome matches :focus-visible on a
   text input for any focus, and the system takes the platform's behaviour
   (design.md §6, audit C27), because a field being typed into should say so.
   Only a select tells a pointer focus from a keyboard one. The reset is
   scoped `:not(:focus-visible)` rather than written as a bare `outline:none`
   a later rule puts back, so the ring is never removed from a keyboard. */
.input:focus,.textarea:focus,.select:focus{ border-color:var(--focus) }
.input:focus:not(:focus-visible),
.textarea:focus:not(:focus-visible),
.select:focus:not(:focus-visible){ outline:none }
.input:focus-visible,.textarea:focus-visible,.select:focus-visible{
  outline:var(--ring) solid var(--focus); outline-offset:var(--ring-off); border-color:var(--focus);
}
.input:disabled,.textarea:disabled,.select:disabled{
  opacity:var(--o-disabled); cursor:not-allowed; background:var(--page-2);
}
.input[readonly]{ background:var(--page-2); color:var(--ink-2) }

/* The chevron is a drawn sprite, not a UA triangle: appearance:none takes
   the native arrow away on every engine and this puts back one that matches
   assets/img/icons.svg (24 viewBox, 2px round caps and joins, the same path
   as #i-chevron-down). It is a data URI rather than a <use>, because a
   <select> has no room for a child element and no ::after of its own.
   THE ONE COST is that the stroke colour is baked in: a data URI cannot
   read var(--ink-2), so %2355566B is that token spelled out and it must be
   moved by hand if --ink-2 ever moves. It is a flat stroke on transparent:
   no gradient, no fill, nothing that could become one.
   It is also drawn twice, disabled in --ink-3 (%2366677A, the value --ink-3
   moved to in design.md §2), because a chevron that keeps its full weight on
   a greyed field says the field can still be opened.                       */
.select{
  appearance:none; -webkit-appearance:none;
  padding-inline-end:var(--s-10);
  cursor:pointer;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2355566B' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9.5 12 15.5 18 9.5'/%3E%3C/svg%3E");
  background-repeat:no-repeat;
  background-position:right var(--s-3) center;
  background-size:18px 18px;
}
.select:disabled{
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2366677A' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9.5 12 15.5 18 9.5'/%3E%3C/svg%3E");
}

/* Error state: never colour alone. The border AND the fill move together,
   and neither of them is the message: .field__err below carries the words
   and an icon, wired with aria-invalid + aria-describedby. The trigger is
   aria-invalid="true" and nothing else, so a field cannot look wrong to a
   sighted reader while reading as fine to a screen reader.                 */
.input[aria-invalid="true"],
.textarea[aria-invalid="true"],
.select[aria-invalid="true"]{ border-color:var(--err); background:var(--err-bg) }
@media (hover:hover){
  .input[aria-invalid="true"]:hover,
  .textarea[aria-invalid="true"]:hover,
  .select[aria-invalid="true"]:hover{ border-color:var(--err) }
}

.field__hint{ font-size:var(--t-sm); line-height:var(--lh-sm); color:var(--ink-3) }
/* The message is a row with an icon slot in front of it: <svg class="icon
   icon--16 alert"> then the words. `align-items:flex-start` plus a nudge
   holds the icon on the FIRST line when the sentence wraps, which on a
   390px phone a Georgian error sentence always does; centred, it drifted
   to the middle of a two line message and stopped reading as a marker.
   The icon is 16px against 14px text (design.md §5) and flex:none so a long
   sentence cannot squeeze it.                                              */
.field__err{
  display:flex; align-items:flex-start; gap:var(--s-2);
  font-size:var(--t-sm); line-height:var(--lh-sm);
  font-weight:500; color:var(--err);
}
/* the nudge is the half-leading of the line it sits on, computed, not
   guessed: (21.7px line box - 16px icon) / 2 */
.field__err .icon{
  color:var(--err); flex:none;
  margin-top:calc((var(--lh-sm) * 1em - 16px) / 2);
}

/* input with a leading icon (search fields etc.) */
.input-wrap{ position:relative; display:flex; align-items:center; min-width:0 }
.input-wrap>.icon{
  position:absolute; inset-inline-start:var(--s-3);
  color:var(--ink-3); pointer-events:none;
}
.input-wrap .input{ padding-inline-start:var(--s-10) }

/* checkbox / radio: 44px label row, 24px control.
   The control is 24px (never below the 24×24 hard floor even when measured on
   its own); the whole 44px label row is the real tap target.               */
.opt{
  display:flex; align-items:center; gap:var(--s-3);
  min-height:44px; min-width:0;
  cursor:pointer; font-size:var(--t-body); color:var(--ink);
}
@media (hover:hover){
  .opt:hover{ color:var(--slate-800) }
}
.opt input[type="checkbox"],
.opt input[type="radio"]{
  appearance:none; -webkit-appearance:none;
  flex:none; width:var(--s-6); height:var(--s-6); margin:0;
  background:var(--paper);
  /* a checkbox IS an input, and its box is its whole affordance: --bd-3 */
  border:var(--bw-1) solid var(--bd-3); border-radius:var(--r-xs);
  cursor:pointer;
  transition:background-color var(--d-press) var(--ease-out),
             border-color     var(--d-press) var(--ease-out);
}
.opt input[type="radio"]{ border-radius:50% }
@media (hover:hover){
  .opt:hover input:not(:disabled){ border-color:var(--slate-800) }
}
.opt input:checked{ background-color:var(--slate-800); border-color:var(--slate-800) }
/* The tick is a data URI, so its stroke is --on-fill spelled out (%23ffffff,
   design.md §2): it moves by hand if --on-fill ever does. */
.opt input[type="checkbox"]:checked{
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12.5 10 17.5 19 7'/%3E%3C/svg%3E");
  background-size:16px 16px; background-position:center; background-repeat:no-repeat;
}
.opt input[type="radio"]:checked{ box-shadow:inset 0 0 0 var(--s-1) var(--paper) }
/* A checkbox can be the thing that is invalid: the consent box in checkout
   and in the register dialog are both required, and both are reported with
   aria-invalid on the input itself. It takes the same --err border and
   --err-bg fill as every other field, so one look means one thing across
   the whole site. The message is still .field__err beside it.              */
.opt input[aria-invalid="true"]{ border-color:var(--err); background-color:var(--err-bg) }
@media (hover:hover){
  .opt:hover input[aria-invalid="true"]:not(:disabled){ border-color:var(--err) }
}
.opt input:disabled{ cursor:not-allowed }
/* The row fades as one thing, and the label drops to --ink on the way down
   rather than to --ink-3: --ink-3 at .6 over --page composites to 2.28:1,
   --ink to 4.41:1. A disabled option is exempt from AA and still has to be
   readable, because the shopper has to know WHICH option is unavailable.   */
.opt:has(input:disabled){ opacity:var(--o-disabled); cursor:not-allowed; color:var(--ink) }
/* the facet counts stack in a column and every one of them changes when a
   filter moves, so they are tabular */
.opt__count{
  margin-inline-start:auto;
  font-family:var(--f-disp); font-size:var(--t-sm); color:var(--ink-3);
  font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
}

/* Secondary text under an option's label: what a payment method actually is
   („Visa · Mastercard · American Express"), what a delivery choice actually
   costs. The row is the same .opt; the sub takes a full line of its own
   (flex-basis:100%) and is indented past the control so it aligns with the
   label, not with the box. The row grows beyond 44px instead of clipping.
   Wrapping is switched on only where a sub exists, so a plain filter row,
   label + .opt__count, keeps its single-line behaviour untouched.
   --ink-2 on --paper is 7.17:1; the sub is real copy, not a whisper.        */
.opt:has(.opt__sub){ flex-wrap:wrap; row-gap:var(--s-1); padding-block:var(--s-2) }
/* The label keeps the control's line and wraps INSIDE itself. `flex-basis:0`
   is the load-bearing part: at `auto` a long Georgian label („განვადება,
   საქართველოს ბანკი") is measured at its full unwrapped width, overflows the
   row, and flex pushes the whole label onto line 2, where it starts at the
   container edge and loses its alignment with the sub beneath it. At 0 it can
   never overflow, so it stays put and breaks lines normally.
   Both spellings work: name the slot .opt__lb, or let the bare <span> that is
   already there be caught by the second selector. */
.opt__lb,
.opt:has(.opt__sub) > span:not(.opt__sub):not(.opt__count){ flex:1 1 0; min-width:0 }
.opt__sub{
  flex:1 1 100%;
  margin-inline-start:calc(var(--s-6) + var(--s-3));   /* control + row gap */
  font-size:var(--t-sm); line-height:var(--lh-sm);
  color:var(--ink-2);
}
.opt:has(input:disabled) .opt__sub{ color:inherit }
/* NOT DONE HERE, on purpose: reserving vertical space for .field__err so a
   validation message cannot shift the form. Wave 1 asked for it; the checkout
   block handled it in cart.css instead, where the field set is known and the
   grid is its own. A global min-height on every .field would tax every form
   on the site that never validates (search, promo, quantity, newsletter)
   for the benefit of one. Recorded so the omission reads as a decision. */

/* quantity stepper */
.qty{
  display:inline-flex; align-items:center; flex:none;
  background:var(--paper);
  border:var(--bw-1) solid var(--bd-3); border-radius:var(--r-sm);
  overflow:hidden;
}
.qty__btn{
  display:grid; place-items:center;
  width:44px; height:44px; flex:none;
  color:var(--ink-2);
  transition:background-color var(--d-press) var(--ease-out), color var(--d-press) var(--ease-out);
}
@media (hover:hover){
  .qty__btn:hover{ background:var(--page-2); color:var(--ink) }
}
.qty__btn:active{ background:var(--bd) }
/* .55 and --ink, the measured floor the buttons use: at .35 the ± glyph was
   1.9:1 on --paper and a shopper at the stock ceiling could not see which
   half of the stepper had stopped working.                                 */
.qty__btn:disabled{
  opacity:var(--o-disabled); cursor:not-allowed; background:transparent; color:var(--ink);
}
@media (hover:hover){
  .qty__btn:disabled:hover{ background:transparent; color:var(--ink) }
}
/* Always type="text" + inputmode="numeric": guide 21 §21.13 forbids
   type="number" for quantity (scroll-wheel changes, no leading zeros, a
   spinner that duplicates the ± buttons). The spin-button resets that used
   to live here only applied to type="number" and are deliberately gone, so
   nobody reads them as licence to switch. */
.qty__in{
  width:var(--s-12); height:44px; flex:none;
  border:0; background:transparent; text-align:center;
  font-family:var(--f-disp); font-weight:700; font-size:var(--t-body);
  /* the one number on the page that ticks under the shopper's eye */
  font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
}
.qty__in:focus-visible{ outline:var(--ring) solid var(--focus); outline-offset:var(--ring-in) }
/* same loading state as the button above: dim, busy cursor, no second press */
.qty[data-loading]{ opacity:var(--o-busy); cursor:progress; pointer-events:none }


/* ── 7. HOTLINE: the help panel ──────────────────────────────────────────
   A plain --red panel carrying white text (4.6:1) and the number at
   --t-num-xl. THIS IS THE ONLY RED-FILLED PANEL ON THE SITE.
   It used to be drawn as a telephone box: a domed top edge, a 3×6 grid of
   white glazing bars at 12%, a crown hairline at 20%. Both of its reasons
   are gone. The bars were tuned to a much deeper red. Over --red they
   composite to #D35A63, a 1.19:1 delta, so the grid could not be seen at
   all. And the red now comes out of the client's own logo rather than out
   of a phone box, which makes the motif illustration used as decoration
   (design.md §1). What is left is the panel.
   ---------------------------------------------------------------------- */
.hotline{
  display:flex; flex-direction:column; gap:var(--s-2);
  padding:var(--s-6) var(--s-5) var(--s-5);
  background:var(--red); color:var(--on-fill);
  border-radius:var(--r);
  box-shadow:var(--sh-2);
}
.hotline__label{ font-size:var(--t-sm); font-weight:500; color:var(--on-fill) }
.hotline__num{
  display:inline-flex; align-items:center; gap:var(--s-2);
  min-height:44px;                    /* it is a tel: link, full touch target */
  font-family:var(--f-disp); font-weight:700; letter-spacing:var(--ls-heading);
  font-size:var(--t-num-xl); line-height:var(--lh-num); color:var(--on-fill);
  white-space:nowrap;                 /* a phone number is one unbroken string */
  border-radius:var(--r-xs);
  transition:background-color var(--d-hover) var(--ease);
}
/* A control on a fill it does not own: the one white wash, as the toast's
   controls do (§2 state table), never a fade of the number itself. */
@media (hover:hover){
  .hotline__num:hover{ background:var(--scrim); text-decoration:underline; text-underline-offset:3px }
}
.hotline__num:active{ background:var(--scrim) }
.hotline__hours{ font-size:var(--t-sm); color:var(--on-fill); font-weight:400 }
.hotline :focus-visible{ outline-color:var(--on-fill) }


/* ── 8. BREADCRUMBS: station-dot separators ──────────────────────────── */
/* The label role (design.md §3: crumbs are labels, 500) for every crumb:
   from 768px the trail's links were 400 beside the current crumb and the
   phone's back link at 500 (review S12). */
.crumbs{ font-size:var(--t-sm); font-weight:500; min-width:0 }
.crumbs ol{ display:flex; align-items:center; flex-wrap:wrap; gap:0 var(--s-2); min-width:0 }
.crumbs li{ display:flex; align-items:center; gap:var(--s-2); min-width:0 }
.crumbs li + li::before{
  content:""; flex:none;
  width:var(--dot); height:var(--dot); border-radius:50%;
  background:var(--c);
}
.crumbs a,
.crumbs span[aria-current]{
  display:inline-flex; align-items:center;
  min-height:44px; min-width:0;
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
/* A crumb link's target is at least 44px wide however short its name
   („Home" was 38px at 768, the shared translator's request): --s-1 of air
   on each side, given back by its margins so the words stay where they
   were, and a 44px floor for a name shorter than that. */
.crumbs a{
  min-width:44px;
  padding-inline:var(--s-1); margin-inline:calc(-1 * var(--s-1));
  color:var(--ink-2);
  transition:color var(--d-hover) var(--ease);
}
@media (hover:hover){
  .crumbs a:hover{ color:var(--ink-strong); text-decoration:underline; text-underline-offset:3px }
}
.crumbs [aria-current="page"]{ color:var(--ink); font-weight:500 }


/* ── 9. PAGINATION ───────────────────────────────────────────────────────
   Real links, 44px at every viewport including the desktop one: a page
   number is the smallest target on the catalog and there is no width at
   which shrinking it pays. The current page is marked by FILL, not by
   colour alone, and catalog.js puts aria-current="page" on the same stop,
   so the mark and the announcement are the same fact.

   The gap between the number and the icon is here so previous and next can
   be an icon PLUS the word: „წინა" and „შემდეგი" read as controls at a
   glance where a bare chevron has to be inferred, and the row wraps rather
   than crowding at 320px. catalog.js emits
   `<a class="pager__i">{icon}<span>წინა</span></a>` for both ends.

   The DOM order is icon first for both, which is right for „წინა" and
   backwards for „შემდეგი": every other forward control on the site puts its
   arrow after the words („ყველა ახალი →", the opening's delivery link, the
   mega-panel's see-all), and only the back controls lead with the chevron.
   The last stop in the list is the next link whenever there is one, so the
   row below flips that one item visually without touching the markup or the
   reading order. `:has(.icon)` is the guard: when the shopper is on the last
   page the final stop is a plain number and nothing flips.

   BELOW 480px, ONE COMPACT ROW that never wraps (design.md §6): previous
   (chevron and word), „გვერდი 5 / 80" (.pager__of, tabular, --ink-2), next.
   The numbered run and its gaps hide and every control stays 44px; at 320px
   the full run broke into two rows and split the numbers from their next
   link (audits C10, K34). The markup contract: an `li.pager__of` between
   previous and next, holding the page sentence. Without it the row still
   collapses to previous and next.                                          */
.pager li:last-child .pager__i:has(> .icon){ flex-direction:row-reverse }
.pager{
  display:flex; align-items:center; justify-content:center;
  flex-wrap:wrap; gap:var(--s-2);
}
.pager__i{
  display:inline-flex; align-items:center; justify-content:center; gap:var(--s-1);
  min-width:44px; height:44px; padding-inline:var(--s-3);
  background:var(--paper); color:var(--ink-2);
  /* the quiet control's edge: --bd was 1.23:1, weaker than every other
     control (audit C10) */
  border:var(--bw-1) solid var(--bd-2); border-radius:var(--r-sm);
  font-family:var(--f-disp); font-size:var(--t-sm); font-weight:600; line-height:var(--lh-ui);
  /* Google Sans digits are proportional by default (design.md 3). A pager
     is a row of numbers in boxes of one width, and 1 next to 42 next to 11
     wanders without this.                                                  */
  font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
  transition:background-color var(--d-hover) var(--ease),
             border-color     var(--d-hover) var(--ease),
             color            var(--d-hover) var(--ease);
}
.pager__i .icon{ width:18px; height:18px; --sw:2 }
@media (hover:hover){
  .pager__i:hover{ background:var(--paper-2); border-color:var(--ink-strong); color:var(--ink-strong) }
  .pager__i[aria-current="page"]:hover{ background:var(--slate-700); border-color:var(--slate-700); color:var(--on-fill) }
}
.pager__i:active{ background:var(--page-2) }
.pager__i[aria-current="page"]{ background:var(--slate-800); border-color:var(--slate-800); color:var(--on-fill) }
.pager__i[aria-current="page"]:active{ background:var(--slate-900); border-color:var(--slate-900) }
/* catalog.js drops previous and next at the ends rather than greying them
   (FLOWS flow 3), so this is a state the pager mostly does not use. It
   ships anyway, at the measured --o-disabled and on --ink, because a page
   that does grey a stop must not grey it into 1.9:1.                       */
.pager__i:disabled,
.pager__i[aria-disabled="true"]{
  opacity:var(--o-disabled); cursor:not-allowed; background:var(--paper); color:var(--ink);
}
@media (hover:hover){
  .pager__i:disabled:hover,
  .pager__i[aria-disabled="true"]:hover{ background:var(--paper); border-color:var(--bd-2); color:var(--ink) }
}
.pager__gap{
  display:inline-flex; align-items:flex-end; justify-content:center;
  min-width:var(--s-6); height:44px; padding-bottom:var(--s-3);
  color:var(--ink-3);
}
/* The page sentence is inline text in a block, never a flex row: as flex
   items its words and figures lost the spaces between them and read
   „გვერდი1/240" in both languages (verify-catalog). It is a flex item of
   the pager, whose align-items centres it on the 44px row. */
.pager__of{
  display:none;
  padding-inline:var(--s-2);
  font-size:var(--t-sm); line-height:var(--lh-ui); color:var(--ink-2);
  font-variant-numeric:tabular-nums; font-feature-settings:"tnum" 1;
  white-space:nowrap;
}
@media (max-width:479.98px){
  .pager{ flex-wrap:nowrap }
  .pager li:not(:has(.icon)):not(.pager__of){ display:none }
  .pager__of{ display:block }
}


/* ── 10. TOAST: the live region ───────────────────────────────────────────
   Host carries role="status"; toasts are appended into it by core.js.

   WHERE IT SITS: --s-4 above --chrome-bottom (design.md §4, §14.13), the
   sum the chrome contract already keeps of whatever band is pinned to the
   bottom edge: the bottom bar, or the product call bar a page registers in
   its place as --pin-bottom. It is --s-6 from 768px, where no bottom bar is
   left. The toast host is --z-toast: over every bar and the filter sheet,
   under the drawer.

   --toast-lift retires with this. It is still read, as a transition only:
   product.css adds it while its call bar is on, and until that page
   registers the bar as --pin-bottom (its PR2) the toast would otherwise sit
   under the bar. A page that registers its bar must drop the lift.
   ---------------------------------------------------------------------- */
.toast-host{
  position:fixed; z-index:var(--z-toast);
  left:50%; transform:translateX(-50%);
  bottom:calc(var(--s-4) + var(--chrome-bottom) + var(--toast-lift, 0px));
  display:flex; flex-direction:column; align-items:center; gap:var(--s-2);
  width:min(420px, calc(100% - var(--s-8)));
  pointer-events:none;
}
@media (min-width:768px){
  .toast-host{
    bottom:calc(var(--s-6) + var(--chrome-bottom) + var(--toast-lift, 0px));
  }
}
/* A toast never stands over the control that raised it (core.js
   placeToasts, design.md §6 "Toast"): when the band above the bottom bar
   holds that control (the heart just tapped at the foot of a 320x640
   screen, and its neighbour, review B5), the host stands --s-4 under the
   pinned chrome at the top instead, and its toast arrives from above. */
.toast-host.toast-host--top{
  top:calc(var(--chrome-top) + var(--s-4) + env(safe-area-inset-top));
  bottom:auto;
}
.toast-host--top .toast:not(.is-on){ transform:translateY(-10px) }

.toast{
  display:flex; align-items:center; gap:var(--s-3);
  width:100%;
  padding:var(--s-3) var(--s-4);
  background:var(--slate-800); color:var(--on-fill);
  border-radius:var(--r-sm);
  box-shadow:var(--sh-3);
  font-size:var(--t-sm); line-height:var(--lh-small-ka);
  pointer-events:auto;
  opacity:0; transform:translateY(10px);
  transition:opacity var(--d-toast) var(--ease), transform var(--d-toast) var(--ease);
}
.toast.is-on{ opacity:1; transform:none }
/* The dot hangs in front of the message and stays with its first line. As
   a flex item of its own, a message that named a product wrapped away from
   it and left the dot alone on the toast's first line. */
.toast__t{
  position:relative;
  padding-inline-start:calc(var(--dot-lg) + var(--s-3));
}
.toast__t::before{
  content:""; position:absolute; inset-inline-start:0;
  top:calc((var(--lh-small-ka) * 1em - var(--dot-lg)) / 2);
  width:var(--dot-lg); height:var(--dot-lg); border-radius:var(--r-pill);
  background:var(--on-fill);
}
.toast--err{ background:var(--err) }
.toast--ok .toast__t::before{ background:var(--ok-2) }

/* action toast: LS.toast(msg, {action:{label, href|onClick}}).
   The control is a real 44px target on the dark surface: --on-fill on --slate-800
   is 11.01:1. The row wraps, so on a narrow phone the action drops under the
   message instead of squeezing it; .toast__t keeps the message shrinkable so
   a long product name wraps rather than pushing the control off the edge.
   core.js gives this variant a longer life (6.5s) and freezes it while it is
   hovered or focused: a control nobody can reach is not a control.          */
.toast--act{ align-items:center; flex-wrap:wrap; row-gap:var(--s-1) }
.toast__t{ flex:1 1 auto; min-width:0 }
.toast__act{
  flex:none; margin-inline-start:auto;
  display:inline-flex; align-items:center;
  min-height:44px; padding-inline:var(--s-3);
  border-radius:var(--r-xs);
  font-size:var(--t-sm); line-height:var(--lh-ui); font-weight:600; color:var(--on-fill);
  text-decoration:underline; text-underline-offset:3px;
  transition:background-color var(--d-hover) var(--ease),
             transform var(--d-press) var(--ease-press);
}
/* Press is the same wash plus the scale(.97) every other control on the
   site uses, rather than a second, heavier alpha: one scrim value, one weight. */
@media (hover:hover){
  .toast__act:hover{ background:var(--scrim) }
}
.toast__act:active{ background:var(--scrim); transform:scale(.97) }

/* The close control. A toast on a phone can cover the thing the shopper was
   reaching for, and until it times out there is nothing to do about it: a
   tap on the body of a toast does nothing, which is correct, and leaves no
   way out. This is the way out, and it is a real 44px square rather than a
   12px glyph, because it sits at the edge of a box that is itself at the
   edge of the screen. core.js appends it last inside every toast it raises:
     <button class="toast__x" type="button" aria-label="დახურვა">
       <svg class="icon icon--16" aria-hidden="true"><use href="#i-close"/></svg>
     </button>
   IT IS TAKEN OUT OF THE FLEX FLOW, and that is the whole trick. As a flex
   item it wrapped: the message is `flex:1 1 auto`, so its hypothetical size
   is its full text, the first flex line is spent on it, and a 44px square
   after it drops to line two, where on an action toast it landed half a
   line off the control beside it. Absolute at the top inline end, the
   dismiss is in the corner a dismiss belongs in, at every width and on
   every variant, and the message and the action can wrap around each other
   exactly as design.md §6 says they should. The end padding on the toast is
   what keeps the words out from under it; without a close control the toast
   keeps its ordinary 16px and loses no measure.                            */
.toast{ position:relative }
.toast:has(.toast__x){ padding-inline-end:44px }
.toast__x{
  position:absolute; inset-block-start:0; inset-inline-end:0;
  display:grid; place-items:center;
  width:44px; height:44px;
  border-radius:var(--r-xs);
  color:var(--on-fill);
  transition:background-color var(--d-hover) var(--ease),
             transform var(--d-press) var(--ease-press);
}
@media (hover:hover){
  .toast__x:hover{ background:var(--scrim) }
}
.toast__x:active{ background:var(--scrim); transform:scale(.97) }
/* --focus is 2.53:1 on --slate-800 and fails; --focus-2 is 6.57:1 (design.md
   §2). Every focusable thing inside a toast sits on that dark fill.         */
.toast :focus-visible{ outline-color:var(--focus-2) }


/* ── 11. SKELETON: loading product grids ─────────────────────────────────
   A flat block, and nothing else. It used to sweep a white
   linear-gradient across itself on a 1.4s loop. Two rules killed it: the
   gradient is banned outright, and RULES rule 9 caps every animation at
   300ms with no exemption for a loop, so a shimmer inside the budget would
   have to cycle faster than four times a second, which is a flicker rather
   than a hint. The block already says what it needs to say. What tells the
   shopper the page is working is the shape of the grid appearing at the
   right size, which is also what keeps CLS at zero, and that is the part
   worth spending on.
   ---------------------------------------------------------------------- */
.skeleton{
  background:var(--page-2);
  border-radius:var(--r-xs);
}
/* A bar is a block, so a line inside a paragraph (the aisle line) has a
   height and a width of its own. */
.skel-line{ display:block; height:14px; border-radius:var(--r-xs) }
.skel-line--xs{ width:40% } .skel-line--sm{ width:62% }
.skel-line--md{ width:88% } .skel-line--lg{ width:100% }

/* The product card skeleton IS the card (core.js §7): the same .card__media,
   .card__body, .card__cat, .card__title and .card__price, so it sits inside
   the same container queries and cannot end up a different height from the
   card it stands in for. */
.skel-card{ pointer-events:none }
/* the whole square is the placeholder: the plain --page-2 well of a photo
   still loading (§15.3), so skeleton and loading card are one look */
.skel-card .card__media{ background:var(--page-2); padding:0 }
/* The aisle bar sits in a line box of the real line's height: an empty
   inline box has no line box at all, and the skeleton came out 20px short. */
.skel-card .card__cat{
  display:flex; align-items:center;
  min-height:calc(var(--t-small-ka) * var(--lh-small-ka));
}
/* Two bars in the box .card__title already reserves for two lines. The real
   title is a -webkit-box clamped to 2; here the box is a flex column and the
   bars sit at the top and the bottom of it, which is where the two lines of
   text they stand for sit. The box's own min-height holds the price row in
   place, exactly as it does on a real card.                                */
.skel-card .card__title{
  display:flex; flex-direction:column; justify-content:space-between;
  padding-block:var(--s-1);
}
/* The price bar is a real .price line box: the &nbsp; inside it gives it the
   height the number has. The heart keeps its 44px slot, empty. */
.skel-card .skel-price{ width:72px; border-radius:var(--r-xs) }
.skel-card .skel-heart{ flex:none; width:44px; height:44px; margin-inline-start:auto }


/* ── 12. EMPTY STATE ───────────────────────────────────────────────────────
   One component for every state that has nothing to list or could not load
   (design.md §6 "Empty state"): zero results, the empty wishlist, the empty
   aisle, a load error, a section error on home, the search layer's no
   match, and as a whole page the 404 and the product that was not found. It
   was one idea built four times, with two marks, three widths and three
   heading levels (audits C11, L29, D36). LS.empty() in core.js builds it.

   · No card chrome: no fill, no border, no radius. A card around it nested
     it inside the page's own frame and left 245px of text at 320px (K15).
   · Start-aligned, on the content edge of its region; the sentence at
     --measure-short. Centred copy ran to five lines on a phone (TC17).
   · The mark: a 64px --page-2 circle holding the state's own sprite icon at
     28px (--sw 1.75, --ink-2): heart (empty wishlist), search (no results,
     the 404), alert (a load error), tag (a product not found), grid (an
     empty aisle). It replaces the transit roundel (I8).
   · The title: the panel title role at the level the outline needs; on a
     status page (.empty--page) it is the page's h1 at --t-h1, and the
     sentence under it is the page's lede.
   · Actions, the primary first: below 480px each full width, --s-3 apart;
     from 480px side by side, wrapping.
   · Suggestions: a label in the label role, then a wrapping row of
     suggestion chips (.chip--sug), every name whole.
   · --s-4 between the parts and --s-10 above the block inside a region.
     Inside a rail the block spans the whole rail (it was squeezed into one
     card slot, C12).
   ---------------------------------------------------------------------- */
.empty{
  display:flex; flex-direction:column; align-items:flex-start;
  gap:var(--s-4);
  margin-top:var(--s-10);
  min-width:0;
  text-align:start;
}
.empty--page{ margin-top:0 }
.empty__mark{
  display:grid; place-items:center; flex:none;
  width:var(--s-16); height:var(--s-16);
  border-radius:var(--r-pill);
  background:var(--page-2); color:var(--ink-2);
}
.empty__mark .icon{ width:28px; height:28px; --sw:1.75 }
.empty__t{
  font-size:var(--t-h3); line-height:var(--lh-h3); font-weight:700; color:var(--ink);
  letter-spacing:var(--ls-heading);
  text-wrap:balance;
}
.empty--page .empty__t{ font-size:var(--t-h1); line-height:var(--lh-h1) }
.empty__d{
  max-width:var(--measure-short);
  font-size:var(--t-body); line-height:var(--lh-body); color:var(--ink-2);
  text-wrap:pretty;
}
/* On a status page the sentence is the page's lede (design.md §14.6,
   §14.12): the lede role (16px, 18px from 768), at most --measure wide,
   --gap-head-in under the h1 and --gap-head-out over the way on. The
   column's own gap is --s-4, so the margins give back or add the rest. It
   was body copy 16px under the h1 at every width (verify-product V9). */
.empty--page .empty__d{
  max-width:var(--measure);
  margin-top:calc(var(--gap-head-in) - var(--s-4));
  margin-bottom:calc(var(--gap-head-out) - var(--s-4));
  font-size:var(--t-lede); line-height:var(--lh-lede);
}
.empty__actions{
  display:flex; flex-direction:column; align-items:stretch; gap:var(--s-3);
  width:100%;
}
/* a whole Georgian sentence on a button wraps inside it rather than
   leaving the column (a 320px column is 288px) */
.empty__actions .btn{
  height:auto; min-height:44px; padding-block:var(--s-2);
  white-space:normal;
}
@media (min-width:480px){
  .empty__actions{ flex-direction:row; flex-wrap:wrap; align-items:center; width:auto }
}
.empty__sugs{ display:flex; flex-direction:column; gap:var(--s-2); width:100% }
/* the label role in its group-label form: --ink-2 at 600 (§3) */
.empty__sugl{
  font-size:var(--t-small-ka); line-height:var(--lh-ui); font-weight:600;
  color:var(--ink-2);
}
.empty__chips{ display:flex; flex-wrap:wrap; gap:var(--s-2); min-width:0 }
.empty__chips > li{ display:flex; max-width:100%; min-width:0 }
/* a section's error inside a rail spans the rail */
.rail > .empty{ flex:1 1 100%; margin-top:0 }


/* ── 13. RAIL: horizontal scroll-snap row ────────────────────────────────
   One direction of movement per section: the rail scrolls, it never wraps.
   design.md §14.4: 72% of the row per card below 480, 46% to 767, 3.3 in
   view from 768 to 1023 (so the fourth card always shows its edge: three
   whole cards and no arrows hid 9 of 12 new arrivals, audit L20), 4 with
   arrows from 1024 and 5 from 1280.
   ---------------------------------------------------------------------- */
.rail-wrap{ position:relative; min-width:0 }
.rail{
  display:flex; gap:var(--s-4);
  overflow-x:auto; overscroll-behavior-x:contain;
  scroll-snap-type:x mandatory;
  /* bleed to the screen edge on mobile so cards don't look boxed in */
  margin-inline:calc(var(--gut) * -1);
  padding-inline:var(--gut);
  scroll-padding-inline:var(--gut);
  /* Room inside the scroller, which clips: a focus ring's reach above
     (--ring plus --ring-off, 5px) and 32px below for the hover shadow's
     fade. With 4px the ring lost its top row (S-3); with 12px --sh-2 ended
     in a hard line 14px under the lifted card, ten levels of red in one
     pixel row, where a grid card's fades out (S-9): at 32px what is left
     of it is one level. The negative margin gives it all back, so the rail
     adds nothing to the rhythm around it (§4 "Rhythm"): the gap under a
     rail is the section's own. Not more above: a see-all link that wraps
     onto its own line keeps 12.9px of target air under its label and the
     rail's box starts --gap-head less this under the row, so 8px took 3px
     of the link's 44px (the product page at 320). A card both hovered and
     focused lifts 2px and shows its ring's top edge once the pointer
     leaves. */
  --rail-ring:calc(var(--ring) + var(--ring-off));
  padding-block:var(--rail-ring) var(--s-8);
  margin-block:calc(var(--rail-ring) * -1) calc(var(--s-8) * -1);
  scrollbar-width:none; -ms-overflow-style:none;
}
/* The bleed is a PHONE rule and it stops being one at 768px. On a phone the
   gutter is the only thing between the row and the screen edge, so running
   the scroller through it is what stops the cards looking boxed in. On a
   desktop the shell is centred inside a much wider window, and the same
   rule puts the half card that is scrolled out of view OUTSIDE the column
   every other section on the page lines up with: a sliver of a photograph
   floating in the left margin beside the heading, clipped by the window on
   the right and by nothing on the left. From 768px the row starts and ends
   on the container's own edges, like the grids under it.
   It keeps the reach of a focus ring (--ring plus --ring-off, 5px) on each
   side, given back by the margin: at 0 the first card's ring lost its whole
   inline start side and the last card's its end side (S-3). The cards at
   rest still start and end on the container's edges; a card scrolled past
   an edge shows 5px of itself in the gutter, not the gutter's 24 or 32. */
@media (min-width:768px){
  .rail{
    margin-inline:calc(var(--rail-ring) * -1);
    padding-inline:var(--rail-ring);
    scroll-padding-inline:var(--rail-ring);
  }
}
.rail::-webkit-scrollbar{ display:none }
.rail > *{ flex:0 0 72%; scroll-snap-align:start; min-width:0 }
@media (min-width:480px) { .rail > *{ flex-basis:46% } }
@media (min-width:768px) { .rail > *{ flex-basis:calc((100% - var(--s-4) * 3) / 3.3) } }
@media (min-width:1024px){ .rail > *{ flex-basis:calc((100% - var(--s-4) * 3) / 4) } }
@media (min-width:1280px){ .rail > *{ flex-basis:calc((100% - var(--s-4) * 4) / 5) } }

/* There is no position indicator under a rail. There was one, a 3px track
   with a fluid thumb driven by transform alone, and it is deleted, not
   hidden: on a row whose next card is already half in view, and which has
   prev/next from 1024px, a bar underneath was a third thing saying what the
   row itself and its arrows had both already said, and under a row that also
   advances by itself it read as a progress meter rather than as a scroll
   position. If it comes back, it comes back with a reason this comment does
   not contain.                                                             */
/* prev / next: desktop only, the rail is swipeable everywhere (duality) */
.rail-nav{ display:none }
@media (min-width:1024px){
  .rail-nav{ display:flex; align-items:center; gap:var(--s-2) }
}
/* The rail arrows are the quiet control family of §2's state table, with the
   pager box: a square on --r-sm with a --bw-1 --bd-2 edge. A circle is for a
   control that floats over a photograph, and these stand in the heading row
   (§6 "Icon button": two shapes and no more). */
.rail-nav__b{
  display:grid; place-items:center;
  width:44px; height:44px; flex:none;
  background:var(--paper); color:var(--ink-2);
  border:var(--bw-1) solid var(--bd-2); border-radius:var(--r-sm);
  transition:border-color var(--d-hover) var(--ease), color var(--d-hover) var(--ease),
             background-color var(--d-hover) var(--ease),
             transform var(--d-press) var(--ease-press);
}
@media (hover:hover){
  .rail-nav__b:hover{ border-color:var(--ink-strong); color:var(--ink-strong); background:var(--paper-2) }
}
.rail-nav__b:active{ background:var(--page-2); transform:scale(.97) }
/* --o-disabled and --ink: the same measured floor as every other disabled
   control in this file. A rail sits at one end of its scroll most of the
   time, so one of these two is greyed most of the time, and at .35 the
   chevron was 1.9:1 on --paper.                                            */
.rail-nav__b:disabled{
  opacity:var(--o-disabled); cursor:not-allowed; background:var(--paper); color:var(--ink); transform:none;
}
@media (hover:hover){
  .rail-nav__b:disabled:hover{ border-color:var(--bd-2); color:var(--ink); background:var(--paper) }
}


/* ── 14. DISCLOSURE (the accordion): vertical only ─────────────────────────
   Horizontal tabs are banned (guide 21 §21.2: 27% of users miss them).
   ONE DISCLOSURE, TWO STEPS (design.md §6), where the product details, the
   filter groups and the footer's phone groups were three builds with their
   own sizes and scripts (audit C22):
     full      a trigger of at least 56px, the subhead role (--t-h3, 600), a
               24px chevron: the product page's details;
     compact   .acc--compact on the list or any ancestor: 48px on touch and
               44px under a fine pointer, --t-body 600 on --lh-ui, a 20px
               chevron: the filter groups, the footer's groups on phones.
   One toggle: LS.disclosure(root) in core.js puts .is-open on .acc__item and
   aria-expanded on its trigger. Markup:
     <div class="acc__item">
       <h3 class="acc__h"><button class="acc__trigger" aria-expanded="false"
           aria-controls="p1">Name <svg class="icon acc__chev">…</svg></button></h3>
       <div class="acc__panel" id="p1"><div class="acc__inner"><div class="acc__c">…
   ---------------------------------------------------------------------- */
.accordion{ border-top:var(--bw-1) solid var(--bd) }
.acc__item{ border-bottom:var(--bw-1) solid var(--bd) }
.acc__h{ margin:0; font-size:inherit; font-weight:inherit; line-height:inherit; letter-spacing:0 }
.acc__trigger{
  display:flex; align-items:center; justify-content:space-between; gap:var(--s-4);
  width:100%; min-height:56px;
  padding-block:var(--s-3);
  font-size:var(--t-h3); line-height:var(--lh-h3); font-weight:600;
  color:var(--ink); text-align:start;
  transition:color var(--d-hover) var(--ease);
}
@media (hover:hover){
  .acc__trigger:hover{ color:var(--ink-link) }
}
.acc__trigger:active{ color:var(--slate-900) }
.acc__trigger:disabled,
.acc__trigger[aria-disabled="true"]{ opacity:var(--o-disabled); color:var(--ink); cursor:not-allowed }
.acc__chev{
  color:var(--ink-2); flex:none;
  transition:transform var(--d-ui) var(--ease-out), color var(--d-hover) var(--ease);
}
@media (hover:hover){
  .acc__trigger:hover .acc__chev{ color:var(--ink-link) }
}
.acc__trigger[aria-expanded="true"] .acc__chev{ transform:rotate(180deg) }

.acc--compact .acc__trigger{
  min-height:48px; padding-block:var(--s-2);
  font-size:var(--t-body); line-height:var(--lh-ui);
}
@media (hover:hover) and (pointer:fine){
  .acc--compact .acc__trigger{ min-height:44px }
}
.acc--compact .acc__chev{ width:20px; height:20px }

/* A COLLAPSED PANEL LEAVES THE TAB ORDER, and that is what visibility is
   doing here rather than any fade. `grid-template-rows:0fr` plus
   `overflow:hidden` on .acc__inner closes the track to nothing, but a link
   inside a zero height box is still focusable: Tab walks into a panel
   nobody can see, the page scrolls to a control that is not there, and the
   focus ring lands on a hairline. `visibility:hidden` removes the subtree
   from the tab order and from the accessibility tree in one property, and
   it is a discrete property, so it needs a delay rather than a duration:
   opening flips it at 0s so the content is present while the track grows;
   closing waits out the full --d-ui and flips at the end, so the panel is
   still visible while it shuts. Both directions stay inside the 200ms
   budget, and nothing animates that a reduced-motion reader would notice.
   THIS MAKES A LOCAL `inert` REDUNDANT: product.js sets one on its own
   collapsed panels and can stop.                                           */
.acc__panel{
  display:grid; grid-template-rows:0fr;
  visibility:hidden;
  transition:grid-template-rows var(--d-ui) var(--ease-out),
             visibility 0s linear var(--d-ui);
}
.acc__item.is-open > .acc__panel{
  grid-template-rows:1fr; visibility:visible;
  transition:grid-template-rows var(--d-ui) var(--ease-out), visibility 0s;
}
.acc__inner{ overflow:hidden; min-height:0 }
/* An OPEN panel shows the whole ring of a control on its edge. The clip is
   there for the track's animation, not for rings, and `hidden` cut the
   outside ring of a price chip in the filter column at 1440 (verify S-8).
   Open, it clips with a margin where the engine can (`hidden` stays the
   fallback), and `clip` is no scroll container, so a focused control
   cannot scroll the panel either. The margin is --s-2, not the ring's 5px
   sum: Chrome drops any calc() in overflow-clip-margin that holds a var()
   (it computed to 0), and 8px covers the ring. It arrives once the track
   has grown, as a discrete transition delayed by --d-ui (0s under reduced
   motion; Chrome does not interpolate this property, so allow-discrete is
   what makes the delay hold), and leaves at once on closing, so nothing
   shows past a moving track. */
@supports (overflow-clip-margin:1px){
  .acc__item.is-open > .acc__panel > .acc__inner{
    overflow:clip; overflow-clip-margin:var(--s-2);
    transition:overflow-clip-margin 0s linear var(--d-ui) allow-discrete;
  }
}
.acc__c{ padding-bottom:var(--s-5); color:var(--ink-2) }
.acc__c > * + *{ margin-top:var(--s-3) }
.acc--compact .acc__c{ padding-bottom:var(--s-4) }


/* ── 15. SEGMENTED CONTROL (.seg) ──────────────────────────────────────────
   Two or three halves that choose one of a set: the catalog's grid and list
   toggle, and the language pair (design.md §6). A --paper track with a
   --bw-1 --bd-3 inset edge (it stands in the toolbar beside a field),
   --r-sm, and NO `overflow:hidden`: each end half carries the track's radius
   itself, so nothing clips the ring (it showed as a sliver on the language
   toggle, audits C19, B31). Halves are 44px (40px under a fine pointer from
   1024px), the control role, --s-4 inline padding.
     unselected   --ink-2; hover --page-2 and --ink; press --bd
     selected     aria-pressed="true" (or aria-current on a link):
                  --slate-800 with --on-fill; hover --slate-700; press
                  --slate-900 (the pressed language half used to paint its
                  white label on --bd, 1.23:1, audit TC8)
     disabled     --o-disabled with an --ink label (the ENG half until
                  English is live), its reason kept in its name
     focus        the ring inside the half at --ring-in: --focus on a light
                  half, --focus-2 on the selected one (audit TC12)
   Markup: <div class="seg" role="group" aria-label="…">
             <button class="seg__b" type="button" aria-pressed="true">…</button>
             <button class="seg__b" type="button" aria-pressed="false">…</button>
           </div>
   ---------------------------------------------------------------------- */
.seg{
  display:inline-flex; align-items:stretch; flex:none;
  background:var(--paper);
  border-radius:var(--r-sm);
  box-shadow:inset 0 0 0 var(--bw-1) var(--bd-3);
}
.seg__b{
  display:inline-flex; align-items:center; justify-content:center; gap:var(--s-2);
  min-width:44px; height:44px; padding-inline:var(--s-4);
  font-size:var(--t-sm); line-height:var(--lh-ui); font-weight:600;
  color:var(--ink-2); background:transparent;
  border-radius:0;
  text-decoration:none; white-space:nowrap;
  transition:background-color var(--d-hover) var(--ease),
             color            var(--d-hover) var(--ease);
}
.seg__b:first-child{ border-start-start-radius:var(--r-sm); border-end-start-radius:var(--r-sm) }
.seg__b:last-child{ border-start-end-radius:var(--r-sm); border-end-end-radius:var(--r-sm) }
.seg__b .icon{ width:20px; height:20px; --sw:2 }
@media (min-width:1024px) and (hover:hover) and (pointer:fine){
  .seg__b{ height:40px }
}
.seg__b:focus-visible{ outline-offset:var(--ring-in) }
@media (hover:hover){
  .seg__b:hover{ background:var(--page-2); color:var(--ink) }
}
.seg__b:active{ background:var(--bd) }
.seg__b[aria-pressed="true"],
.seg__b[aria-current]:not([aria-current="false"]){ background:var(--slate-800); color:var(--on-fill) }
@media (hover:hover){
  .seg__b[aria-pressed="true"]:hover,
  .seg__b[aria-current]:not([aria-current="false"]):hover{ background:var(--slate-700); color:var(--on-fill) }
}
.seg__b[aria-pressed="true"]:active,
.seg__b[aria-current]:not([aria-current="false"]):active{ background:var(--slate-900) }
.seg__b[aria-pressed="true"]:focus-visible,
.seg__b[aria-current]:not([aria-current="false"]):focus-visible{ outline-color:var(--focus-2) }
.seg__b:disabled,
.seg__b[aria-disabled="true"]{
  opacity:var(--o-disabled); color:var(--ink); cursor:not-allowed;
}
@media (hover:hover){
  .seg__b:disabled:hover,
  .seg__b[aria-disabled="true"]:hover{ background:transparent; color:var(--ink) }
}
/* A selected half that is not a control: the language pair's current
   language is a <span aria-current="true">, said and not offered, and no
   Tab stop (review S14). It keeps the selected look and answers neither
   the pointer nor the thumb, so it never reads as something to press. */
.seg > span.seg__b{ cursor:default }
@media (hover:hover){
  .seg > span.seg__b[aria-current]:not([aria-current="false"]):hover{ background:var(--slate-800) }
}
.seg > span.seg__b[aria-current]:not([aria-current="false"]):active{ background:var(--slate-800) }


/* ── 16. OVERLAY SCRIM ─────────────────────────────────────────────────────
   The one wash under every overlay that takes the page away (design.md §6
   "Overlay primitive"): --scrim-dark, fading over --d-panel, never a blur
   and never a gradient. The drawer's color-mix and the sheet's .56 were the
   same wash spelled two other ways (audits C18, T38). The layer is the
   caller's: the shell's scrim sits on --z-scrim, a page's modal on
   --z-modal. LS.overlay() in core.js toggles `is-on` with the panel. The
   lightbox is a native <dialog> and takes --scrim-media on ::backdrop. */
.scrim{
  position:fixed; inset:0;
  background:var(--scrim-dark);
  opacity:0;
  transition:opacity var(--d-panel) var(--ease);
}
.scrim.is-on{ opacity:1 }
.scrim[hidden]{ display:none }


/* ── 17. REDUCED MOTION ──────────────────────────────────────────────────
   No @keyframes ship from this file. The one that used to, `ls-spin`, drove
   the button's loading ring on an infinite loop, which RULES rule 9 does not
   allow at any speed; the loading state is drawn, not animated (§1).
   base.css has already collapsed every duration to 0s; what is left here is
   the travel itself.
   ---------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce){
  /* Every press scale in this file, listed rather than caught with a
     wildcard, so adding a control means deciding what its press does here. */
  .btn:active,
  .iconbtn:active,
  .qty__btn:active,
  .rail-nav__b:active,
  .toast__act:active,
  .toast__x:active{ transform:none }
  /* the toast arrives and leaves by opacity alone; the 10px rise is gone,
     and so is the drop of a toast standing at the top */
  .toast,
  .toast-host--top .toast:not(.is-on){ transform:none }
  /* The chevron keeps its rotation: it is a STATE, not a motion, and with
     the duration at 0s it turns instantly. */
  .acc__trigger[aria-expanded="true"] .acc__chev{ transform:rotate(180deg) }
}
/* the hover travel: the card's lift and the see-all arrow's step */
@media (prefers-reduced-motion: reduce) and (hover:hover){
  .card:hover,
  .sechead__all:hover .icon{ transform:none }
}
