@import url('tokens.css');

/* --amber, not a hard-coded #f0b86e. Measured 1.78:1 on the light theme's
   white card, where «غير متوفر حالياً» was all but unreadable. It never shows
   on the homepage — out of stock products do not reach the front page — which
   is why a homepage contrast sweep misses it. --amber is the token for
   exactly this: the one interrupt, burnt dark on light so scarcity stays
   visible on white. */
.stock-out { display:block; color:var(--amber); font-size:.9rem; margin-block-start:.4rem; }
.add:disabled { cursor:not-allowed; opacity:.6; }

/* =========================================================================
   base
   ========================================================================= */

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

/* The UA stylesheet's [hidden] { display: none } loses to any author rule that
   sets display — so .card { display: flex } silently defeated it, and the
   product filter set `hidden` on 88 cards while every one of them stayed on
   screen. Declared once, globally, rather than patched per component. */
[hidden] { display: none !important; }

html {
  background: var(--void);
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  /* A FLAT ground. It was a three-stop vertical gradient, which the design
     review ruled out: gradients belong in the controlled hero light and the
     primary action, nowhere else.
     Flattening also makes the contrast figures true rather than approximate.
     tests/test_contrast.py measures text against --void and --lift, but with a
     gradient the actual backdrop depended on how far down the page the text
     happened to sit, so the measured number described a surface no text was
     reliably on. Now it is the surface. */
  background-color: var(--void);
  color: var(--chalk);
  font-family: var(--font-body);
  font-size: var(--fs-body);
  line-height: var(--lh-ar);
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3 {
  font-family: var(--font-display);
  font-weight: 700;
  line-height: 1.25;
  letter-spacing: -0.02em;
  margin: 0;
}

a { color: inherit; text-decoration: none; }
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }
img { max-width: 100%; display: block; }

/* Latin runs inside Arabic text: product names, part numbers, spec strings.
   Isolated so the bidi algorithm cannot drag punctuation to the wrong end. */
.ltr {
  direction: ltr;
  unicode-bidi: isolate;
  display: inline-block;
}
.mono {
  font-family: var(--font-mono);
  font-variant-ligatures: none;
  letter-spacing: -0.01em;
}

/* A clock time with an Arabic meridiem — "12:00 م" — is mixed-direction, so it
   is isolated rather than forced LTR like .ltr does. Isolation also stops the
   en-dash between two of them from reordering the ends of the range. */
.hours__t {
  unicode-bidi: isolate;
  font-family: var(--font-mono);
  font-variant-ligatures: none;
  white-space: nowrap;
}
/* One day-group per line. The footer column is narrow enough that a single
   joined line broke mid-range — a dangling "–" on one line and the closing
   time alone on the next, reading as two unrelated times. */
.hours__rule { display: block; }
/* The open–close pair travels as one unit, so the only break point left is
   between rules, where it carries meaning. */
.hours__span { white-space: nowrap; }

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

.sr-only {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}

.wrap {
  max-inline-size: var(--max);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

/* =========================================================================
   the signature: LED light strip
   A 2px emitted line, brightest at the inline start, falling off toward the
   end. In RTL "to left" puts the bright end on the right — the start.
   ========================================================================= */

.led {
  --led: var(--lilac);
  position: relative;
  block-size: 2px;
  inline-size: 100%;
  background: linear-gradient(to left, var(--led), transparent 85%);
  border-radius: 2px;
}
.led::after {           /* the bloom the strip casts on the surface below */
  content: '';
  position: absolute;
  inset-block-start: 0;
  inset-inline: 0;
  block-size: 26px;
  background: linear-gradient(to left, var(--led), transparent 70%);
  opacity: 0.13;
  filter: blur(7px);
  pointer-events: none;
}
[dir="ltr"] .led,
[dir="ltr"] .led::after { background-image: linear-gradient(to right, var(--led), transparent 85%); }

.led--own   { --led: var(--own); }

/* =========================================================================
   header
   ========================================================================= */

.head {
  position: sticky;
  inset-block-start: 0;
  z-index: 40;
  background: color-mix(in srgb, var(--void) 88%, transparent);
  backdrop-filter: blur(14px);
  border-block-end: 1px solid var(--hair2);
}

.head__bar {
  display: flex;
  align-items: center;
  gap: clamp(0.75rem, 2vw, 2rem);
  block-size: 68px;
}

.brandmark { display: flex; align-items: center; gap: 0.6rem; flex: none; }
.brandmark img { block-size: 38px; inline-size: auto; }
.brandmark__text {
  font-family: var(--font-mono);
  font-weight: 600;
  font-size: 1.05rem;
  letter-spacing: 0.14em;
  color: var(--chalk);
  direction: ltr;
}
.brandmark__text b { color: var(--lilac); font-weight: 600; }

/* On a phone the wrapper is not there at all as far as layout is concerned:
   its children go back to being direct children of the bar, which is what the
   phone header's rules were written against. */
.head__mid { display: contents; }

.nav {
  display: flex;
  gap: 1.4rem;
  margin-inline-start: auto;
  font-size: var(--fs-small);
  color: var(--dim);
}
/* Phone-only; the media query below turns it on. */
.menubtn {
  display: none;
  place-items: center;
  inline-size: 40px; block-size: 40px;
  border: 1px solid var(--hair2);
  border-radius: var(--radius);
  color: var(--chalk);
  background: var(--lift);
}
.menubtn[aria-expanded="true"] { background: var(--lift2); border-color: var(--lilac); }

.nav a { transition: color var(--fast) var(--ease); }
.nav a:hover, .nav a[aria-current="page"] { color: var(--chalk); }

/* A lit rule that draws itself under the link you are on or reaching for.
   Scoped to the bar: in the phone panel the links are stacked rows with their
   own separators, and a second line under each would just be noise. */
@media (min-width: 861px) {
  .nav a { position: relative; padding-block: 6px; }
  .nav a::after {
    content: '';
    position: absolute;
    inset-inline: 0;
    inset-block-end: 0;
    block-size: 2px;
    border-radius: 2px;
    background: var(--led-hot);
    box-shadow: 0 0 10px 1px color-mix(in srgb, var(--led-hot) 60%, transparent);
    transform: scaleX(0);
    transition: transform 0.28s var(--ease);
  }
  .nav a:hover::after,
  .nav a:focus-visible::after,
  .nav a[aria-current="page"]::after { transform: scaleX(1); }
  /* Reaching for a link lifts it a hair; the rule arrives underneath. */
  .nav a { transition: color var(--fast) var(--ease), translate var(--fast) var(--ease); }
  .nav a:hover { translate: 0 -1px; }
}

@media (prefers-reduced-motion: reduce) {
  .nav a::after { transition: none; }
  .nav a:hover { translate: none; }
}

/* The magnifier that stands in for the field until it is asked for. Phone
   only shows it never: down there the tab bar at the bottom of the screen is
   the way in, and two search affordances on one screen is one too many. */
.searchbtn {
  display: none;
  place-items: center;
  flex: none;
  inline-size: var(--tap);
  block-size: var(--tap);
  border: 1px solid var(--hair2);
  border-radius: var(--radius);
  background: var(--lift);
  color: var(--chalk);
  cursor: pointer;
  transition: border-color var(--fast) var(--ease), background var(--fast) var(--ease);
}
.searchbtn:hover { border-color: var(--lilac); background: var(--lift2); }
.searchbtn:focus-visible { outline: 2px solid var(--lilac); outline-offset: 3px; }
@media (min-width: 861px) {
  .searchbtn { display: inline-grid; }
}

/* THE MIDDLE OF THE BAR, DEAD CENTRE.
   Centred on the bar itself, not on the gap between the logo and the
   controls — those two sides are different widths, so anything relying on
   auto margins would sit visibly off. Absolute, so neither side can push it:
   the logo keeps the inline start, the switch and the request list the
   inline end.

   940px, not the 861px the rest of the desktop header starts at. Absolute
   centring takes the group out of the flow, so nothing stops it from growing
   over the controls — measured at 861px, the links crossed into the theme
   switch by 12px. True centring needs the wider of the two sides' width
   available on BOTH sides, and below ~890px the bar does not have it. Under
   that the wrapper stays `display: contents` and the links sit in the flow
   exactly as they did before, pushed toward the end by the nav's auto
   margin: off-centre, but nothing on top of anything else. */
@media (min-width: 940px) {
  .head__bar { position: relative; }
  .head__mid {
    position: absolute;
    left: 50%;
    transform: translateX(-50%);
    display: flex;
    align-items: center;
    gap: 1.4rem;
    max-inline-size: min(60%, 46rem);
  }
  .nav { margin-inline-start: 0; }
  /* Which leaves the flow to the logo and, pushed to the far end, the pair
     that closes the bar. */
  .themesw { margin-inline-start: auto; }
}

@media (min-width: 861px) {
  /* Open on a desktop, the field takes the room the links were using and the
     links fold into the menu button — which is otherwise phone-only. The bar
     is not wide enough to hold both a real search field and five nav items
     without one of them being a token gesture, and the field is the thing
     that was asked for. */
  body.is-searching .head__bar {
    flex-wrap: wrap;
    block-size: auto;
    min-block-size: 68px;
    padding-block-end: var(--s1);
  }

  /* Back into the flow while the field is open, and this is not a detail.
     The folded links open as a row of the bar itself — `flex-basis: 100%`
     below — which only works if they are a child of the bar. Left inside the
     centred group they measured 272px wide with a transparent background,
     hanging 45px below the header and over the hero: a menu with nothing
     behind it. In the flow the bar wraps, grows, and the header's own surface
     carries the panel, which is what it was built to do.

     In the flow the field then does what this block always meant it to: it
     grows into the room the links were using, 582px of it at 1280, sitting
     a little off true centre. Closed — which is the state the links are
     actually read in — the group is still absolutely centred to the pixel. */
  body.is-searching .head__mid { display: contents; position: static; }
  /* The switch holds the end of the bar by an auto margin while the group is
     centred; in the flow the field's own growth does that, and the margin
     would only fight it for the same free space. */
  body.is-searching .themesw { margin-inline-start: 0; }
  /* A definite width, not `flex: 1`. The group that holds the field is
     absolutely placed so it can sit dead centre, which makes it shrink to fit
     its contents — and a flex child told to grow inside a box that is only as
     wide as its contents does not grow. Measured: the field opened at 203px,
     a token box. Given a size of its own it is a real field again, and the
     group still centres on it. */
  body.is-searching .search {
    max-inline-size: none;
    inline-size: min(32rem, 32vw);
  }
  body.is-searching .menubtn { display: inline-grid; }
  body.is-searching .nav {
    order: 9;
    flex-basis: 100%;
    display: none;
    flex-direction: column;
    gap: 0;
    margin-inline-start: 0;
    padding-block: 0.4rem 0.8rem;
  }
  body.is-searching .nav[data-open="true"] { display: flex; }
  body.is-searching .nav a {
    padding-block: 0.7rem;
    border-block-start: 1px solid var(--hair);
    font-size: var(--fs-body);
  }
  /* The category map hangs off the header's bottom edge, which just moved. */
  body.is-searching .nav__map { align-self: flex-start; }
}
body.is-searching .searchbtn { border-color: var(--lilac); background: var(--lift2); }

.search {
  flex: 1 1 auto;
  min-inline-size: 0;
  max-inline-size: 340px;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem 0.85rem;
  background: var(--lift);
  border: 1px solid var(--hair2);
  border-radius: var(--radius);
  transition: border-color var(--fast) var(--ease);
}
.search:focus-within { border-color: var(--lilac); }
.search input {
  flex: 1; min-inline-size: 0;
  background: none; border: 0; outline: none;
  color: var(--chalk); font: inherit; font-size: var(--fs-small);
}
.search input::placeholder { color: var(--dim); }
.search svg { flex: none; color: var(--faint); }

/* Closed by default, at every width — AFTER the block above, which sets
   display:flex on .search itself. Declared before it, this lost on source
   order and the field stayed on screen with the button beside it. */
.search { display: none; }
body.is-searching .search { display: flex; }

/* ---- opening and closing ------------------------------------------------
   display cannot be transitioned, and the closed state has to be display:none
   or a zero-width flex item still claims the bar's gap. So the motion is a
   keyframe that runs as the field appears, and a matching one on the way out
   that the script holds the element open for. Two names rather than one
   reversed, because an animation played backwards on a `both` fill leaves the
   element sitting on its from-state. */
@keyframes lu-search-in  { from { opacity: 0; transform: translateY(-6px); } to { opacity: 1; transform: none; } }
@keyframes lu-search-out {   to { opacity: 0; transform: translateY(-6px); } }
body.is-searching .search { animation: lu-search-in 0.3s var(--ease) both; }
body.is-search-closing .search { animation: lu-search-out 0.22s var(--ease) both; }
/* The input carries the caret, so it arrives a beat after its own box. */
body.is-searching .search input { animation: lu-search-in 0.34s var(--ease) 0.06s both; }

@media (min-width: 861px) {
  /* On the bar it grows sideways out of the button rather than dropping,
     because nothing moved vertically to justify a drop. */
  @keyframes lu-search-in-x  { from { opacity: 0; transform: scaleX(0.9); } to { opacity: 1; transform: none; } }
  @keyframes lu-search-out-x {   to { opacity: 0; transform: scaleX(0.9); } }
  body.is-searching .search { animation-name: lu-search-in-x; }
  body.is-search-closing .search { animation-name: lu-search-out-x; }
}

@media (prefers-reduced-motion: reduce) {
  body.is-searching .search,
  body.is-searching .search input,
  body.is-search-closing .search { animation: none; }
}

/* ---- theme switch -------------------------------------------------------
   A real switch, not an icon that swaps: the track shows both states at once
   so the control says which mode you are in, not merely which one you would
   get. `checked` means dark, matching the default, so the knob starts on the
   moon and slides to the sun. */
/* Secondary prominence, per the review: appearance is a preference somebody
   sets once, and it was sitting in the header at the same visual weight as
   search and the request list, which are what people came for. Smaller, on a
   hairline rather than a lit panel, and it only picks up the accent on hover
   or focus. Its hit area is padded back up to --tap so demoting it visually
   does not make it harder to press. */
.themesw {
  position: relative;
  flex: none;
  inline-size: 52px;
  block-size: 28px;
  padding: 0;
  border: 1px solid var(--hair);
  border-radius: 999px;
  background: transparent;
  cursor: pointer;
  transition: background var(--fast) var(--ease), border-color var(--fast) var(--ease);
}
/* Padding cannot grow the switch without moving the knob, so the target is
   extended past the painted control instead. */
.themesw::after {
  content: '';
  position: absolute;
  inset: 50% 0 0 0;
  transform: translateY(-50%);
  block-size: var(--tap);
}
.themesw:hover { border-color: var(--lilac); background: var(--lift2); }
.themesw:focus-visible { outline: 2px solid var(--lilac); outline-offset: 3px; }

.themesw__knob {
  position: absolute;
  inset-block-start: 2px;
  inset-inline-start: 2px;
  inline-size: 24px;
  block-size: 24px;
  border-radius: 50%;
  background: var(--core);
  display: grid;
  place-items: center;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35);
  /* inset-inline-start, not translateX, so the knob travels toward the sun end
     in RTL and LTR alike without a direction-specific keyframe. */
  transition: inset-inline-start var(--drawer-out) var(--ease),
              background var(--fast) var(--ease);
}
.themesw__knob svg {
  grid-area: 1 / 1;
  inline-size: 14px;
  block-size: 14px;
  fill: none;
  stroke: #fff;
  stroke-width: 2;
  stroke-linecap: round;
  transition: opacity var(--fast) var(--ease), transform var(--drawer-out) var(--ease);
}
.themesw__sun  { opacity: 0; transform: rotate(-80deg) scale(.5); }
.themesw__moon { opacity: 1; }

/* aria-checked drives the visual state — the attribute the button already needs
   for assistive tech, so there is no second source of truth to fall out of sync. */
/* 24px, not 34. The track is 52 wide and the knob 24, and the knob rests 2px
   in — so the far end is 52 - 24 - 2, less the borders. At 34 it hung 7px past
   the track's edge in the light state, measured. The 560px block below does
   the same arithmetic for the smaller switch and already had it right. */
.themesw[aria-checked="false"] .themesw__knob { inset-inline-start: 24px; background: var(--amber); }
.themesw[aria-checked="false"] .themesw__sun  { opacity: 1; transform: none; }
.themesw[aria-checked="false"] .themesw__moon { opacity: 0; transform: rotate(80deg) scale(.5); }
:root[data-theme="light"] .themesw__knob svg { stroke: #1A1024; }

@media (prefers-reduced-motion: reduce) {
  .themesw__knob, .themesw__knob svg { transition: none; }
}
/* Under 720px the header is already tight; the switch keeps its hit area but
   the request button's label is what gives way first (see below). */
@media (max-width: 560px) {
  .themesw { inline-size: 54px; block-size: 28px; }
  .themesw__knob { inline-size: 22px; block-size: 22px; }
  .themesw[aria-checked="false"] .themesw__knob { inset-inline-start: 28px; }
}

.reqbtn {
  position: relative;
  flex: none;
  display: flex; align-items: center; gap: 0.5rem;
  padding: 0.5rem 0.9rem;
  border: 1px solid var(--hair2);
  border-radius: var(--radius);
  font-size: var(--fs-small);
  transition: border-color var(--fast) var(--ease), background var(--fast) var(--ease);
}
.reqbtn:hover { border-color: var(--lilac); background: var(--wash); }
.reqbtn__count {
  min-inline-size: 20px;
  padding-inline: 5px;
  text-align: center;
  background: var(--core);
  color: #fff;
  border-radius: 10px;
  font-family: var(--font-mono);
  font-size: var(--fs-micro);
  line-height: 20px;
}
.reqbtn__count[data-empty="true"] { display: none; }

/* delivery strip — the only hard number on the whole site. STATIC.
   It was a marquee. A claim like this is read at a glance or not at all, and
   on the homepage it ran at the same time as the hero carousel, so two
   independent motions competed above the fold before the reader had chosen to
   look at anything. One sentence, sitting still. */
/* delivery strip — the only hard number on the whole site, now a ticker.
   The moving row is aria-hidden and repeated to fill the width; .ship__still
   carries the sentence once for assistive tech and is the ONLY thing shown
   under prefers-reduced-motion. */
.ship {
  position: relative;
  border-block-end: 1px solid var(--hair2);
  background: var(--lift);
  overflow: hidden;
  -webkit-mask: linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent);
          mask: linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent);
}
/* Present for screen readers, off-screen for eyes. Not display:none — that
   would take it out of the accessibility tree along with the pixels, and the
   visible track is aria-hidden, so the delivery terms would vanish entirely. */
.ship__still {
  position: absolute;
  inline-size: 1px; block-size: 1px;
  margin: -1px; padding: 0; border: 0;
  clip-path: inset(50%);
  overflow: hidden; white-space: nowrap;
}
.ship__track {
  display: flex;
  align-items: center;
  inline-size: max-content;
  padding-block: 0.5rem;
  font-size: var(--fs-small);
  color: var(--dim);
  animation: lu-marquee 60s linear infinite;
}
/* Same sign rule as the brand marquee: an RTL max-content track already extends
   left of the window, so it has to travel RIGHT to bring the duplicate in. */
[dir="rtl"] .ship__track { animation-name: lu-marquee-rtl; }
.ship:hover .ship__track, .ship:focus-within .ship__track { animation-play-state: paused; }
.ship__item {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  white-space: nowrap;
}
.ship__item svg { flex: none; color: var(--lilac); }
.ship__sep {
  color: var(--faint);
  font-size: 0.55em;
  padding-inline: 1.4rem;
}
.ship b { color: var(--chalk); font-weight: 600; }

@media (prefers-reduced-motion: reduce) {
  .ship__track { display: none; }
  .ship__still {
    position: static;
    inline-size: auto; block-size: auto;
    margin: 0; clip-path: none;
    padding-block: 0.5rem;
    font-size: var(--fs-small);
    color: var(--dim);
    text-align: center;
  }
}
@keyframes lu-marquee     { from { transform: translateX(0); } to { transform: translateX(-50%); } }
@keyframes lu-marquee-rtl { from { transform: translateX(0); } to { transform: translateX(50%); } }

/* =========================================================================
   buttons
   ========================================================================= */

.btn {
  /* isolation:isolate is load-bearing: the ::before fill sits at z-index:-1 and
     without a new stacking context it paints over the page rather than under
     the label. */
  position: relative;
  isolation: isolate;
  overflow: hidden;
  display: inline-flex;
  white-space: nowrap;
  flex-shrink: 0;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  padding: 0.72rem 1.4rem;
  border-radius: 999px;
  border: 1px solid var(--hair2);
  font-size: var(--fs-small);
  /* 600, not 500. Alexandria carries the whole site now, and every weight it
     is asked for is another ~60KB across the arabic and latin subsets — more
     than dropping a whole second family saved. This rule and .price--ask were
     the only two asking for 500, so moving both to 600 retires the weight
     entirely. Done here rather than by removing 500 from the font URL: CSS
     font matching resolves a missing 500 DOWNWARD first, so the buttons would
     have landed on 400 and read weaker, not heavier. */
  font-weight: 600;
  color: var(--chalk);
  transition: color var(--fast) var(--ease), border-color var(--fast) var(--ease);
}
.btn::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  transform: scaleX(0);
  transform-origin: 100% 50%;
  background: linear-gradient(90deg, var(--indigo), var(--core));
  transition: transform var(--drawer-out) var(--ease);
}
.btn:hover::before { transform: scaleX(1); }
.btn:hover { border-color: transparent; color: #fff; }
[dir="ltr"] .btn::before { transform-origin: 0 50%; }
.btn:active { transform: translateY(1px); }
.btn--primary {
  background: linear-gradient(90deg, var(--indigo), var(--core));
  border-color: transparent;
  color: #fff;
  font-weight: 600;
  box-shadow: 0 8px 26px -10px var(--glow);
}
.btn--primary::before { display: none; }   /* already filled */
.btn--primary:hover { filter: brightness(1.08); }
.btn--ghost { border-color: var(--hair2); color: var(--chalk); }

@media (prefers-reduced-motion: reduce) {
  .btn, .btn::before { transition: none; }
}
.btn--wa { background: var(--whatsapp); color: #04220F; inline-size: 100%; }
.btn--wa:hover { filter: brightness(1.08); }
.btn[disabled] { opacity: 0.45; cursor: not-allowed; }
.btn[disabled]:active { transform: none; }

/* =========================================================================
   section heads
   ========================================================================= */

.section { padding-block: var(--section); }
.section__head { margin-block-end: 2rem; }
.section__head .led { margin-block-end: 1.1rem; max-inline-size: 220px; }
.section__title { font-size: var(--fs-h1); }
/* Body font, not mono. This is a full Arabic sentence, and mono was the wrong
   instinct: even with Arabic reaching a real face through the fallback chain,
   the SPACE still came from Plex Mono's fixed advance, so the words sat too
   far apart to read as a sentence. Prose gets the prose face. */
.section__meta {
  font-family: var(--font-body);
  font-size: var(--fs-small);
  color: var(--dim);
  margin-block-start: 0.35rem;
}
@media (min-width: 760px) { }
.cat > .led { position: absolute; inset-block-start: 0; inset-inline: 0; }

/* =========================================================================
   product cards
   ========================================================================= */

.grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));
  gap: 1rem;
}
/* A 210px minimum leaves room for only one column on a phone, which is a very
   long scroll through 92 items. Two columns is what a catalog this size wants. */
@media (max-width: 560px) {
  .grid { grid-template-columns: repeat(2, 1fr); gap: 0.6rem; }
  .card__name { font-size: var(--fs-micro); }
}

/* ---- product tile -------------------------------------------------------
   Photograph first, data second. A gradient surface plus an almost-invisible
   border is what gives depth; the previous flat panel with a 1px grey box was
   half of why the page read as "a pale grid". */
.card {
  position: relative;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  border-radius: var(--radius-lg);
  border: 1px solid var(--hair);
  /* Solid raised charcoal, not a gradient surface — same rule as the page. */
  background: var(--lift);
  transition: transform var(--drawer-out) var(--ease),
              border-color var(--fast) var(--ease),
              box-shadow var(--drawer-out) var(--ease);
}
.card:hover { border-color: color-mix(in srgb, var(--lilac) 45%, var(--hair2)); transform: translateY(-2px); }
.card[data-avail="out"] .card__name { color: var(--dim); }
.card[data-avail="out"]:hover { transform: none; }

/* Placeholder: names the exact photo the shop still owes us, rather than
   hiding the gap behind a grey box. */
.ph {
  inline-size: 100%; block-size: 100%;
  display: grid; place-content: center; gap: 0.45rem;
  text-align: center;
  background-image:
    linear-gradient(var(--hair) 1px, transparent 1px),
    linear-gradient(90deg, var(--hair) 1px, transparent 1px);
  background-size: 22px 22px;
}
.ph__icon { color: var(--faint); opacity: 0.55; justify-self: center; }

.card__body { padding: 0.85rem 0.9rem 0.9rem; display: flex; flex-direction: column; flex: 1; gap: 0.35rem; }
.card__name {
  font-size: var(--fs-small);
  line-height: 1.45;
  color: var(--chalk);
  direction: ltr;
  text-align: start;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  min-block-size: 2.6em;
}
.card__foot {
  margin-block-start: auto;
  padding-block-start: 0.7rem;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
}

/* =========================================================================
   availability — occupies the slot where a price would normally sit
   ========================================================================= */

/* Two shapes share the .add hook: a 30px icon button on a card, and a full
   labelled button in the hero and on product pages. The icon sizing must not
   reach the labelled one — .add and .btn have equal specificity, so without
   :not(.btn) the fixed 30px width wins by source order and the label spills
   out of a 30px box. */
.add:not(.btn) {
  flex: none;
  inline-size: 30px; block-size: 30px;
  display: grid; place-items: center;
  border: 1px solid var(--hair2);
  border-radius: var(--radius);
  color: var(--dim);
  line-height: 1;
  transition: all var(--fast) var(--ease);
}
.add:not(.btn):hover { border-color: var(--lilac); background: var(--core); color: #fff; }
.add:not(.btn)[data-in="true"] { border-color: var(--lilac); background: var(--core); color: #fff; }
.add[disabled] { opacity: 0.35; cursor: not-allowed; }
.add:not(.btn)[disabled]:hover { border-color: var(--hair2); background: none; color: var(--dim); }

/* horizontal rail */
.rail {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(200px, 232px);
  gap: 1rem;
  overflow-x: auto;
  padding-block-end: 0.75rem;
  scroll-snap-type: inline mandatory;
  scrollbar-width: thin;
  scrollbar-color: var(--hair2) transparent;
}
.rail > * { scroll-snap-align: start; }

/* =========================================================================
   filters
   ========================================================================= */

.pill {
  padding: 0.45rem 0.95rem;
  border: 1px solid var(--hair2);
  border-radius: 999px;
  font-size: var(--fs-small);
  color: var(--dim);
  transition: all var(--fast) var(--ease);
}
.pill:hover { color: var(--chalk); border-color: var(--faint); }
.pill[aria-pressed="true"] {
  color: var(--chalk);
  border-color: var(--lilac);
  background: var(--wash);
  box-shadow: 0 0 0 1px var(--wash), 0 0 14px var(--wash);
}

.result {
  font-family: var(--font-mono);
  font-size: var(--fs-small);
  color: var(--dim);
  margin-block-end: 1.25rem;
}

.empty {
  padding-block: 4rem;
  text-align: center;
  color: var(--dim);
}
.empty h3 { font-size: var(--fs-h2); color: var(--chalk); margin-block-end: 0.6rem; }

/* =========================================================================
   product page
   ========================================================================= */

.pd { display: grid; gap: clamp(1.5rem, 4vw, 3.5rem); padding-block: 2.5rem var(--section); }
/* The photo column. One child now that the description has moved across to
   the information column, but min-width:0 still earns its place: without it a
   wide image refuses to shrink below its intrinsic width and the grid track
   pushes the page sideways. */
.pd__visual { display: grid; min-width: 0; }
.pd__info { min-width: 0; }
@media (min-width: 900px) { .pd { grid-template-columns: 1.05fr 1fr; align-items: start; } }

/* Breadcrumbs carry Arabic category names ("قطع الكمبيوتر"), on all 92 product
   pages. Same reason as .section__meta: prose face for prose. */
.crumb {
  font-family: var(--font-body);
  font-size: var(--fs-micro);
  color: var(--dim);
  margin-block-end: 1rem;
}
.crumb a:hover { color: var(--dim); }

.pd__name {
  font-size: var(--fs-h1);
  line-height: 1.25;
  direction: ltr;
  text-align: start;
  font-family: var(--font-body);
  font-weight: 600;
  margin-block-end: 0.6rem;
}

/* the price slot */
.pd__slot {
  margin-block: 1.75rem;
  padding: 1.1rem 1.25rem;
  border: 1px solid var(--hair2);
  border-radius: var(--radius);
  background: var(--lift);
}
.pd__gone { display: block; font-weight: 600; color: var(--dim); }
.pd__ask { display: block; margin-block-start: 0.5rem; font-size: var(--fs-small); color: var(--dim); }

.pd__actions { display: flex; flex-wrap: wrap; gap: 0.7rem; margin-block-end: 1.75rem; }
.pd__actions .btn { flex: 1 1 190px; }

/* The shop's own words about a product. Only rendered when it has written
   some — see build_product — so this never styles an empty box. */
.pd__desc {
  display: grid;
  gap: 16px;
  padding: clamp(20px, 2.5vw, 32px);
  border: 1px solid var(--hair2);
  border-radius: var(--radius-lg);
  background: var(--lift);
  min-width: 0;
}
.pd__desc h2 {
  font-family: var(--font-body);
  font-size: clamp(22px, 2.2vw, 30px);
  color: var(--chalk); font-weight: 600;
  margin: 0; letter-spacing: 0; line-height: 1.5;
}
.pd__desc h2::before {
  content: ''; display: block; width: 48px; height: 2px;
  background: var(--lilac); margin-block-end: 12px;
}
.pd__desc-body { min-width: 0; max-width: 72ch; }
.pd__desc p {
  margin: 0 0 1rem; color: var(--chalk);
  font-size: clamp(17px, 1.4vw, 19px); line-height: 1.95;
  letter-spacing: 0; overflow-wrap: anywhere; unicode-bidi: plaintext;
}
.pd__desc p:last-child { margin-block-end: 0; }
.pd__desc-body { border-block-start: 1px solid var(--hair2); padding-block-start: 16px; }

/* Folded under the product name. The HEIGHT is animated rather than the text
   swapped, so opening reads as the panel growing and not as the page jumping
   under the reader. Both the clamp and the full height are measured in JS and
   arrive as custom properties — the stylesheet cannot count lines of Arabic.

   is-clamped is added by JS only when the words actually overrun, so a short
   description is never folded and never grows a button. */
.pd__desc-body {
  overflow: hidden;
  transition: max-height 0.45s cubic-bezier(0.22, 0.61, 0.36, 1);
}
.pd__desc.is-clamped .pd__desc-body {
  max-height: var(--desc-cap, 7.5em);
  /* The last line fades out instead of being guillotined, which is how a
     reader knows there is more without being told. */
  -webkit-mask-image: linear-gradient(to bottom, #000 62%, transparent 100%);
  mask-image: linear-gradient(to bottom, #000 62%, transparent 100%);
}
.pd__desc.is-open .pd__desc-body { max-height: var(--desc-full); }
.pd__desc-more {
  display: inline-flex; align-items: center; gap: 6px;
  justify-self: start;
  min-height: 44px; padding: 0 2px;
  border: 0; background: none; cursor: pointer;
  color: var(--lilac); font: inherit; font-weight: 600;
}
.pd__desc-more[hidden] { display: none; }
.pd__desc-more svg { transition: transform 0.3s var(--ease, ease); }
.pd__desc.is-open .pd__desc-more svg { transform: rotate(180deg); }
.pd__desc-more:focus-visible { outline: 3px solid var(--lilac); outline-offset: 3px; border-radius: 6px; }
@media (prefers-reduced-motion: reduce) {
  .pd__desc-body, .pd__desc-more svg { transition: none; }
}

.pd__ship { border: 1px solid var(--hair2); border-radius: var(--radius); overflow: hidden; }
.pd__ship div {
  display: flex; justify-content: space-between; gap: 1rem;
  padding: 0.7rem 1rem;
  font-size: var(--fs-small);
}
.pd__ship div + div { border-block-start: 1px solid var(--hair2); }
.pd__ship dt { color: var(--dim); margin: 0; }
.pd__ship dd { margin: 0; color: var(--chalk); font-weight: 600; }
.pd__ship .free { color: var(--free); }

/* =========================================================================
   request list drawer — not a cart
   ========================================================================= */

.scrim {
  position: fixed; inset: 0; z-index: 50;
  background: rgba(0, 0, 0, 0.65);
  backdrop-filter: blur(3px);
  opacity: 0; visibility: hidden;
  transition: opacity var(--drawer-out) var(--ease), visibility var(--drawer-out);
}
.scrim[data-open="true"] { opacity: 1; visibility: visible; transition-duration: var(--drawer-in); }

.drawer {
  position: fixed;
  inset-block: 0;
  inset-inline-end: 0;
  z-index: 51;
  inline-size: min(430px, 100%);
  display: flex;
  flex-direction: column;
  background: var(--lift);
  border-inline-start: 1px solid var(--hair2);
  transform: translateX(100%);
  transition: transform var(--drawer-out) var(--ease);
  visibility: hidden;
}
[dir="rtl"] .drawer { transform: translateX(-100%); }
.drawer[data-open="true"] {
  transform: translateX(0);
  visibility: visible;
  transition: transform var(--drawer-in) var(--ease-out);
}
/* The largest moving thing on the site: on a phone this is the whole viewport
   travelling across, which is the case prefers-reduced-motion exists for.
   Every other element here that moves — the hero cards, the switch knob, the
   search field, the nav underline — already had an escape and this one did
   not. It still opens and closes; it just arrives instead of sliding.

   The scrim below keeps its fade deliberately: opacity is not motion, and
   without it the drawer would appear over a black rectangle that snapped in. */
@media (prefers-reduced-motion: reduce) {
  .drawer,
  .drawer[data-open="true"] { transition: none; }
}

.drawer__head {
  /* Four things on one 375px row: title, count, empty, close. Nothing in it
     may wrap — a two-line header steals from the list below it, which is the
     whole point of the panel. */
  display: flex; align-items: center; gap: 0.5rem;
  padding: 1.1rem 1.25rem;
  border-block-end: 1px solid var(--hair2);
}
.drawer__head h2 { font-size: var(--fs-h2); white-space: nowrap; }
.drawer__n { font-family: var(--font-mono); font-size: var(--fs-small); color: var(--dim); margin-inline-start: auto; white-space: nowrap; }
/* 44px, like the clear button beside it. It was 21x37 — the smallest target
   in the drawer, and the one a reader reaches for with a thumb while holding
   the phone one-handed. The glyph is unchanged; only the box around it grew. */
.drawer__x {
  color: var(--dim);
  display: inline-flex; align-items: center; justify-content: center;
  inline-size: 2.75rem; min-block-size: 2.75rem;
  margin-inline-end: -0.6rem;   /* keeps the glyph on the original optical edge */
  border-radius: 0.55rem;
}
.drawer__x:hover { color: var(--chalk); }
/* The chevron points where the drawer goes: off the inline end, which is the
   left in Arabic and the right in English. */
[dir="ltr"] .drawer__x svg { transform: scaleX(-1); }
.line__x { display: inline-grid; place-items: center; }

/* The scroll is the list AND the paperwork under it. Measured before this:
   with twelve items on a 375x812 phone the list got 296px (36% of the screen)
   and the block below it 443 (55%) — the reader could see two and a half of
   the twelve lines they were there to check. */
.drawer__scroll { flex: 1; overflow-y: auto; overscroll-behavior: contain; }
.drawer__body { padding: 0.5rem 1.25rem; }

/* Two rows, not three columns. The steppers and the remove button are three
   44px targets — deliberately, see the .step rule near the bottom of this
   file — plus the quantity, which comes to 172px of chrome. Sharing one row
   with them left the NAME, which is the only thing on the line a customer
   reads, with 118px in the 430px desktop drawer and 64px on a 360px phone,
   where "LEVEL UP BUILD i9 12900K + RTX 4070 TI 12G" came apart into one word
   per line and a 166px-tall row. The controls get their own row instead: the
   name gets 304px and 249px, and the line is no taller than it was. */
.line {
  display: grid;
  grid-template-columns: 56px 1fr;
  grid-template-areas:
    "media body"
    "media ctl";
  gap: 0.5rem 0.85rem;
  align-items: center;
  padding-block: 0.9rem;
}
.line__media { grid-area: media; }
.line__body { grid-area: body; }
.line__ctl { grid-area: ctl; }
.line + .line { border-block-start: 1px solid var(--hair2); }
.line__media {
  inline-size: 56px; block-size: 56px;
  background: var(--lift2);
  border: 1px solid var(--hair2);
  border-radius: var(--radius);
  overflow: hidden;
  display: grid; place-items: center;
}
.line__media { position: relative; }
.line__media img {
  position: absolute;
  inset: 8%;
  inline-size: 84%;
  block-size: 84%;
  object-fit: contain;
}
.line__name { font-size: var(--fs-small); line-height: 1.4; direction: ltr; text-align: start; }
/* Deliberately no price column. The space simply is not there. */
.line__ctl { display: flex; align-items: center; gap: 0.25rem; }
.step {
  inline-size: 26px; block-size: 26px;
  display: grid; place-items: center;
  border: 1px solid var(--hair2);
  border-radius: var(--radius);
  color: var(--dim);
  font-family: var(--font-mono);
  transition: all var(--fast) var(--ease);
}
.step:hover { border-color: var(--lilac); color: var(--chalk); }
.line__q { min-inline-size: 26px; text-align: center; font-family: var(--font-mono); font-size: var(--fs-small); }
/* Pushed to the far end of its own row, away from the + it used to sit
   against. Delete and increment are not neighbours worth having adjacent when
   both are 44px and reached by thumb. */
.line__x { color: var(--dim); padding: 0.2rem; margin-inline-start: auto; }
.line__x:hover { color: var(--amber); }

.drawer__foot { border-block-start: 1px solid var(--hair2); padding: 1.1rem 1.25rem; display: grid; gap: 0.85rem; }
/* Pinned, and only this. Everything above it can be scrolled past; the way to
   send the order cannot be. */
.drawer__act {
  border-block-start: 1px solid var(--hair2);
  padding: 0.85rem 1.25rem calc(0.85rem + env(safe-area-inset-bottom, 0px));
  background: var(--lift);
  display: grid;
  gap: 0.5rem;
}

.field label { display: block; font-size: var(--fs-micro); color: var(--dim); margin-block-end: 0.35rem; }
.field select {
  inline-size: 100%;
  padding: 0.6rem 0.8rem;
  background: var(--lift2);
  border: 1px solid var(--hair2);
  border-radius: var(--radius);
  color: var(--chalk);
  font: inherit;
  font-size: var(--fs-small);
}
.field select:focus { outline: none; border-color: var(--lilac); }

.freeline {
  font-size: var(--fs-small);
  color: var(--whatsapp);
  display: flex; align-items: center; gap: 0.4rem;
}
.freeline[hidden] { display: none; }

.ref { font-family: var(--font-mono); font-size: var(--fs-micro); color: var(--dim); }
.hint { font-size: var(--fs-micro); color: var(--dim); line-height: 1.7; margin: 0; }
.warn {
  font-size: var(--fs-micro);
  color: var(--amber);
  background: var(--amber-wash);
  border: 1px solid color-mix(in srgb, var(--amber) 35%, transparent);
  border-radius: var(--radius);
  padding: 0.55rem 0.7rem;
  margin: 0;
}
.warn[hidden] { display: none; }

/* =========================================================================
   footer
   ========================================================================= */

.foot { border-block-start: 1px solid var(--hair2); padding-block: 3rem 2.5rem; background: var(--lift); }
.foot h3 { font-size: var(--fs-small); color: var(--chalk); margin-block-end: 0.9rem; font-family: var(--font-body); }
.foot p, .foot li { color: var(--dim); font-size: var(--fs-small); margin: 0 0 0.4rem; }
.foot a:hover { color: var(--chalk); }

/* Three columns on a desktop, stacked on a phone: the shop's name and
   strapline, where it is, and when it opens. The name column is given the
   most room because it carries the mark and a line of Latin that must not
   wrap mid-phrase. */
.foot__in {
  display: grid;
  gap: clamp(1.75rem, 4vw, 3.5rem);
  align-items: start;
}
@media (min-width: 720px) {
  .foot__in { grid-template-columns: minmax(0, 1.3fr) minmax(0, 1fr) minmax(0, 1fr); }
}
.foot__brand { display: grid; gap: 0.7rem; justify-items: start; }
.foot__lockup { display: flex; align-items: center; gap: 0.6rem; }
.foot__lockup img { block-size: 38px; inline-size: auto; }
/* The header's wordmark, to the letter — same face, weight and tracking. The
   footer is the one place the name is a sign-off rather than a link, so it is
   not an <a>, but it must not look like a second, different logo. */
.foot__name {
  font-family: var(--font-mono);
  font-weight: 600;
  font-size: 1.05rem;
  letter-spacing: 0.14em;
  color: var(--chalk);
  direction: ltr;
}
.foot__name b { color: var(--lilac); font-weight: 600; }
/* The strapline, not fine print: --fs-small, the size the address beside it
   is set in, rather than the --fs-micro of the copyright line below. No width
   cap — one was tried at 22ch and broke a 31-character phrase across two lines
   inside a 427px column. The column is the only constraint it needs. */
.foot__tag {
  font-size: var(--fs-small);
  color: var(--dim);
  letter-spacing: 0.06em;
  margin: 0;
}
.foot__legal {
  margin-block-start: 2.5rem;
  padding-block-start: 1.5rem;
  border-block-start: 1px solid var(--hair2);
  font-size: var(--fs-micro);
  color: var(--dim);
  display: flex; flex-wrap: wrap; gap: 0.75rem; justify-content: space-between;
}

/* =========================================================================
   motion
   ========================================================================= */

/* Idle RGB breathe, at very low amplitude, on hover only. */
@keyframes breathe {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.62; }
}
.cat:hover > .led, .card:hover .led { animation: breathe 2.8s var(--ease) infinite; }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
  .card:hover { transform: none; }
}

@media (max-width: 520px) {
  /* The mark alone carries the brand at this width; the wordmark's 87px
     is better spent on the search field. */
  .brandmark__text { display: none; }
}

@media (max-width: 860px) {
  /* min-block-size, not block-size: the bar has to grow when the nav panel
     opens inside it, and a fixed height clipped the links out of sight. */
  .head__bar {
    min-block-size: 60px; block-size: auto;
    gap: var(--s1); flex-wrap: wrap;
    padding-block-end: var(--s1);
  }
  /* When the tab bar opens it, the field takes a full-width row of its own.
     Squeezed into the bar beside the other controls it was a ~90px box on a
     375px screen — nominally present, practically unusable. */
  body.is-searching .search {
    order: 8;
    flex: 1 0 100%;
    max-inline-size: none;
    min-block-size: var(--tap);
  }

  /* Visually hidden, NOT display:none. display:none takes the word out of the
     accessibility tree as well as off the screen, and this button's only other
     content is the count — so its accessible name collapsed to a bare number,
     announced as "0" with no indication of what it opens. */
  .reqbtn span:not(.reqbtn__count) {
    position: absolute; inline-size: 1px; block-size: 1px;
    padding: 0; margin: -1px; overflow: hidden;
    clip-path: inset(50%); white-space: nowrap;
  }

  /* Three groups, not one huddle. The controls used to sit in a row from the
     inline start, leaving the rest of the bar empty: the menu, the switch and
     the request list all crowded together with nothing to balance them.

     The mark is centred on the BAR rather than on what is left over between
     the controls, which is why it is positioned rather than given auto
     margins — the two sides are different widths, so an auto-margin centre
     would sit visibly off. Absolute is safe here: the bar sets its own
     min-block-size and the mark is shorter than that. */
  .head__bar { position: relative; }
  .brandmark {
    position: absolute;
    left: 50%;
    transform: translateX(-50%);
    /* Pinned to the first row and given its height, so the mark stays level
       with the buttons either side of it when the bar grows a second row for
       the search field. Left to its static position it drifted down and sat
       across the field's top edge. */
    /* Matched to the buttons rather than to the bar. The bar carries a bottom
       padding, which shifts its flow items up but means nothing to an
       absolutely placed box — so centring on the bar left the mark 4px below
       the buttons. Half that padding puts it exactly on them. */
    inset-block-start: calc(var(--s1) / 2);
    block-size: var(--tap);
  }
  /* With the field open the bar is two rows: the buttons sit flush at the top
     and the mark goes with them, which also stops its box from lying across
     the field and swallowing taps meant for it. */
  body.is-searching .brandmark { inset-block-start: 0; }
  /* Which leaves the flow to the three buttons: menu and switch at the inline
     start, the request list pushed to the far end. */
  .reqbtn { margin-inline-start: auto; }

  /* The nav becomes a panel under the bar, opened by .menubtn. */
  .menubtn { display: inline-grid; }
  .nav {
    order: 9;
    flex-basis: 100%;
    display: none;
    flex-direction: column;
    gap: 0;
    padding-block: 0.4rem 0.8rem;
  }
  .nav[data-open="true"] { display: flex; }
  .nav a {
    padding-block: 0.7rem;
    border-block-start: 1px solid var(--hair);
    font-size: var(--fs-body);
  }
}

/* The standalone .promise strip is gone. Its four headings now ride inside the
   hero as .hero__promise; the sentences under them were dropped with it. */

/* quick-browse chips — every sub-category, one tap, zero images needed */
.chips { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.chip {
  display: inline-flex; align-items: baseline; gap: 0.45rem;
  padding: 0.5rem 0.95rem;
  border: 1px solid var(--hair2);
  border-radius: 999px;
  font-size: var(--fs-small);
  color: var(--dim);
  transition: all var(--fast) var(--ease);
}
.chip:hover { color: var(--chalk); border-color: var(--lilac); background: var(--wash); }
.chip b { font-family: var(--font-mono); font-size: var(--fs-micro); color: var(--dim); font-weight: 400; }

/* the two custom builds — the only products that are actually theirs, and the
   only ones whose spec string IS the product photograph */
.builds { display: grid; gap: 1rem; }
@media (min-width: 800px) { .builds { grid-template-columns: 1fr 1fr; } }
.build {
  position: relative;
  padding: clamp(1.5rem, 3vw, 2.4rem);
  background: var(--lift);
  border: 1px solid var(--hair2);
  border-radius: var(--radius-lg);
  overflow: hidden;
  display: flex; flex-direction: column;
  transition: border-color var(--fast) var(--ease);
}
.build:hover { border-color: color-mix(in srgb, var(--own) 40%, var(--hair2)); }
.build > .led { position: absolute; inset-block-start: 0; inset-inline: 0; }
.build__tag {
  font-family: var(--font-mono); font-size: var(--fs-micro);
  letter-spacing: 0.16em; color: var(--dim); margin-block-end: 1.1rem;
}
.build__part { display: block; direction: ltr; text-align: start; }
.build__part + .build__part { margin-block-start: 0.3rem; }
.build__part span {
  font-family: var(--font-mono); font-weight: 600;
  font-size: clamp(1.15rem, 0.9rem + 1.1vw, 1.75rem);
  letter-spacing: -0.02em; color: var(--chalk); line-height: 1.2;
}
.build__part em {
  display: block; font-style: normal;
  font-family: var(--font-mono); font-size: var(--fs-micro);
  color: var(--dim); letter-spacing: 0.1em;
}
.build__foot { margin-block-start: auto; padding-block-start: 1.75rem; display: flex; gap: 0.6rem; flex-wrap: wrap; }
.brandtile {
  padding: 0.7rem 1.1rem;
  border: 1px solid var(--hair2);
  border-radius: var(--radius);
  background: var(--lift);
  color: var(--dim);
  font-family: var(--font-mono);
  font-size: var(--fs-small);
  letter-spacing: 0.04em;
  direction: ltr;
  transition: all var(--fast) var(--ease);
}
.brandtile:hover { color: var(--chalk); border-color: var(--lilac); }
.brandtile[data-depth="3"] { font-size: 1.15rem; color: var(--chalk); font-weight: 600; }
.brandtile[data-depth="2"] { font-size: 1rem; color: var(--chalk); }

/* visit — one physical shop, and with 3 Instagram followers the address is
   load-bearing information rather than a footer afterthought */
.visit {
  display: grid; gap: 2rem;
  padding: clamp(1.75rem, 4vw, 3rem);
  border: 1px solid var(--hair2);
  border-radius: var(--radius-lg);
  background: var(--lift);
}
@media (min-width: 800px) { .visit { grid-template-columns: 1.2fr 1fr; align-items: center; } }
.visit h2 { font-size: var(--fs-h1); margin-block-end: 0.9rem; }
.visit p { margin: 0 0 0.5rem; color: var(--dim); }
.visit dl { margin: 0; display: grid; gap: 0.9rem; }
.visit dt { font-size: var(--fs-micro); color: var(--dim); margin-block-end: 0.2rem; }
.visit dd { margin: 0; font-size: var(--fs-small); }

.section--tight { padding-block: clamp(2.25rem, 4vw, 3.5rem); }

/* =========================================================================
   catalog toolbar
   ========================================================================= */

/* ---- catalog layout: filter panel beside a grouped grid ----------------
   The pill rows this replaces were all horizontal, so on a phone half the
   options sat off-frame behind a sideways scroll, and none of them said how
   much was behind any choice. */

/* ---- active filters, each removable on its own ------------------------ */
.chips--active:empty { display: none; }
.achip {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.3rem 0.7rem;
  border: 1px solid var(--lilac);
  border-radius: 999px;
  background: var(--wash);
  color: var(--chalk);
  font-size: var(--fs-micro);
}
.achip span { color: var(--dim); }
.achip:hover { border-color: var(--amber); }
.achip:hover span { color: var(--amber); }

/* ---- bundle foot ------------------------------------------------------ */
.bundle__note { font-size: var(--fs-micro); color: var(--dim); margin: var(--s2) 0 0; }

.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.75rem;
  padding-block: 1rem;
  margin-block-end: 1.5rem;
  border-block: 1px solid var(--hair2);
}

.toolbar .result { margin: 0; margin-inline-start: auto; }

@media (max-width: 640px) {
  .toolbar { gap: 0.6rem; }
  .toolbar .result { margin-inline-start: 0; inline-size: 100%; }
}

/* The request-drawer thumbnail keeps the light lightbox: photos are
   flattened onto this exact colour by scripts/fetch_images.py, so the thumb
   blends in with no visible edge. Keep in step with FLATTEN_BG there. The
   card and product-page plates no longer use this — they MULTIPLY the photo
   onto the dark panel instead, and this rule outranked theirs via :has(). */
.line__media:has(img) { background: rgb(233, 230, 238); }

/* =========================================================================
   prices
   The site originally had none by design — availability sat in the price slot.
   Now that the shop publishes them, price leads and availability follows it as
   a smaller note, which is the order a shopper reads in.
   ========================================================================= */

.price {
  display: inline-flex;
  align-items: baseline;
  gap: 0.25rem;
  font-family: var(--font-mono);
  font-weight: 600;
  font-size: 0.95rem;
  color: var(--chalk);
  letter-spacing: -0.01em;
}
/* Arabic order: the amount first, the currency to its left — 200 د.ل. The box
   inherits the page's rtl, so the number is the first flex item and lands on
   the right. Only the digits are forced ltr, and isolated, so a grouped number
   like 1,234.50 cannot be reordered around its own comma. */
.price .num { direction: ltr; unicode-bidi: isolate; }
.price small { font-size: var(--fs-micro); font-weight: 400; color: var(--dim); }
.price--lg { font-size: clamp(1.5rem, 1.2rem + 1.2vw, 2.1rem); }

/* ---- on sale ------------------------------------------------------------
   Three parts and a clear order of loudness: the price you pay, then what it
   was, then by how much. The shop asked for the new price "بوضوح أكبر" and
   that is done by making the OLD one quieter rather than the new one bigger —
   a sale price set larger than every other price on the shelf would make the
   discounted products shout over the rest of the catalogue. */
.price--sale { flex-wrap: wrap; gap: 0.3rem 0.5rem; }
.price__now { display: inline-flex; align-items: baseline; gap: 0.25rem; }
.price__was {
  display: inline-flex; align-items: baseline; gap: 0.2rem;
  color: var(--faint);
  font-size: 0.82em;
  font-weight: 400;
  text-decoration-thickness: 1px;
}
.price__was .num { direction: ltr; unicode-bidi: isolate; }
.price__was small { font-size: inherit; color: inherit; }
.price__off {
  font-family: var(--font-body);
  font-size: var(--fs-micro);
  font-weight: 600;
  line-height: 1;
  padding: 0.28em 0.5em;
  border-radius: 999px;
  color: #fff;
  /* --core, not a new colour. It is the token documented as the fill that
     carries white text, it follows the theme on its own, and it keeps the
     badge inside the identity the shop asked to preserve. --amber was the
     other candidate and is spoken for: the palette reserves it for genuine
     last-piece stock, which is the one interrupt on this site. */
  background: var(--core);
  white-space: nowrap;
}

/* ---- the shop's own short tags ------------------------------------------
   "2K", "210Hz", "16GB". Chips, not a label/value table: they are read at a
   glance beside a product name, and half of them have no label worth
   printing — nobody needs to be told that 210Hz is a refresh rate. */
.tags {
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem;
  margin: 0.45rem 0 0;
  padding: 0;
  list-style: none;
}
.tags li {
  font-family: var(--font-mono);
  font-size: var(--fs-micro);
  line-height: 1;
  letter-spacing: 0.01em;
  color: var(--dim);
  background: var(--lift);
  border: 1px solid var(--hair2);
  border-radius: 6px;
  padding: 0.32em 0.5em;
  /* Latin units in an Arabic column: isolate so "16GB" cannot be reordered,
     and so a tag never drags the row's direction around with it. */
  direction: ltr;
  unicode-bidi: isolate;
  white-space: nowrap;
}
/* On a card the row must never become a second paragraph. The build already
   caps how many are printed; this stops a long one from forcing a third line. */
.tags--card { overflow: hidden; max-block-size: 1.9rem; }
.tags--pd { margin-block-start: 0.7rem; }
/* No price yet: say so plainly rather than showing a zero or a blank. */
.price--ask {
  font-family: var(--font-body);
  font-weight: 600;      /* the other half of retiring weight 500; see .btn */
  font-size: var(--fs-small);
  color: var(--dim);
}
.card__foot > div { display: flex; flex-direction: column; gap: 0.15rem; }
/* flex, not block: this needs to span the slot so the line below starts level
   with it, but `display: block` also threw away .price's flex gap, which is
   what set the amount off from د.ل — they rendered touching. */
.pd__slot .price--lg { display: flex; gap: 0.35rem; margin-block-end: 0.5rem; }

/* request list: prices and a total */
/* One string, "200 د.ل", so isolate rather than override: the page's rtl puts
   the amount on the right and the currency beside it, as in the price above. */
.line__price { font-family: var(--font-mono); font-size: var(--fs-micro); color: var(--dim); unicode-bidi: isolate; }
.total {
  display: flex; align-items: baseline; justify-content: space-between;
  padding-block: 0.9rem;
  border-block-start: 1px solid var(--hair2);
  font-size: var(--fs-small);
}
.total b { font-family: var(--font-mono); font-size: 1.15rem; color: var(--chalk); }
.total small { display: block; font-size: var(--fs-micro); color: var(--dim); font-weight: 400; }

/* =========================================================================
   Google reviews
   ========================================================================= */

.grev__score {
  display: flex; align-items: center; flex-wrap: wrap; gap: 0.75rem;
  margin-block-end: 1.5rem;
}
.grev__score b { font-family: var(--font-mono); font-size: 1.9rem; line-height: 1; }
.grev__score span { font-size: var(--fs-small); color: var(--dim); }
.star { color: var(--amber); font-size: 1rem; letter-spacing: -0.05em; }
.star--off { color: var(--hair2); }

.grev__rail { grid-auto-columns: minmax(260px, 320px); }
.grev {
  position: relative;
  display: flex; flex-direction: column; gap: 0.6rem;
  padding: 1.25rem;
  background: var(--lift);
  border: 1px solid var(--hair2);
  border-radius: var(--radius-lg);
  overflow: hidden;
}
.grev__top { display: flex; align-items: center; gap: 0.6rem; }
.grev__g {
  inline-size: 26px; block-size: 26px; flex: none;
  display: grid; place-items: center;
  border: 1px solid var(--hair2); border-radius: 50%;
  font-family: var(--font-mono); font-weight: 600; font-size: 0.8rem;
  color: var(--dim);
}
.grev__who { font-size: var(--fs-small); font-weight: 600; color: var(--lilac); direction: ltr; text-align: start; }
.grev__when { font-size: var(--fs-micro); color: var(--dim); }
.grev__text { margin: 0; font-size: var(--fs-small); color: var(--dim); line-height: 1.9; }

/* =========================================================================
   travelling border light
   gamingcenter.ly runs a light around its card edges (gc-snake-top/right/
   bottom/left, 2s linear). The technique is worth having; their red is not.
   This is the same idea rendered in the LED language already used here — one
   purple bead, brand colour, only on hover, and only on the cards that invite
   a click. Two elements rather than four keyframed edges: a conic gradient
   masked to a 1px ring costs one paint and cannot drift out of sync.
   ========================================================================= */

@property --lu-angle { syntax: '<angle>'; initial-value: 0deg; inherits: false; }

.grev::after,
.build::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  padding: 1px;
  background: conic-gradient(from var(--lu-angle),
              transparent 0deg, transparent 300deg,
              var(--lilac) 340deg, var(--lilac) 352deg, transparent 360deg);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask-composite: exclude;
  opacity: 0;
  transition: opacity var(--fast) var(--ease);
  pointer-events: none;
}
.grev:hover::after,
.build:hover::after {
  opacity: 1;
  animation: lu-travel 2.4s linear infinite;
}
@keyframes lu-travel { to { --lu-angle: 360deg; } }

/* Browsers without @property cannot animate the angle; they simply get the
   static ring, which still reads as a highlight rather than as nothing. */
@supports not (background: conic-gradient(from 0deg, red, blue)) {
  .grev::after, .cat::after, .build::after { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .grev:hover::after, .build:hover::after { animation: none; }
}

/* Real manufacturer marks -------------------------------------------------
   Every logo arrives in its brand's own colours — Razer green, MSI red, NZXT
   purple — and a row of those reads as a ransom note. Flattening them all to
   white makes the row one object and lets the ground carry the colour. That
   is also why these are SVG only: a PNG with a baked-in white background
   cannot be flattened and would sit in the row as a bright rectangle. */
.mfrmark {
  display: grid;
  place-items: center;
  padding: 0.55rem 1.1rem;
  border: 1px solid transparent;
  border-radius: var(--r-sm);
  transition: opacity .18s ease, border-color .18s ease;
}
.mfrmark img {
  inline-size: auto;
  max-inline-size: none;
  filter: brightness(0) invert(1);
  opacity: .58;
  transition: opacity .18s ease;
}
/* brightness(0) crushes the mark to black; invert(1) then lifts it to white.
   On a light ground the invert has to go or the whole row turns white-on-white
   and disappears — the logos are flattened to ink instead. */
:root[data-theme="light"] .mfrmark img { filter: brightness(0); opacity: .62; }
/* Sized by shape, not by one height. After trimming each viewBox to its ink,
   aspect runs from 0.82:1 (MSI's dragon) to 12.9:1 (the Thermalright
   wordmark); a single height renders the glyphs postage-stamp small beside
   the long words. Heights fall as marks get wider so each carries roughly
   the same optical weight. Widths stay free — a long wordmark simply takes
   more of the row, which is what it does on a real shelf. */
.mfrmark[data-shape="glyph"] img { block-size: 34px; }
.mfrmark[data-shape="mid"]   img { block-size: 28px; }
.mfrmark[data-shape="wide"]  img { block-size: 23px; }
.mfrmark[data-shape="long"]  img { block-size: 17px; }

.mfrmark:hover img,
.mfrmark:focus-visible img { opacity: 1; }
.mfrmark:hover { border-color: var(--hair2); }
.mfrmark:focus-visible { outline: 2px solid var(--lilac); outline-offset: 2px; }

/* ---- sub-page header --------------------------------------------------- */
.pagehead {
  padding-block: clamp(2.2rem, 5vw, 3.6rem) clamp(1rem, 2vw, 1.6rem);
  border-block-end: 1px solid var(--hair2);
}
.pagehead .led { max-inline-size: 200px; margin-block-end: 1.1rem; }
.pagehead h1 { font-size: var(--fs-h1); margin-block-end: 0.7rem; }
.pagehead__lede { color: var(--dim); max-inline-size: 52ch; margin: 0; }

/* ---- the custom-build steps -------------------------------------------- */
.steps {
  list-style: none;
  counter-reset: step;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 0.8rem;
}
@media (min-width: 800px) { .steps { grid-template-columns: 1fr 1fr; } }
.steps li {
  counter-increment: step;
  position: relative;
  padding: 1.1rem 3.4rem 1.1rem 1.2rem;
  background: var(--lift);
  border: 1px solid var(--hair2);
  border-radius: var(--radius-lg);
}
[dir="ltr"] .steps li { padding: 1.1rem 1.2rem 1.1rem 3.4rem; }
.steps li::before {
  content: counter(step);
  position: absolute;
  inset-block-start: 1.05rem;
  inset-inline-start: 1.1rem;
  inline-size: 1.7rem; block-size: 1.7rem;
  display: grid; place-items: center;
  background: var(--core);
  color: #fff;
  border-radius: 50%;
  font-family: var(--font-mono);
  font-size: var(--fs-micro);
}
.steps b { display: block; margin-block-end: 0.2rem; }
.steps span { color: var(--dim); font-size: var(--fs-small); }

/* ---- contact routes ---------------------------------------------------- */
.contacts { display: grid; gap: 0.8rem; }
@media (min-width: 700px) { .contacts { grid-template-columns: repeat(3, 1fr); } }
.contact {
  display: flex;
  align-items: center;
  gap: 0.9rem;
  padding: 1.1rem 1.2rem;
  background: var(--lift);
  border: 1px solid var(--hair2);
  border-radius: var(--radius-lg);
  transition: border-color var(--fast) var(--ease), background var(--fast) var(--ease);
}
.contact:hover { border-color: var(--lilac); background: var(--lift2); }
.contact svg { flex: none; color: var(--lilac); }
.contact b { display: block; }
.contact span { color: var(--dim); font-size: var(--fs-small); }

.build__price { margin-block-start: 1rem; }

/* =========================================================================
   product tile extras: sheen, flag, reveal
   ========================================================================= */

/* ---- product tile: media, sheen, flag ---------------------------------- */
.card:hover {
  transform: translateY(-5px);
  border-color: color-mix(in srgb, var(--lilac) 45%, transparent);
  box-shadow: 0 20px 46px -24px #000;
}
.card__brand {
  font-size: var(--fs-micro);
  color: var(--dim);
  letter-spacing: 0.14em;
}

/* Reveal on scroll. Only a translate and a fade — if the class never arrives
   the text is still readable, because a stuck opacity:0 is a blank page. */
.card, .section__head { opacity: 1; }
.card.is-in, .cat.is-in { animation: lu-rise 0.6s var(--ease) both; }
@keyframes lu-rise { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: none; } }

@media (prefers-reduced-motion: reduce) {
  .card { transition: none; }
  .card:hover { transform: none; }
  .card.is-in, .cat.is-in { animation: none; }
}

/* ---- footer on the light theme's blue floor -----------------------------
   The light page now ends in --floor (#0F5FA8). Dark ink measures 2.6:1 on
   that, so the footer — the only block sitting on it — flips to light text.
   Dark theme is unaffected: its floor is near-black and the tokens already
   read against it. */
:root[data-theme="light"] .foot {
  background: var(--deepblue);
  color: #DCE9F6;
}
:root[data-theme="light"] .foot h3 { color: #FFFFFF; }
:root[data-theme="light"] .foot p,
:root[data-theme="light"] .foot li,
:root[data-theme="light"] .foot a { color: #DCE9F6; }
:root[data-theme="light"] .foot a:hover { color: #FFFFFF; }
:root[data-theme="light"] .foot__legal {
  color: #C3DAEE;
  border-block-start-color: rgba(255, 255, 255, 0.22);
}
:root[data-theme="light"] .foot .hours__t,
:root[data-theme="light"] .foot .hours__rule { color: #DCE9F6; }
/* The sign-off wordmark was missed when the footer was flipped for the blue
   floor. It kept --chalk and --lilac, which on the light theme are the DARK
   ink and the darkened accent — measured on #0F5FA8 at 2.90:1 for "LEVELUP"
   and 1.45:1 for the "UP". Everything else in this block exists because the
   floor stopped following the theme; this is the one element left behind,
   and it is the shop's own name. */
:root[data-theme="light"] .foot__name { color: #FFFFFF; }
:root[data-theme="light"] .foot__name b { color: #F0DBFF; }

/* A request-list line with no product photo. It used to print the last six
   digits of the SKU; SKUs are the shop's internal reference and mean nothing
   to a customer, so it is a neutral mark instead. */
.line__noimg {
  inline-size: 18px;
  block-size: 18px;
  border-radius: 4px;
  border: 1px dashed var(--hair2);
  display: block;
}

/* =========================================================================
   interactive targets
   -------------------------------------------------------------------------
   A project usability floor of --tap (44px) on everything you press. Placed
   last on purpose: these are minimums that must survive whatever a component
   set earlier, and they only touch size — never colour, so the theme blocks
   above are unaffected.

   min-block-size rather than height: several of these carry Arabic that wraps
   at narrow widths, and a fixed height would clip the second line.
   ========================================================================= */
.btn,
.pill,
.menubtn,
.reqbtn,
.achip {
  min-block-size: var(--tap);
}

/* Square controls: the icon add button on a card, the quantity steppers, and
   the drawer's remove. These were 28-34px boxes — inside the range where a
   thumb hits the neighbouring control instead. */
.add,
.step,
.line__x {
  min-inline-size: var(--tap);
  min-block-size: var(--tap);
}

/* The card's add button is a circle inside a tight footer, so it grows by
   padding rather than by box size, keeping the footer's rhythm. */
.card__foot .add { border-radius: 999px; }

/* Sourced specifications. Absent on most cards, by design — see card(). */
.card__specs {
  list-style: none;
  margin: var(--s1) 0 0;
  padding: 0;
  display: grid;
  gap: 2px;
}
.card__specs li {
  display: flex;
  gap: var(--s1);
  font-size: var(--fs-micro);
  color: var(--dim);
}
.card__specs em { font-style: normal; color: var(--faint); flex: none; }
.card__specs span { min-inline-size: 0; }

.card__facts { display: flex; align-items: center; gap: var(--s1); flex-wrap: wrap; }

/* The add action carries words now. An icon-only "+" gave no indication of
   what it added to, and it was the one control on the card a customer has to
   understand. */
.btn--add {
  font-size: var(--fs-micro);
  padding-inline: var(--s2);
  inline-size: 100%;
  justify-content: center;
}
.card__foot {
  display: grid;
  gap: var(--s1);
  margin-block-start: auto;
  padding-block-start: var(--s1);
}

/* Catalogue grid columns, per the review: three beside the desktop filter
   panel, two on a phone while the name and the labelled add action still fit,
   one below that. auto-fill with a min would have produced four cramped
   columns on a wide screen, which is how the incomplete rows started. */
/* Two columns from the narrowest phone up, not from 420px.
   One column meant a 307px-wide card 500px tall on a 375px screen — a
   product and a half per screenful, and 92 of them to scroll past. Measured
   against crown.ly, which the owner pointed at: at the same 375px they run
   two columns of 166px with a 12px gutter and a 366px card, four products
   per screen. This is that, in this site's spacing scale. */
.catalog__main .grid {
  display: grid;
  gap: 0.75rem;
  grid-template-columns: repeat(2, minmax(0, 1fr));
}
@media (min-width: 560px) { .catalog__main .grid { gap: var(--s2); } }
@media (min-width: 900px) { .catalog__main .grid { grid-template-columns: repeat(3, 1fr); } }

.builds { display: grid; gap: var(--s2); }
@media (min-width: 760px) { .builds { grid-template-columns: repeat(2, 1fr); } }

/* Static brand strip: one row, wrapping, no motion. */
.brandstrip__h {
  font-size: var(--fs-small);
  font-weight: 600;
  color: var(--dim);
  margin: 0 0 var(--s2);
}
/* brand wall — two rows drifting in opposite directions.
   Same machinery as the delivery ticker (.ship__track): a max-content track
   carrying the row twice, travelling exactly half its width so the copy lands
   where the original began. */
.brandwall { display: grid; gap: var(--s2); }
.brandrow {
  overflow: hidden;
  /* Fades the ends so tiles enter and leave instead of being guillotined at
     the edge of the wrap. */
  -webkit-mask: linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent);
          mask: linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent);
}
.brandrow__track {
  display: flex;
  align-items: center;
  inline-size: max-content;
  opacity: 0.75;
  /* Duration comes from the build, as seconds per mark — see homepage.py.
     A fixed 60s is a fixed number of LAPS, not a fixed speed: the row used to
     hold 14 marks and now holds 21, so the same 60s moved it half again as
     fast as the speed the owner approved. The fallback is the old value, for
     any track rendered without the property. */
  animation: lu-marquee var(--marquee-s, 60s) linear infinite;
}
/* Spacing as a per-tile margin, NOT flex gap: 2N tiles carry 2N-1 gaps, so
   half the track is half a gap short of one full copy and the loop jumps once
   a minute. A trailing margin on every tile makes the two halves identical. */
.brandrow__track > a { flex: none; margin-inline-end: var(--s3); }
/* Same sign rule as the ticker: an RTL max-content track already extends left
   of the window, so it travels RIGHT to bring the duplicate in. */
[dir="rtl"] .brandrow__track { animation-name: lu-marquee-rtl; }
/* There used to be a second row here running against the first. The wall is
   one row now (issue #5), so the rule that reversed row 1 has gone with it
   rather than sitting here matching nothing. */
.brandrow:hover .brandrow__track,
.brandrow:focus-within .brandrow__track { animation-play-state: paused; }
.brandrow:hover .brandrow__track { opacity: 1; }

/* Asked to stop moving, it becomes the wrapped block this replaced — not a
   frozen track, which would strand most of the logos off-screen. The duplicate
   copy leaves with the motion that needed it. */
@media (prefers-reduced-motion: reduce) {
  .brandrow { overflow: visible; -webkit-mask: none; mask: none; }
  .brandrow__track {
    flex-wrap: wrap;
    inline-size: auto;
  }
  /* Both selectors, matching the specificity of the rules that set the
     animation above. `.brandrow__track { animation: none }` alone loses
     animation-name to `[dir="rtl"] .brandrow__track` (0,1,1 beats 0,1,0) and
     the row only sits still because the shorthand also zeroed the duration —
     which the next person to set a duration here would undo without knowing.
     A third selector for `.brandrow[data-row="1"]` went with the second row. */
  .brandrow__track,
  [dir="rtl"] .brandrow__track { animation: none; }
  .brandrow__track > a[aria-hidden="true"] { display: none; }
}

/* A reconciled line the shop can no longer supply. Kept in the list — the
   customer chose it — but marked, and excluded from the total. */
.line__flag {
  display: inline-block;
  margin-block-start: 2px;
  font-size: var(--fs-micro);
  color: var(--amber);
}
.line[data-blocked="true"] .line__media,
.line[data-blocked="true"] .line__name { opacity: 0.55; }
.line[data-blocked="true"] .line__price { text-decoration: line-through; opacity: 0.6; }

/* =========================================================================
   POWERED ON — the visual layer
   -------------------------------------------------------------------------
   A dark room where the hardware is the only light source. Appended last so
   it wins every tie; everything below is single-class specificity on purpose.
   The signature is the LED strip: it powers on from the reading-start edge as
   a section arrives, it runs along the top of every plate, and it brightens
   when you reach for something. Under reduced motion it is simply lit.
   ========================================================================= */

/* ---- the room --------------------------------------------------------- */
body {
  background-color: var(--void);
  /* The ONE ambient light: a soft pool high in the room, above the hero. This
     is the "controlled hero light" — not a surface gradient, but what a lit
     object does to the dark around it. */
  background-image: radial-gradient(60% 40% at 50% -6%, var(--wash), transparent 70%);
  background-repeat: no-repeat;
}

/* ---- the strip -------------------------------------------------------- */
.led {
  display: block;
  block-size: 2px;
  inline-size: 100%;
  border-radius: 2px;
  background: var(--led);
  box-shadow: 0 0 10px 1px color-mix(in srgb, var(--led) 55%, transparent);
  transform-origin: 100% 50%;                     /* RTL: light from the right */
}
[dir="ltr"] .led { transform-origin: 0 50%; }
.led::after { display: none; }                    /* the box-shadow is the bloom now */

/* Powers on when its section arrives. initReveal adds .is-lit. Fails OPEN:
   no IntersectionObserver, or reduced motion, and the strip is simply on. */
.led--power {
  transform: scaleX(0);
  opacity: 0.35;
  transition: transform var(--power) var(--ease-out), opacity 0.4s var(--ease);
}
.led--power.is-lit { transform: scaleX(1); opacity: 1; }

/* Along the top edge of a plate or tile. Dim at rest; full brightness when
   you reach for it. */
.led--edge {
  position: absolute;
  inset-block-start: 0;
  inset-inline: 0;
  z-index: 2;
  block-size: 2px;
  opacity: 0.45;
  transition: opacity var(--fast) var(--ease), box-shadow var(--fast) var(--ease);
}
.card:hover .led--edge, .card:focus-within .led--edge,
.cat:hover  .led--edge, .cat:focus-within  .led--edge {
  opacity: 1;
  box-shadow: 0 0 16px 2px color-mix(in srgb, var(--led) 70%, transparent);
}

.section__head .led { max-inline-size: 160px; margin-block-end: var(--s2); }
@media (min-width: 720px) { }

/* ---- the plate: every product is an object on a lit panel -------------- */
.card {
  position: relative;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  border-radius: var(--radius-lg);
  border: 1px solid var(--hair);
  background: var(--lift);
  transform: none;
  transition: border-color var(--fast) var(--ease), transform 0.35s var(--ease),
              box-shadow 0.35s var(--ease);
}
.card:hover {
  border-color: var(--hair2);
  transform: translateY(-3px);
  box-shadow: 0 26px 50px -30px #000;
}
.card[data-avail="out"]:hover { transform: none; }
.card__plate {
  position: relative;
  display: grid;
  place-items: center;
  aspect-ratio: 1;
  background: var(--plate, #E9E6EE);
  border-block-end: 1px solid var(--hair);
  overflow: hidden;
}
.card__plate::after {                          /* rim light, brightens on reach */
  content: '';
  position: absolute;
  inset: 0;
  box-shadow: inset 0 -50px 70px -50px var(--rim);
  opacity: 0.35;
  pointer-events: none;
  transition: opacity 0.4s var(--ease);
}
.card:hover .card__plate::after { opacity: 0.9; }
.card__plate img {
  position: absolute;
  width: var(--photo-size, 94%);
  height: var(--photo-size, 94%);
  left: var(--photo-left, 3%);
  top: var(--photo-top, 3%);
  object-fit: contain;
  object-position: var(--photo-position, 50% 50%);
  padding: 0;
}
.card[data-avail="out"] .card__plate img { filter: grayscale(1) brightness(0.9); opacity: 0.55; }

/* The name AS the image. Large, in the display face, like a part number on a
   box. Latin runs are LTR inside the RTL page; overflow-wrap keeps a long
   model number from splitting mid-token. */
.card__plate--type {
  background: var(--lift2);
  padding: var(--s3);
  align-content: center;
  gap: var(--s1);
  text-align: start;
}
.card__type {
  display: block;
  font-family: var(--font-display);
  /* Sized against the VIEWPORT, which stopped being a fair proxy for the card
     the moment the phone grid went to two columns: 22px of signage type on a
     147px plate broke "XPG LANDER 500" across three lines and clipped it.
     74 of 92 products are read this way, so this is the common case, not the
     exception. Smaller in the 2-up grid, unchanged everywhere else. */
  font-size: clamp(1.35rem, 1rem + 1.6vw, 2rem);
  font-weight: 700;
  line-height: 1.12;
  color: var(--chalk);
  opacity: 0.92;
  overflow-wrap: anywhere;
}
/* AFTER .card__type, not with the other phone rules near the top of this
   file. Those sit earlier, and at equal specificity the later rule wins — the
   override was written up there first and did nothing at all, leaving 22px
   type breaking a build name across seven lines. */
@media (max-width: 560px) {
  .card__type { font-size: clamp(0.95rem, 0.7rem + 1.1vw, 1.3rem); }
  .card__plate--type { padding: var(--s2); }
}

.card__typecat {
  display: block;
  margin-block-start: var(--s1);
  font-size: var(--fs-micro);
  letter-spacing: 0.12em;
  color: var(--lilac);
}
.card__body { padding: var(--s2); gap: var(--s1); }
.card__name { font-family: var(--font-body); font-weight: 600; font-size: var(--fs-small); }

/* ---- the brand ticker --------------------------------------------------- */

/* ---- section heads ---------------------------------------------------- */
.section__title { font-family: var(--font-display); font-size: var(--fs-h2); }

/* ---- reduced motion: everything is simply lit ------------------------- */
@media (prefers-reduced-motion: reduce) {
  .led--power { transform: none; opacity: 1; transition: none; }
  .cat, .card, .cat__sign, .card__plate img { transition: none; }
}

/* ---- pd__media: the plate, at detail size ------------------------------
   The product page's photo was still on the old flat white plate while the
   hero and every card had moved to the lit panel. Same treatment here: the
   panel, the photo multiplied onto it, the rim light, and the strip along the
   top — the .led the template already emits inside .pd__media. */
.pd__media {
  position: relative;
  display: grid;
  place-items: center;
  aspect-ratio: 1;
  /* Photographs may be framed to different shapes, so the plate changes
     height as you swipe between them. Eased over the same half second as the
     slide, the two read as one movement. Where a browser will not interpolate
     aspect-ratio it simply snaps, which is what it did before. */
  transition: aspect-ratio 0.5s cubic-bezier(0.22, 0.61, 0.36, 1);
  overflow: hidden;
  border-radius: var(--radius-lg);
  background: var(--plate, #E9E6EE);
  border: 1px solid var(--hair2);
  box-shadow: 0 30px 70px -30px #000, inset 0 1px 0 rgba(245, 240, 255, 0.06);
}
.pd__media > .led { position: absolute; inset-block-start: 0; inset-inline: 0; z-index: 2; }
.pd__media::after {
  content: '';
  position: absolute;
  inset: 0;
  box-shadow: inset 0 -70px 90px -60px var(--rim);
  opacity: 0.7;
  pointer-events: none;
}
.pd__media img {
  position: absolute;
  width: var(--photo-size, 94%);
  height: var(--photo-size, 94%);
  left: var(--photo-left, 3%);
  top: var(--photo-top, 3%);
  object-fit: contain;
  object-position: var(--photo-position, 50% 50%);
  padding: 0;
}
/* The unphotographed placeholder inside the same panel keeps its icon but
   sits on the panel rather than on its own grey box. */
.pd__media .ph { background: transparent; border: 0; }
.pd-gallery { min-width: 0; }

/* The photographs sit side by side off stage and slide. JS writes --slide-x
   per plate: its distance from the one on show, plus the finger.

   touch-action is what lets the whole gesture run on passive listeners — the
   browser keeps vertical scrolling, we get the horizontal, and nothing has to
   call preventDefault to stop the page moving under a thumb. */
.pd__media.is-swipeable { touch-action: pan-y; cursor: grab; }
.pd__media.is-dragging { cursor: grabbing; }
.pd-gallery__photo {
  display: block; position: absolute; inset: 0;
  transform: translate3d(var(--slide-x, 0px), 0, 0);
  transition: transform 0.5s cubic-bezier(0.22, 0.61, 0.36, 1);
}
/* While the hand is down the plate IS the finger: no easing between where it
   was and where it is, or the picture lags behind the thumb. */
.pd__media.is-dragging .pd-gallery__photo { transition: none; }
/* Off-stage plates are still in the document, so they must not swallow a
   click meant for the one on show. */
.pd-gallery__photo[aria-hidden="true"] { pointer-events: none; }
.pd-gallery__photo[hidden], .pd-gallery__controls[hidden] { display: none; }
@media (prefers-reduced-motion: reduce) {
  .pd-gallery__photo { transition: none; }
}
.pd-gallery__thumbs { display: flex; flex-wrap: wrap; gap: 8px; margin-block-start: 12px; }
.pd-gallery__thumb { display: block; width: 64px; height: 64px; border: 2px solid transparent; border-radius: 8px; overflow: hidden; background: #f5f5f7; }
.pd-gallery__thumb img { width: 100%; height: 100%; object-fit: contain; padding: 4px; }
.pd-gallery__thumb[aria-current="true"] { border-color: var(--lilac); box-shadow: 0 0 0 1px var(--lilac); }
.pd-gallery__controls { display: flex; align-items: center; justify-content: center; gap: 16px; margin-block-start: 12px; }
.pd-gallery__controls button { min-width: 44px; min-height: 44px; border: 1px solid currentColor; border-radius: 8px; background: transparent; color: inherit; font: inherit; cursor: pointer; }
.pd-gallery a:focus-visible, .pd-gallery button:focus-visible { outline: 3px solid var(--lilac); outline-offset: 3px; }

/* =========================================================================
   THE PARTS WALL — the catalogue in design mode
   -------------------------------------------------------------------------
   The parts wall in a shop: labelled bins you scan by category, each with a
   strip of light along its edge. The category rail is that row of labels —
   the signature spent here. The sidebar becomes the control panel, its
   options lit by an LED dot rather than a bar. The grid stays continuous;
   when a filter narrows it to one category, that name is stated at signage
   size, the same device as the homepage doors.
   ========================================================================= */

/* ---- the empty plate --------------------------------------------------- */
.empty--plate {
  position: relative;
  overflow: hidden;
  text-align: start;
  padding: var(--s4);
  border-radius: var(--radius-lg);
  background: var(--lift2);
  border: 1px solid var(--hair);
}
.empty__term {
  margin: 0 0 var(--s2);
  font-family: var(--font-display);
  font-size: clamp(1.5rem, 1.1rem + 2vw, 2.4rem);
  font-weight: 700;
  line-height: 1.1;
  color: var(--chalk);
  opacity: 0.9;
  overflow-wrap: anywhere;
}
.empty__term:empty { display: none; }
.empty--plate h3 { margin-block-end: var(--s1); }
.empty--plate p { margin: 0 0 var(--s2); color: var(--dim); }
.empty--plate p:last-child { margin: 0; }

/* ---- reduced motion --------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
}

/* =========================================================================
   HERO SLIDER — restored
   -------------------------------------------------------------------------
   The owner's hero: one slide per photographed category, the category's most
   expensive photographed part as the illustration. It was replaced once by a
   stable headline with rotating panes and asked for back. Dark: the photo is a
   tilted card with a real edge. Light: it dissolves into the page. Arrows in
   the bottom corners, swipe on touch, pauses on hover and focus.
   ========================================================================= */

.hero { position: relative; border-block-end: 1px solid var(--hair); }
.hero__stage {
  position: relative;
  /* Taller than the old split, because the slide is now a stack: image card on
     top, caption underneath. The caption needs ~150px and the card wants the
     rest, so a 46vw stage would have squeezed the photograph to a strip. */
  block-size: clamp(470px, 60vw, 600px);
  overflow: hidden;
}
.hero__panes { position: absolute; inset: 0; }

/* A stack, not a split: image card on top, caption beneath, the same shape at
   every width. That is what removes the mobile wash — the photograph is never
   behind the words, so it never has to fade to keep them readable. */
.sl {
  position: absolute;
  inset: 0;
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto;
  justify-items: center;
  gap: clamp(0.9rem, 2.5vw, 1.4rem);
  padding: clamp(1.1rem, 3vw, 2rem) var(--gutter) clamp(1.3rem, 3vw, 2rem);
  /* visibility, not just opacity: a transparent slide still takes focus and is
     still read aloud. Hiding it removes it from the tab order for free, with
     no aria-hidden bookkeeping to fall out of sync. */
  opacity: 0;
  visibility: hidden;
  /* Fade THROUGH the background, not across. When the slides were text beside a
     bleeding half-image a straight cross-dissolve was fine; now that each slide
     is an opaque card, two of them at ~50% is a double exposure — measured at
     0.22/0.78 with an RTX ghosting through a case. The outgoing card leaves in
     0.35s and the incoming one is held back 0.3s, so the stage is briefly empty
     instead of showing two products at once. */
  transition: opacity 0.35s ease, visibility 0.35s;
  background:
    radial-gradient(90% 120% at 12% 50%, color-mix(in srgb, var(--c1) 55%, transparent), transparent 62%),
    radial-gradient(70% 90% at 95% 10%, color-mix(in srgb, var(--c2) 26%, transparent), transparent 60%),
    linear-gradient(105deg, #0B0614 12%, #140A1F 60%, #0A0512 100%);
  /* The slide stays dark in BOTH themes on purpose: it is a lit stage for a
     photograph, and the tints only read against a dark ground. Because the
     ground no longer follows the theme, the text colours on it must be
     pinned here rather than inherited from --chalk/--dim. */
  color: #FBF8FF;
}
/* Held back by 0.3s so the outgoing card has cleared before this one arrives.
   visibility flips immediately — it only exists to keep hidden slides out of
   the tab order, and delaying it would leave the slide unfocusable while it
   fades in. */
.sl.is-on {
  opacity: 1;
  visibility: visible;
  transition: opacity 0.45s ease 0.3s, visibility 0s;
}

/* ---- the image card ---------------------------------------------------
   A solid object with depth, at every width. It was previously a flat 26% wash
   behind the text on phones, which is what "flushed out" — a pale rectangle
   with no edges. It is now the same card on a phone as on a desktop: an opaque
   plate, tilted a few degrees in perspective and lifted off the slide by two
   stacked shadows, so it reads as a thing sitting in space rather than a
   background. */
/* The box takes the PHOTO's shape, from --ar emitted per slide by hero.py.
   With the box and the photo the same rectangle there is nothing for
   object-fit: contain to letterbox, so the feather lands on the photo's own
   edge on all four sides. Falls back to `auto` when the ratio is unavailable. */
.sl__art {
  position: relative;
  block-size: 100%;
  aspect-ratio: var(--ar, auto);
  inline-size: auto;
  max-inline-size: min(100%, 46rem);
  perspective: 1200px;
}
/* The pool. --plate is the photo's OWN border colour, sampled per image by
   hero.py, spread behind it and faded into the slide.
   Feathering the photo's edge alone did not work and could not: the plate is
   baked into the pixels, so softening the boundary turned a clean rectangle
   into a blurry pale slab — worse than the edge it was hiding. Extending the
   plate outwards instead means there is no boundary left to see, because the
   colour is identical on both sides of it.
   A BLURRED RECTANGLE, not a radial gradient. The photo is a rectangle and an
   ellipse cannot cover its corners: inscribed in the same box, the ellipse
   leaves all four corners sitting on a half-faded pool, which drew the very
   rectangle outline this is meant to dissolve.
   The geometry: the box is inset -11% and blurred 30px, so the blur eats
   roughly the margin it was given and the SOLID plateau lands just outside the
   photo's own edge. The photo's feathered border therefore sits on solid plate
   colour, and the fade into the slide happens beyond the photo entirely.
   THE POOL IS LIGHT-THEME ONLY. On a near-white page the plate and the page are
   almost the same colour, so the photo genuinely dissolves into it. On the dark
   stage it cannot: a light plate on a dark ground has nowhere to fade TO, and
   the pool just became a pale glow around a pale slab. Dark gets a card with a
   real edge instead — see .sl__art img below. */
.sl__art::before {
  content: '';
  position: absolute;
  inset: -11%;
  z-index: 0;
  pointer-events: none;
  background: var(--plate, #F0EEF5);
  filter: blur(30px);
  display: none;                     /* dark is the default; see light override */
}

/* A gentle feather now, because the pool is doing the blending. Two linear
   gradients intersected soften all four sides while leaving the middle alone —
   what a radial mask cannot promise, since an ellipse tight enough to reach the
   corners also eats the ends of a wide product like a monitor.
   `mask-composite: intersect` is the standard property and
   `-webkit-mask-composite: source-in` the older WebKit spelling of it. */
/* Fills its box exactly, because the box was given the photo's own aspect ratio
   above. Sizing the IMAGE to its intrinsic ratio instead was tried and pushed
   it 34px past its grid row — a percentage block-size on a centred grid item
   does not resolve the way it looks like it should — and the caption ended up
   printed across the product. Constraining the container is what holds. */
/* DARK (the default): a card with a real edge.
   The padding is filled by --plate — the photo's OWN sampled background — so
   the margin around the product is the same colour as the photo's ground and
   the two read as one continuous surface. The old hardcoded #F0EEF5 did not
   match a single one of the six photos, which is why the card used to show a
   faint seam between its padding and the image. */
.sl__art img {
  position: relative;
  z-index: 1;
  inline-size: 100%;
  block-size: 100%;
  object-fit: contain;               /* never crop a product to fill a box */
  background: var(--plate, #F0EEF5);
  border-radius: clamp(14px, 2vw, 22px);
  padding: clamp(0.7rem, 2.5vw, 1.8rem);
  /* A close shadow for contact, a long one for lift, and a hairline so the
     light plate keeps an edge against the dark stage. */
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.35),
              0 34px 60px -26px rgba(0, 0, 0, 0.85),
              inset 0 0 0 1px rgba(255, 255, 255, 0.10);
  transform: perspective(1200px) rotateY(-6deg) rotateX(2.5deg);
  transform-origin: 50% 60%;
  transition: transform 0.6s var(--ease);
}
/* The drift now rocks the card rather than scaling it, which is what sells the
   third dimension — a plane that only zooms reads as flat. */
.sl.is-on .sl__art img { animation: lu-tilt 17s ease-in-out infinite alternate; }
@keyframes lu-tilt {
  from { transform: perspective(1200px) rotateY(-6deg) rotateX(2.5deg) scale(1); }
  to   { transform: perspective(1200px) rotateY(4deg)  rotateX(-1.5deg) scale(1.03); }
}

/* The mobile wash and its scrim are gone. Both existed to keep text readable on
   top of a faded photograph; the stacked layout puts the words BELOW the card
   instead, so there is nothing to compensate for and the photo keeps its full
   opacity on a phone. A shallower tilt there — a 6° rotation costs real width
   on a 375px screen, and the shadow already carries the depth. */
@media (max-width: 560px) {
  .sl__art img {
    transform: perspective(900px) rotateY(-3deg) rotateX(1.5deg);
  }
  .sl.is-on .sl__art img { animation-name: lu-tilt-sm; }
  @keyframes lu-tilt-sm {
    from { transform: perspective(900px) rotateY(-3deg) rotateX(1.5deg) scale(1); }
    to   { transform: perspective(900px) rotateY(2deg)  rotateX(-1deg)  scale(1.02); }
  }
}
/* The caption under the card: category name, one line, one way in. The kicker,
   the product name and the two-button row are gone with the product-led slide —
   this sells the CATEGORY, and a second button would only compete with the one
   that matters. */
.sl__txt {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  max-inline-size: 34rem;
}
.sl__txt > * { opacity: 0; transform: translateY(16px); }
.sl.is-on .sl__txt > * { animation: lu-up 0.7s cubic-bezier(0.2, 0.8, 0.2, 1) forwards; }
.sl.is-on .sl__txt > *:nth-child(1) { animation-delay: 0.10s; }
.sl.is-on .sl__txt > *:nth-child(2) { animation-delay: 0.18s; }
.sl.is-on .sl__txt > *:nth-child(3) { animation-delay: 0.26s; }
@keyframes lu-up { to { opacity: 1; transform: none; } }

.sl__h { font-size: clamp(1.5rem, 3.6vw, 2.4rem); margin: 0 0 0.3rem; }
.sl__p { color: #B3A8C2; margin: 0 0 0.9rem; font-size: var(--fs-small); }

/* ---- controls ----
   Arrows only. The dot row is gone: its fill animation was a progress hint, and
   that decoration was costing eight extra controls in the tab order in front of
   everything else on the page. */
/* The vertical middle, at the reader's request, which reverses an earlier call
   recorded here: they were moved to the bottom corners because centred on the
   sides they sat over the photograph and the feathered card edges made the
   overlap read as a smudge. That reason was real, so the move back answers it
   rather than ignoring it — the plate below is opaque enough to be a control
   on top of a picture instead of a haze over it, and 10px from the edge keeps
   them off the front card, over the faded outer pair of the fan. */
.sl__arw {
  /* transform, not the newer `translate` property: these pages are opened on
     whatever Android phone the customer owns. */
  position: absolute; inset-block-start: 50%; transform: translateY(-50%);
  /* var(--tap), not 40px. These are thumb targets on a phone and every other
     square control on the site is held to 44 by the rule near the bottom of
     this file; these were the exception nobody had measured. */
  /* ABOVE THE DECK. This was z-index 3 and the arrows were dead on desktop —
     not visually missing, not unbound, just unclickable: elementFromPoint at
     the centre of the next arrow returned a .sl, and calling .click() on the
     button directly advanced the slide perfectly.
     The coverflow ranks the panes 5 for the front card and 4 for its
     neighbours so they stack correctly against each other, and .hero__panes is
     positioned with z-index auto — so it creates NO stacking context and those
     panes are ranked against the arrows, which are its siblings, rather than
     kept inside it. A pane covers the whole stage, corners included, so every
     click on an arrow landed on a card.
     Below 900px none of the coverflow applies, the panes have no z-index at
     all and 3 was enough — which is exactly why this was desktop-only and
     survived the phone fix. 6 outranks the front card; if the deck is ever
     re-ranked, this has to stay above it (tests/test_release.py checks). */
  z-index: 6; inline-size: var(--tap); block-size: var(--tap); border-radius: 50%;
  display: grid; place-items: center; font-size: 1.3rem; line-height: 1;
  /* Opaque enough to read as a control sitting ON the picture: over artwork a
     half-transparent disc is the smudge the old comment described. */
  border: 1px solid rgba(255, 255, 255, 0.22); background: rgba(10, 6, 18, 0.72);
  backdrop-filter: blur(8px); color: var(--chalk); cursor: pointer;
  box-shadow: 0 6px 20px -8px #000;
  transition: background var(--fast) var(--ease), border-color var(--fast) var(--ease);
}
.sl__arw:hover { background: var(--core); border-color: transparent; }
.sl__arw--next { inset-inline-start: 10px; }
.sl__arw--prev { inset-inline-end: 10px; }

@media (prefers-reduced-motion: reduce) {
  .sl { transition: none; }
  .sl.is-on .sl__art img { animation: none; }
  .sl.is-on .sl__txt > * { animation: none; opacity: 1; transform: none; }
}

/* ---- hero slide in light theme ------------------------------------------
   The slide was pinned dark in both themes because the tints were chosen to
   glow against a near-black stage. That left a dark band across the top of an
   otherwise light page. It now follows the theme: the same per-slide tint, but
   laid over white at roughly a third of the strength, since a wash that reads
   as "a hint of colour" on black reads as "muddy" on white. Every colour that
   was pinned for the dark stage is handed back to the tokens here. */
:root[data-theme="light"] .sl {
  color: var(--chalk);
  background:
    radial-gradient(90% 120% at 12% 50%, color-mix(in srgb, var(--c1) 20%, transparent), transparent 64%),
    radial-gradient(70% 90% at 95% 10%, color-mix(in srgb, var(--c2) 14%, transparent), transparent 62%),
    linear-gradient(105deg, #FFFFFF 10%, #F7F3FC 58%, #EFE9F7 100%);
}
:root[data-theme="light"] .sl__p { color: var(--dim); }
/* The kicker was missed when the light slide was derived. It kept the pale
   lilac pinned for the dark stage (#C6B8DA) and sat on a near-white gradient
   at roughly 1.4:1 — «تجميعات Level UP» was invisible on the light theme.
   Same correction as .sl__p above, which is the line that should have had a
   neighbour from the start. */
:root[data-theme="light"] .sl__kick { color: var(--dim); }

/* The card plate (#F0EEF5) and the light slide behind it (#F7F3FC) are within a
   couple of percent of each other, so on this theme the shadow is the only
   thing giving the card an edge. The dark stage's heavy shadow reads as a
   smudge on white, so it is swapped for a lighter one plus a real hairline. */
/* LIGHT: the blend. Here the plate and the page are within a few percent of
   each other, so pooling the photo's own colour behind it and feathering the
   edge genuinely dissolves the rectangle — the product ends up floating on the
   page with no container at all. Everything the dark card draws (plate,
   radius, padding, shadow, hairline) is taken back off. */
:root[data-theme="light"] .sl__art::before { display: block; }
:root[data-theme="light"] .sl__art img {
  background: none;
  border-radius: 0;
  padding: 0;
  box-shadow: none;
  -webkit-mask-image:
    linear-gradient(to right, transparent 0%, #000 7%, #000 93%, transparent 100%),
    linear-gradient(to bottom, transparent 0%, #000 6%, #000 94%, transparent 100%);
          mask-image:
    linear-gradient(to right, transparent 0%, #000 7%, #000 93%, transparent 100%),
    linear-gradient(to bottom, transparent 0%, #000 6%, #000 94%, transparent 100%);
  -webkit-mask-composite: source-in;
          mask-composite: intersect;
}

:root[data-theme="light"] .sl__arw {
  /* Raised with the dark plate above, and for the same reason: centred on the
     sides these now sit over the artwork rather than in an empty corner. */
  background: rgba(255, 255, 255, 0.86);
  border-color: var(--hair2);
  color: var(--chalk);
}
:root[data-theme="light"] .sl__arw:hover { background: var(--core); color: #fff; border-color: transparent; }

/* =========================================================================
   THE BROWSE CARD — the homepage's one way in
   -------------------------------------------------------------------------
   No products on the homepage. One lit plate under the hero: the store's
   name at signage size ghosted behind, three measured numbers, one button.
   The whole plate is the link, so on a phone it is one tap target.
   ========================================================================= */
.browse {
  position: relative;
  display: block;
  overflow: hidden;
  min-block-size: clamp(15rem, 34vw, 22rem);
  padding: clamp(1.5rem, 4vw, 3rem);
  border-radius: var(--radius-lg);
  background: var(--lift);
  border: 1px solid var(--hair);
  color: inherit;
  text-decoration: none;
  transition: border-color var(--fast) var(--ease), transform 0.3s var(--ease),
              background var(--fast) var(--ease);
}
.browse:hover, .browse:focus-visible {
  background: var(--lift2);
  border-color: color-mix(in srgb, var(--led) 45%, var(--hair2));
  transform: translateY(-3px);
}
.browse:focus-visible { outline: 2px solid var(--lilac); outline-offset: 3px; }
.browse:hover .led--edge, .browse:focus-visible .led--edge {
  opacity: 1;
  box-shadow: 0 0 18px 2px color-mix(in srgb, var(--led) 70%, transparent);
}
/* The signage word: the door device, at door scale. */
.browse__sign {
  position: absolute;
  inset-block-start: -0.16em;
  inset-inline-end: -0.04em;
  font-family: var(--font-display);
  font-size: var(--fs-sign);
  font-weight: 700;
  line-height: 1;
  white-space: nowrap;
  color: var(--chalk);
  opacity: 0.06;
  pointer-events: none;
  user-select: none;
  transition: opacity 0.3s var(--ease), transform 0.3s var(--ease);
}
.browse:hover .browse__sign, .browse:focus-visible .browse__sign { opacity: 0.11; transform: translateX(-0.02em); }
.browse__body {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--s2);
  max-inline-size: 36rem;
  block-size: 100%;
  justify-content: flex-end;
}
.browse__h {
  font-family: var(--font-display);
  font-size: var(--fs-h1);
  line-height: 1.15;
  margin: 0;
}
.browse__p { margin: 0; color: var(--dim); max-inline-size: 40ch; }
.browse__cta { display: inline-flex; align-items: center; gap: 0.45rem; margin-block-start: var(--s1); }
[dir="rtl"] .browse__cta svg { transform: scaleX(-1); }

@media (prefers-reduced-motion: reduce) {
  .browse, .browse__sign { transition: none; }
  .browse:hover, .browse:focus-visible { transform: none; }
}

/* =========================================================================
   THE MAP — navigation
   -------------------------------------------------------------------------
   Reaching a category used to mean landing on the catalogue and scrolling a
   rail, or opening the filter sheet. Now the whole taxonomy is one panel in
   every page's chrome: a dropdown under the header on a desktop, a bottom
   sheet on a phone, the same markup. On a phone a fixed tab bar puts home,
   the map, search and the request list in the thumb zone; the catalogue's
   filter button floats above that bar once it has scrolled away.
   ========================================================================= */

/* ---- the header trigger (desktop) ------------------------------------- */
.nav__map {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  font-size: var(--fs-small);
  cursor: pointer;
  transition: color var(--fast) var(--ease);
}
.nav__map svg { transition: transform var(--fast) var(--ease); }
.nav__map:hover, .nav__map[aria-expanded="true"] { color: var(--chalk); }
.nav__map[aria-expanded="true"] svg { transform: rotate(180deg); }
.nav__map:focus-visible { outline: 2px solid var(--lilac); outline-offset: 3px; border-radius: 4px; }

/* ---- the panel ---------------------------------------------------------- */
.catmap {
  position: fixed;
  z-index: 55;
  background: var(--deep);
  border: 1px solid var(--hair2);
  color: var(--chalk);
  visibility: hidden;
  opacity: 0;
  transition: opacity var(--fast) var(--ease), transform 0.3s var(--ease),
              visibility 0s linear 0.3s;
}
.catmap[data-open="true"] {
  visibility: visible;
  opacity: 1;
  transition: opacity var(--fast) var(--ease), transform 0.3s var(--ease), visibility 0s;
}
.catmap__scrim { position: fixed; inset: 0; z-index: 54; background: rgba(5, 4, 8, 0.55); }
.catmap__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s2);
  margin-block-end: var(--s2);
}
.catmap__title { margin: 0; font-family: var(--font-display); font-size: var(--fs-h3); font-weight: 600; }
.catmap__x {
  inline-size: var(--tap);
  block-size: var(--tap);
  border-radius: 50%;
  border: 1px solid var(--hair2);
  background: var(--lift);
  color: var(--chalk);
  display: grid;
  place-items: center;
}
.catmap__all, .catmap__sec, .catmap__list a {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s1);
  min-block-size: var(--tap);
  padding: 0.5rem 0.8rem;
  border-radius: var(--radius);
  background: var(--lift);
  border: 1px solid var(--hair);
  color: var(--chalk);
  text-decoration: none;
  transition: border-color var(--fast) var(--ease), background var(--fast) var(--ease);
}
.catmap__all:hover, .catmap__list a:hover {
  background: var(--lift2);
  border-color: color-mix(in srgb, var(--lilac) 45%, var(--hair2));
}
.catmap__all { margin-block-end: var(--s2); font-weight: 600; }
.catmap__sec {
  font-family: var(--font-display);
  font-weight: 600;
  background: transparent;
  border-color: transparent;
  padding-inline: 0.2rem;
}
.catmap__sec:hover { color: var(--lilac); }
.catmap__group + .catmap__group { margin-block-start: var(--s2); }
.catmap__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 6px;
}
.catmap__list a { font-size: var(--fs-small); }
.catmap a:focus-visible, .catmap__x:focus-visible { outline: 2px solid var(--lilac); outline-offset: 2px; }

@media (min-width: 861px) {
  /* A panel hung from the header's real bottom edge (set by the JS on open),
     centred physically — left/translate rather than the logical properties,
     which would push it the wrong way in RTL. */
  .catmap {
    inset-block-start: var(--catmap-top, 110px);
    left: 50%;
    transform: translate(-50%, -8px);
    inline-size: min(calc(100% - 2 * var(--gutter)), 64rem);
    max-block-size: calc(100vh - var(--catmap-top, 110px) - 1rem);
    overflow: auto;
    padding: var(--s3);
    border-block-start: 0;
    border-radius: 0 0 var(--radius-lg) var(--radius-lg);
    box-shadow: 0 30px 60px -30px #000;
  }
  .catmap[data-open="true"] { transform: translate(-50%, 0); }
  .catmap__groups { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--s3); }
  .catmap__group + .catmap__group { margin: 0; }
  .catmap__x { display: none; }
  .catmap__head { margin-block-end: var(--s1); }
  .catmap__scrim { background: rgba(5, 4, 8, 0.35); }
  .tabbar { display: none; }
}

@media (max-width: 860px) {
  .nav__map { display: none; }

  /* the sheet */
  .catmap {
    inset-inline: 0;
    inset-block-end: 0;
    max-block-size: 82vh;
    overflow: auto;
    padding: var(--s2) var(--s2) calc(var(--s3) + env(safe-area-inset-bottom));
    border-block-end: 0;
    border-radius: var(--radius-lg) var(--radius-lg) 0 0;
    transform: translateY(100%);
  }
  .catmap[data-open="true"] { transform: none; }
  .catmap::before {
    content: "";
    display: block;
    inline-size: 40px;
    block-size: 4px;
    margin: 0 auto var(--s2);
    border-radius: 2px;
    background: var(--hair2);
  }
  body.is-mapping { overflow: hidden; }

  /* the tab bar */
  body { padding-block-end: calc(58px + env(safe-area-inset-bottom)); }
  .tabbar {
    position: fixed;
    inset-inline: 0;
    inset-block-end: 0;
    z-index: 45;
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    padding-block-end: env(safe-area-inset-bottom);
    background: color-mix(in srgb, var(--void) 92%, transparent);
    backdrop-filter: blur(14px);
    border-block-start: 1px solid var(--hair2);
  }
  .tabbar__it {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 3px;
    min-block-size: 58px;
    padding: 6px 4px;
    border: 0;
    background: none;
    color: var(--dim);
    font: inherit;
    font-size: var(--fs-micro);
    text-decoration: none;
    cursor: pointer;
    transition: color var(--fast) var(--ease);
  }
  .tabbar__it svg { inline-size: 22px; block-size: 22px; }
  .tabbar__it:hover, .tabbar__it[aria-current="page"],
  .tabbar__it[aria-expanded="true"] { color: var(--lilac); }
  /* The LED bar over the current tab, and over the map while it is open. */
  .tabbar__it::before {
    content: "";
    position: absolute;
    inset-block-start: 0;
    left: 50%;
    inline-size: 28px;
    block-size: 3px;
    border-radius: 0 0 3px 3px;
    background: var(--led-hot);
    box-shadow: 0 0 10px 1px color-mix(in srgb, var(--led-hot) 70%, transparent);
    transform: translateX(-50%) scaleX(0);
    transition: transform var(--fast) var(--ease);
  }
  .tabbar__it[aria-current="page"]::before,
  .tabbar__it[aria-expanded="true"]::before { transform: translateX(-50%) scaleX(1); }
  .tabbar__it:focus-visible { outline: 2px solid var(--lilac); outline-offset: -3px; border-radius: var(--radius); }
  .tabbar__n {
    position: absolute;
    inset-block-start: 5px;
    left: calc(50% + 4px);
    min-inline-size: 18px;
    block-size: 18px;
    padding: 0 5px;
    border-radius: 999px;
    background: var(--lilac);
    color: var(--void);
    font-family: var(--font-mono);
    font-size: 11px;
    font-weight: 600;
    line-height: 18px;
    text-align: center;
  }
  .tabbar__n[data-empty="true"] { display: none; }

}

@media (prefers-reduced-motion: reduce) {
  .catmap, .nav__map svg, .tabbar__it, .tabbar__it::before { transition: none; }
}

/* =========================================================================
   THE CATALOGUE — a filter sidebar beside one grid
   -------------------------------------------------------------------------
   Owner, 2026-10-29: filter the way pcmaster.ly/shop does. Its FUNCTION, in
   this site's own materials: the sidebar is a solid case panel (nothing is
   behind it, so no glass), a choice is lit the way a chosen thing is lit
   everywhere here — violet edge, LED glow — and a ticked box is a small LED
   that comes on, not a tick mark.

   1024px and up: the sidebar sits on the reading side, sticky under the
   header, scrolling on its own when it is taller than the window. Below
   that, the same element is a bottom sheet behind the «الفلترة» button, and
   the categories are also a row of chips you swipe.
   ========================================================================= */

/* ---- layout -------------------------------------------------------------- */
@media (min-width: 1024px) {
  .catalog__layout {
    display: grid;
    grid-template-columns: minmax(15rem, 17.5rem) minmax(0, 1fr);
    gap: var(--s3);
    align-items: start;
  }
}
/* Beside the sidebar a 1024px window leaves the grid ~650px: three columns
   there are 200px cards with the name wrapped into the price. Two until the
   room for three is real. */
@media (min-width: 1024px) and (max-width: 1279px) {
  .catalog__main .grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* ---- the sidebar --------------------------------------------------------- */
.filters {
  position: sticky;
  inset-block-start: calc(var(--head-h, 72px) + var(--s2));
  max-block-size: calc(100vh - var(--head-h, 72px) - var(--s2) * 2);
  display: flex;
  flex-direction: column;
  background: var(--lift);
  border: 1px solid var(--hair);
  border-radius: var(--radius-lg);
  color: var(--chalk);
}
.filters__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s1);
  padding: var(--s2) var(--s2) 0;
}
.filters__title {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--fs-h3);
  font-weight: 600;
  line-height: 1.35;
}
.filters__x { display: none; }
.filters__body {
  flex: 1 1 auto;
  min-block-size: 0;
  overflow: auto;
  overscroll-behavior: contain;
  padding: 0 var(--s2);
  scrollbar-width: thin;
}
.filters__foot {
  display: flex;
  gap: var(--s1);
  padding: var(--s2);
  border-block-start: 1px solid var(--hair);
}
.filters__foot .btn { flex: 1; justify-content: center; }
.filters__foot .btn[disabled] { opacity: 0.45; cursor: default; }
/* On a desktop the results are already beside it; there is nothing to show. */
.filters__done { display: none; }
.filters__scrim { display: none; }

/* ---- a group ------------------------------------------------------------- */
.fgroup { padding-block: var(--s2); }
.fgroup + .fgroup { border-block-start: 1px solid var(--hair); }
.fgroup--search { padding-block-start: var(--s1); }
.fgroup__h {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin: 0 0 var(--s1);
  font-family: var(--font-display);
  font-size: 1rem;
  font-weight: 600;
  line-height: 1.4;
  color: var(--chalk);
}
/* The heading's LED: a short lit segment where the line starts, the same
   strip that runs along every plate, at the size of a label. */
.fgroup__h .led--edge {
  position: static;
  flex: none;
  inline-size: 1rem;
  opacity: 1;
}

.fsearch input,
.fprice input {
  inline-size: 100%;
  min-block-size: var(--tap);
  padding: 0 0.75rem;
  border: 1px solid var(--hair2);
  border-radius: var(--radius);
  background: var(--deep);
  color: var(--chalk);
  font: inherit;
  font-size: var(--fs-small);
  transition: border-color var(--fast) var(--ease);
}
.fsearch input::placeholder, .fprice input::placeholder { color: var(--dim); opacity: 1; }
.fsearch input:focus-visible, .fprice input:focus-visible {
  outline: 2px solid var(--lilac);
  outline-offset: 1px;
  border-color: transparent;
}

/* ---- الأقسام ----------------------------------------------------------- */
.ftree { display: grid; gap: 2px; }
.fcats {
  list-style: none;
  margin: 0;
  padding: 0;
  padding-inline-start: 0.5rem;
  display: grid;
  gap: 2px;
}
.fcat {
  position: relative;
  display: flex;
  align-items: center;
  gap: 0.65rem;
  inline-size: 100%;
  min-block-size: var(--tap);
  padding: 4px 8px;
  border: 1px solid transparent;
  border-radius: var(--radius);
  background: none;
  color: var(--dim);
  font: inherit;
  font-size: var(--fs-small);
  line-height: 1.35;
  text-align: start;
  cursor: pointer;
  transition: color var(--fast) var(--ease), background var(--fast) var(--ease),
              border-color var(--fast) var(--ease);
}
.fcat:hover { color: var(--chalk); background: var(--lift2); }
.fcat:focus-visible { outline: 2px solid var(--lilac); outline-offset: -2px; }
.fcat--all, .fcat--sec {
  font-family: var(--font-display);
  font-weight: 600;
  color: var(--chalk);
}
.fcat--sec { margin-block-start: var(--s1); }
.fcat[aria-pressed="true"] {
  color: var(--chalk);
  background: var(--lift2);
  border-color: color-mix(in srgb, var(--led) 45%, var(--hair2));
}
.fcat[aria-pressed="true"]::before {
  content: "";
  position: absolute;
  inset-inline-start: -1px;
  inset-block: 9px;
  inline-size: 3px;
  border-radius: 3px;
  background: var(--led-hot);
  box-shadow: 0 0 10px 1px color-mix(in srgb, var(--led-hot) 70%, transparent);
}

/* The category's own picture, at list size, on the light plate every
   photograph is flattened onto — or its first letter on a dark chip. */
.fface {
  flex: none;
  display: grid;
  place-items: center;
  inline-size: 36px;
  block-size: 36px;
  border-radius: var(--radius);
  background: var(--plate);
  overflow: hidden;
}
.fface img {
  inline-size: 100%;
  block-size: 100%;
  object-fit: contain;
  padding: 3px;
  mix-blend-mode: multiply;
}
/* A category picture he framed carries --photo-* (catalogpage only writes the
   style then); it sits on the plate the way a product card's photo does.
   Physical left/top: a position must not flip in RTL. */
.fface { position: relative; }
.fface img[style] {
  position: absolute;
  width: var(--photo-size, 94%);
  height: var(--photo-size, 94%);
  left: var(--photo-left, 3%);
  top: var(--photo-top, 3%);
  object-position: var(--photo-position, 50% 50%);
  padding: 0;
}
.fface--type {
  background: var(--deep);
  border: 1px solid var(--hair2);
  color: var(--dim);
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 0.95rem;
  line-height: 1;
}

/* ---- options: brand, stock, sale ---------------------------------------- */
.fopts { list-style: none; margin: 0; padding: 0; display: grid; gap: 2px; }
.fopt {
  position: relative;
  display: flex;
  align-items: center;
  gap: 0.65rem;
  min-block-size: var(--tap);
  padding: 0 8px;
  border-radius: var(--radius);
  color: var(--chalk);
  font-size: var(--fs-small);
  cursor: pointer;
  transition: background var(--fast) var(--ease), opacity var(--fast) var(--ease);
}
.fopt:hover { background: var(--lift2); }
.fopt input {
  position: absolute;
  inset-inline-start: 8px;
  inline-size: 20px;
  block-size: 20px;
  margin: 0;
  opacity: 0;
}
.fopt__box {
  flex: none;
  display: grid;
  place-items: center;
  inline-size: 20px;
  block-size: 20px;
  border: 1.5px solid var(--hair2);
  border-radius: 6px;
  background: var(--deep);
  transition: border-color var(--fast) var(--ease);
}
/* On = an LED coming on inside the box. */
.fopt__box::after {
  content: "";
  inline-size: 10px;
  block-size: 10px;
  border-radius: 3px;
  background: var(--led-hot);
  box-shadow: 0 0 8px 1px color-mix(in srgb, var(--led-hot) 70%, transparent);
  transform: scale(0);
  transition: transform var(--fast) var(--ease);
}
.fopt input:checked + .fopt__box { border-color: var(--lilac); }
.fopt input:checked + .fopt__box::after { transform: scale(1); }
.fopt input:focus-visible + .fopt__box { outline: 2px solid var(--lilac); outline-offset: 2px; }
.fopt__t { min-inline-size: 0; }
/* Would find nothing under the other filters: still there, still in place,
   plainly not available. */
.is-dead .fopt { opacity: 0.4; cursor: default; }
.is-dead .fopt:hover { background: none; }

.fmore {
  margin-block-start: 4px;
  padding: 0.5rem 8px;
  min-block-size: var(--tap);
  border: 0;
  background: none;
  color: var(--lilac);
  font: inherit;
  font-size: var(--fs-small);
  font-weight: 600;
  cursor: pointer;
}
.fmore:hover { text-decoration: underline; }
.fmore:focus-visible { outline: 2px solid var(--lilac); outline-offset: 2px; border-radius: var(--radius); }

/* ---- price --------------------------------------------------------------
   Two range inputs on one track. Each input is click-through; only its
   handle takes the pointer, so both handles can be reached. The track runs
   from the reading side, like the page. */
.frange { position: relative; block-size: 32px; margin-block: 0.25rem var(--s1); }
.frange::before,
.frange__fill {
  content: "";
  position: absolute;
  inset-block-start: 50%;
  block-size: 4px;
  margin-block-start: -2px;
  border-radius: 2px;
}
.frange::before { inset-inline: 0; background: var(--hair2); }
.frange__fill {
  inset-inline: 0;
  background: var(--led-hot);
  box-shadow: 0 0 10px 1px color-mix(in srgb, var(--led-hot) 55%, transparent);
}
.frange input {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  margin: 0;
  background: none;
  pointer-events: none;
  -webkit-appearance: none;
  appearance: none;
}
.frange input::-webkit-slider-runnable-track { block-size: 32px; background: transparent; }
.frange input::-moz-range-track { background: transparent; }
.frange input::-webkit-slider-thumb {
  -webkit-appearance: none;
  pointer-events: auto;
  inline-size: 24px;
  block-size: 24px;
  margin-block-start: 4px;
  border: 2px solid var(--lilac);
  border-radius: 50%;
  background: var(--chalk);
  box-shadow: 0 0 12px -1px color-mix(in srgb, var(--led-hot) 65%, transparent);
  cursor: grab;
}
.frange input::-moz-range-thumb {
  pointer-events: auto;
  inline-size: 20px;
  block-size: 20px;
  border: 2px solid var(--lilac);
  border-radius: 50%;
  background: var(--chalk);
  box-shadow: 0 0 12px -1px color-mix(in srgb, var(--led-hot) 65%, transparent);
  cursor: grab;
}
.frange input:focus-visible::-webkit-slider-thumb { outline: 2px solid var(--lilac); outline-offset: 2px; }
.frange input:focus-visible::-moz-range-thumb { outline: 2px solid var(--lilac); outline-offset: 2px; }
.frange input:disabled::-webkit-slider-thumb { opacity: 0.4; cursor: default; }

.fprice { display: flex; align-items: flex-end; gap: 0.5rem; }
.fprice label { flex: 1; display: grid; gap: 4px; min-inline-size: 0; font-size: var(--fs-micro); color: var(--dim); }
.fprice input { font-family: var(--font-mono); text-align: center; -moz-appearance: textfield; }
.fprice input::-webkit-outer-spin-button,
.fprice input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.fprice small { padding-block-end: 0.75rem; color: var(--dim); font-size: var(--fs-micro); }
.fnote { margin: var(--s1) 0 0; color: var(--dim); font-size: var(--fs-micro); line-height: 1.6; }

/* ---- the order -------------------------------------------------------- */
.sortsw { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
.sortsw__k { color: var(--dim); font-size: var(--fs-small); margin-inline-end: 4px; }
.sortsw__b {
  min-block-size: var(--tap);
  padding: 0 1rem;
  border: 1px solid var(--hair);
  border-radius: 999px;
  background: transparent;
  color: var(--dim);
  font: inherit;
  font-size: var(--fs-small);
  cursor: pointer;
  transition: color var(--fast) var(--ease), border-color var(--fast) var(--ease),
              background var(--fast) var(--ease);
}
.sortsw__b:hover { color: var(--chalk); border-color: var(--hair2); }
.sortsw__b:focus-visible { outline: 2px solid var(--lilac); outline-offset: 2px; }
.sortsw__b[aria-pressed="true"] {
  color: var(--chalk);
  background: var(--lift2);
  border-color: color-mix(in srgb, var(--lilac) 55%, transparent);
  box-shadow: 0 0 12px -2px color-mix(in srgb, var(--led-hot) 45%, transparent);
}
.catalog .toolbar { justify-content: space-between; }
.catalog .chips--active { gap: 0.4rem; }

/* ---- the grid's category headings -------------------------------------
   Only under «ترتيب المحل», which is the shop's order and so the only one
   that is still grouped. */
.ghead {
  grid-column: 1 / -1;
  position: relative;
  margin: var(--s3) 0 0;
  padding-block-start: var(--s2);
  font-family: var(--font-display);
  font-size: var(--fs-h3);
  font-weight: 600;
  line-height: 1.4;
  color: var(--chalk);
}
.grid > .ghead:first-child { margin-block-start: 0; }
.ghead .led--edge { inset-inline-end: auto; inline-size: 5rem; opacity: 1; }

/* ---- a phone: the chip row, the bar, the sheet ------------------------ */
.catrow {
  display: flex;
  gap: 0.5rem;
  margin-inline: calc(var(--gutter) * -1);
  padding: 0.25rem var(--gutter) 0.5rem;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-padding-inline: var(--gutter);
  scrollbar-width: none;
}
.catrow::-webkit-scrollbar { display: none; }
.catchip {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  min-block-size: var(--tap);
  padding-block: 3px;
  padding-inline: 3px 14px;
  border: 1px solid var(--hair);
  border-radius: 999px;
  background: var(--lift);
  color: var(--dim);
  font: inherit;
  font-size: var(--fs-small);
  white-space: nowrap;
  cursor: pointer;
  transition: color var(--fast) var(--ease), border-color var(--fast) var(--ease),
              background var(--fast) var(--ease);
}
.catchip:first-child { padding-inline: 16px; }
.catchip .fface { inline-size: 36px; block-size: 36px; border-radius: 50%; }
.catchip:focus-visible { outline: 2px solid var(--lilac); outline-offset: 2px; }
.catchip[aria-pressed="true"] {
  color: var(--chalk);
  background: var(--lift2);
  border-color: color-mix(in srgb, var(--led) 55%, var(--hair2));
  box-shadow: 0 0 14px -4px color-mix(in srgb, var(--led-hot) 60%, transparent);
}

/* The two buttons that stand in for the sidebar, held under the header so
   the filters are one tap away from anywhere in a long grid. The page
   passes under them, so they are a pane, like the header. */
.fbar {
  position: sticky;
  inset-block-start: var(--head-h, 64px);
  z-index: 30;
  display: flex;
  gap: 0.5rem;
  margin-inline: calc(var(--gutter) * -1);
  margin-block-end: var(--s1);
  padding: 0.5rem var(--gutter);
  background: color-mix(in srgb, var(--void) 90%, transparent);
  backdrop-filter: blur(12px);
}
.fbar__b,
.fbar__sort select {
  min-block-size: var(--tap);
  border: 1px solid var(--hair2);
  border-radius: 999px;
  background: var(--lift);
  color: var(--chalk);
  font: inherit;
  font-size: var(--fs-small);
  font-weight: 600;
  cursor: pointer;
}
.fbar__b {
  flex: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
}
.fbar__b:focus-visible, .fbar__sort select:focus-visible { outline: 2px solid var(--lilac); outline-offset: 2px; }
.fbar__n {
  min-inline-size: 20px;
  block-size: 20px;
  padding: 0 6px;
  border-radius: 999px;
  background: var(--lilac);
  color: var(--void);
  font-size: 12px;
  font-weight: 600;
  line-height: 20px;
  text-align: center;
}
.fbar__sort { position: relative; flex: 1; display: flex; }
.fbar__sort select {
  inline-size: 100%;
  padding-inline: 1rem 2.4rem;
  -webkit-appearance: none;
  appearance: none;
  text-align: center;
}
/* A chevron drawn with two borders: no image, so nothing for the CSP to
   refuse. */
.fbar__sort::after {
  content: "";
  position: absolute;
  inset-inline-end: 1.1rem;
  inset-block-start: 50%;
  inline-size: 7px;
  block-size: 7px;
  margin-block-start: -6px;
  border: solid var(--dim);
  border-width: 0 2px 2px 0;
  transform: rotate(45deg);
  pointer-events: none;
}

/* Last, so it beats the display these two are given above. */
@media (min-width: 1024px) {
  .catrow, .fbar { display: none; }
}

@media (max-width: 1023px) {
  .catalog .toolbar { padding: 0; border: 0; margin-block-end: var(--s1); }
  .sortsw { display: none; }

  /* the sheet */
  .filters {
    position: fixed;
    z-index: 57;
    inset-inline: 0;
    inset-block: auto 0;
    max-block-size: 88vh;
    max-block-size: 88dvh;
    border: 1px solid var(--hair2);
    border-block-end: 0;
    border-radius: var(--radius-lg) var(--radius-lg) 0 0;
    background: var(--deep);
    box-shadow: 0 -30px 60px -30px #000;
    visibility: hidden;
    transform: translateY(100%);
    transition: transform 0.3s var(--ease), visibility 0s linear 0.3s;
  }
  .filters[data-open="true"] {
    visibility: visible;
    transform: none;
    transition: transform 0.3s var(--ease), visibility 0s;
  }
  .filters__head { position: relative; padding-block-start: calc(var(--s2) + 6px); }
  .filters__head::before {
    content: "";
    position: absolute;
    inset-block-start: 8px;
    left: 50%;
    inline-size: 40px;
    block-size: 4px;
    margin-left: -20px;
    border-radius: 2px;
    background: var(--hair2);
  }
  .filters__x {
    display: grid;
    place-items: center;
    inline-size: var(--tap);
    block-size: var(--tap);
    border: 1px solid var(--hair2);
    border-radius: 50%;
    background: var(--lift);
    color: var(--chalk);
    cursor: pointer;
  }
  .filters__x:focus-visible { outline: 2px solid var(--lilac); outline-offset: 2px; }
  .filters__done { display: inline-flex; }
  .filters__foot { padding-block-end: calc(var(--s2) + env(safe-area-inset-bottom)); }
  .filters__scrim {
    display: block;
    position: fixed;
    inset: 0;
    z-index: 56;
    background: rgba(5, 4, 8, 0.55);
  }
  body.is-filtering { overflow: hidden; }
}

@media (prefers-reduced-motion: reduce) {
  .filters, .filters[data-open="true"], .fcat, .fopt, .fopt__box, .fopt__box::after,
  .sortsw__b, .catchip { transition: none; }
}

/* ---- the browse card's picture -------------------------------------------
   The owner's still of the shop's kind of hardware, matted to transparency,
   standing on the plate's floor at the card's far end with the copy at the
   near end. It bleeds to the bottom edge so it reads as sitting ON the card,
   not framed inside it. On a phone it stacks above the copy. */
.browse {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.15fr);
  align-items: end;
  gap: var(--s3);
  min-block-size: clamp(17rem, 36vw, 25rem);
}
.browse__body { grid-column: 1; grid-row: 1; }
.browse__art {
  grid-column: 2;
  grid-row: 1;
  position: relative;
  display: block;
  align-self: end;
  margin-block-end: calc(-1 * clamp(1.5rem, 4vw, 3rem));   /* to the card's floor */
  margin-inline-end: calc(-0.5 * clamp(1.5rem, 4vw, 3rem));
}
.browse__art img {
  display: block;
  inline-size: 100%;
  block-size: auto;
  max-block-size: 28rem;
  object-fit: contain;
  object-position: bottom;
  filter: drop-shadow(0 26px 34px rgba(0, 0, 0, 0.55));
  transition: transform 0.6s var(--ease);
}
.browse:hover .browse__art img, .browse:focus-visible .browse__art img { transform: translateY(-4px); }
@media (max-width: 720px) {
  .browse { grid-template-columns: 1fr; align-items: start; gap: var(--s2); min-block-size: 0; }
  .browse__art { grid-column: 1; grid-row: 1; margin: calc(-0.5 * clamp(1.5rem, 4vw, 3rem)) 0 0; }
  .browse__art img { max-block-size: 15rem; margin-inline: auto; object-position: center; }
  .browse__body { grid-row: 2; }
}
@media (prefers-reduced-motion: reduce) {
  .browse__art img { transition: none; }
  .browse:hover .browse__art img, .browse:focus-visible .browse__art img { transform: none; }
}

/* =========================================================================
   COVERFLOW — the hero as a deck, desktop only
   -------------------------------------------------------------------------
   One category face-on in the middle, the next angled in behind it on the
   right, the previous behind on the left. Three cards, not a deep stack:
   only six categories have a photograph, so a longer fan would repeat
   itself, and the shallow version keeps the centre card big enough to read.

   Desktop only, deliberately. At 375px the side cards would eat width the
   phone has not got, so below 900px nothing here applies and the hero stays
   the single card it already was.

   Direction: the next card sits on the RIGHT in both writing directions.
   That is not an oversight about RTL — it is the convention the arrows and
   the swipe already settled on. Advancing moves the deck leftwards, so the
   card coming next enters from the right, and the "next" arrow sits on the
   inline start (the right, in Arabic) pointing left into the motion.
   ========================================================================= */

@media (min-width: 900px) {
  /* The stage owns the light now. It reads --c1/--c2 off whichever pane is in
     front, set by the slider, because a pane can no longer paint the whole
     stage without painting over its neighbours. */
  .hero__stage::before {
    content: '';
    position: absolute;
    inset: 0;
    z-index: 0;
    pointer-events: none;
    background:
      radial-gradient(90% 120% at 12% 50%, color-mix(in srgb, var(--c1, var(--core)) 55%, transparent), transparent 62%),
      radial-gradient(70% 90% at 95% 10%, color-mix(in srgb, var(--c2, var(--lilac)) 26%, transparent), transparent 60%),
      linear-gradient(105deg, #0B0614 12%, #140A1F 60%, #0A0512 100%);
  }

  /* Perspective per pane rather than on the stage: every pane is the same
     full-stage box, so the vanishing point lands in the same place for all of
     them, and nothing needs a preserve-3d chain to reach the card. */
  /* The whole deck follows the hand. One transform on the container rather
     than a drag term added to each card's place in the fan — see the slider's
     own note. It springs back when the hand comes up, while the cards
     re-arrange underneath on their own 0.55s. */
  .hero__panes {
    transform: translate3d(var(--drag, 0px), 0, 0);
    transition: transform 0.45s var(--ease);
  }
  .hero__stage.is-dragging .hero__panes { transition: none; }
  .hero__stage.is-dragging { cursor: grabbing; }

  /* One step for the whole deck, scaled to the stage rather than to any one
     photograph. 14vw holds the proportions the fan was drawn at (58% of a
     front card at 1280), and the clamp keeps it sane on a 2560 monitor and on
     a 900px laptop. */
  .hero__stage { --fan: clamp(9rem, 14vw, 18rem); }

  .sl {
    perspective: 1500px;
    background: none;
    opacity: 1;
    visibility: visible;
    transition: none;
  }
  /* Five cards on stage: the one in front, its neighbours, and the pair
     beyond them set further out, turned further away and fainter. Only the
     pane directly opposite — the wrap-around, |off| 3 of 6 — stays off, since
     it would arrive from whichever side you were not looking at. */
  .sl:not([data-off="0"]):not([data-off="1"]):not([data-off="-1"]):not([data-off="2"]):not([data-off="-2"]) {
    opacity: 0;
    visibility: hidden;
  }

  .sl__art {
    z-index: 1;
    /* Narrow enough that five of them fan out inside the stage instead of
       piling up. Measured at 1280: the outermost pair land just inside the
       frame rather than half off it. */
    max-inline-size: min(56%, 28rem);
    transition: transform 0.55s var(--ease), opacity 0.55s var(--ease);
  }
  /* THE STACKING GOES ON THE PANE, NOT THE CARD.
     Each .sl carries a perspective, and perspective creates a stacking
     context — so a z-index on the card inside it only orders that card
     against its own siblings, of which it has none. Across panes the order
     fell back to document order, which put the LAST pane over the first:
     measured with elementFromPoint at the dead centre of the front card, the
     topmost element there belonged to pane -1. Semi-transparent cards laid
     over the front one are exactly why it looked like the head of the deck
     had transparency it was never given. The panes are siblings, so ranking
     them works. */
  .sl[data-off="0"]  { z-index: 5; }
  .sl[data-off="1"],
  .sl[data-off="-1"] { z-index: 4; }
  .sl[data-off="2"],
  .sl[data-off="-2"] { z-index: 3; }

  /* Opacity still belongs to the card: the pane also holds the caption, and
     the front card must stay wholly opaque — the fade starts behind it. */
  .sl[data-off="0"] .sl__art { opacity: 1; }
  .sl[data-off="1"] .sl__art,
  .sl[data-off="-1"] .sl__art { opacity: 0.55; }
  .sl[data-off="2"] .sl__art,
  .sl[data-off="-2"] .sl__art { opacity: 0.28; }
  /* The fan steps by a FIXED LENGTH, not by a percentage of each card.
     --fan is set on the stage, so every card reads the same number.

     It was translateX(58%) and 104%, and a percentage in a transform resolves
     against the element's own border box. These cards are deliberately not the
     same width — .sl__art takes its aspect-ratio from the photograph, so a
     wide product card is wider than a tall one — which meant every card
     stepped by a different distance. Measured at 1280: centres 105px, 204px,
     153px and 261px apart, and the deck not even mirrored, because the card on
     the left of the front one happened to be a wider photograph than the card
     on its right. That is the "spacing mixed between each card".

     A length after scale() is not scaled — transform functions apply right to
     left, so the card is scaled and rotated first and then moved by exactly
     --fan in the stage's own coordinates. Which is the point: the step no
     longer knows or cares how wide the photograph is. */
  .sl[data-off="1"]  .sl__art { transform: translateX(var(--fan))  rotateY(-30deg) scale(0.74); }
  .sl[data-off="-1"] .sl__art { transform: translateX(calc(var(--fan) * -1)) rotateY(30deg)  scale(0.74); }
  .sl[data-off="2"]  .sl__art { transform: translateX(calc(var(--fan) * 1.8))  rotateY(-42deg) scale(0.56); }
  .sl[data-off="-2"] .sl__art { transform: translateX(calc(var(--fan) * -1.8)) rotateY(42deg)  scale(0.56); }
  /* The photograph's own resting tilt belonged to a hero showing one card.
     Here the depth comes from the arrangement, and a centre card that is also
     askew just reads as crooked. What replaces it is motion the reader
     causes: the cards follow the cursor, the front one further than the ones
     behind it, because that is how parallax works — the near thing sweeps
     past the far one. --px and --py are written by the slider, -1 to 1. */
  .sl__art img,
  .sl.is-on .sl__art img {
    transform: translate3d(calc(var(--px, 0) * var(--depth, 6px)),
                           calc(var(--py, 0) * var(--depth, 6px) * 0.6), 0);
    transition: transform 0.35s cubic-bezier(0.2, 0.8, 0.2, 1);
    animation: none;
  }
  /* Parallax by rank: the nearest card sweeps furthest, the ones behind it
     less, which is the whole trick. */
  .sl[data-off="0"] .sl__art img  { --depth: 12px; }
  .sl[data-off="1"] .sl__art img,
  .sl[data-off="-1"] .sl__art img { --depth: 6px; }
  .sl[data-off="2"] .sl__art img,
  .sl[data-off="-2"] .sl__art img { --depth: 3px; }

  /* And a slow drift under all of it, so the deck is never quite still. This
     uses the `translate` property rather than a transform, which is the whole
     reason it can run at the same time as the parallax above without one
     overwriting the other. */
  .sl[data-off="0"] .sl__art img {
    animation: lu-drift 6s ease-in-out infinite alternate;
  }
  @keyframes lu-drift {
    from { translate: 0 -6px; }
    to   { translate: 0 6px; }
  }

  /* The caption stays where it is and belongs to the centre alone. Hiding it
     rather than fading it also takes its button out of the tab order, which
     is the whole of the focus problem a visible neighbour would otherwise
     have introduced. */
  .sl:not([data-off="0"]) .sl__txt { visibility: hidden; opacity: 0; }

  /* THE LIGHT THEME GETS ITS CARD BACK, here only.
     On light the photograph deliberately dissolves into the page: no plate,
     no edge, no shadow, just a feathered cut-out. That is lovely for one
     picture and useless for three — measured, the neighbours at 62% opacity
     were invisible against the white, and the deck read as a single floating
     monitor. A deck needs objects to stack, so above 900px the light theme
     wears the same lit plate the dark one does. Below 900px, where a single
     card fills the stage, the dissolve is untouched. */
  /* The light theme's own pane background is written as
     :root[data-theme="light"] .sl, which outranks a bare .sl however late it
     comes — so `.sl { background: none }` above silently did nothing here and
     all six panes went on painting the whole stage. The last one in document
     order won, and the deck was hidden behind a single card's backdrop. The
     stage takes the light version of the wash too. */
  :root[data-theme="light"] .sl { background: none; }
  :root[data-theme="light"] .hero__stage::before {
    background:
      radial-gradient(90% 120% at 12% 50%, color-mix(in srgb, var(--c1, var(--core)) 20%, transparent), transparent 64%),
      radial-gradient(70% 90% at 95% 10%, color-mix(in srgb, var(--c2, var(--lilac)) 14%, transparent), transparent 62%),
      linear-gradient(105deg, #FFFFFF 10%, #F7F3FC 58%, #EFE9F7 100%);
  }

  :root[data-theme="light"] .sl__art::before { display: none; }
  :root[data-theme="light"] .sl__art img {
    background: var(--plate, #F0EEF5);
    border-radius: clamp(14px, 2vw, 22px);
    padding: clamp(0.7rem, 2.5vw, 1.8rem);
    box-shadow: 0 2px 6px rgba(20, 10, 35, 0.10),
                0 26px 44px -22px rgba(20, 10, 35, 0.40);
    -webkit-mask-image: none;
            mask-image: none;
  }
  /* Faded cards need more of themselves on a near-white page than on a black
     one: the same 0.28 that reads as "further back" on the dark stage simply
     disappears here. */
  :root[data-theme="light"] .sl[data-off="1"] .sl__art,
  :root[data-theme="light"] .sl[data-off="-1"] .sl__art { opacity: 0.72; }
  :root[data-theme="light"] .sl[data-off="2"] .sl__art,
  :root[data-theme="light"] .sl[data-off="-2"] .sl__art { opacity: 0.45; }
}

@media (prefers-reduced-motion: reduce) {
  .sl__art { transition: none; }
  /* No drift, no parallax. The slider does not write the properties either,
     so this is belt and braces for a stylesheet read on its own. */
  .sl__art img {
    animation: none;
    transition: none;
    translate: none;
    transform: none;
  }
}

/* =========================================================================
   THE STRIP — the hero on a phone, joined up
   -------------------------------------------------------------------------
   The panes stop being a stack of cross-fading layers and become one row you
   drag. Card and caption travel together under the finger, the browser does
   the scrolling, and scroll-snap parks the next one square.

   Why native scrolling rather than a transform the JS drags: momentum, rubber
   banding at the ends, the direction of a right-to-left row and the way a
   near-vertical flick should be left alone are all things the engine already
   knows and gets right on the device. The old code judged the gesture after
   the fact, on touchend, so nothing moved while the thumb did — the slide
   simply cross-faded once it was let go. That is what felt disconnected.
   ========================================================================= */

@media (max-width: 899px) {
  .hero__panes {
    position: static;
    display: flex;
    inline-size: 100%;
    block-size: 100%;
    overflow-x: auto;
    overflow-y: hidden;
    scroll-snap-type: x mandatory;
    overscroll-behavior-x: contain;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
  }
  .hero__panes::-webkit-scrollbar { display: none; }

  .sl {
    position: relative;
    inset: auto;
    flex: 0 0 100%;
    scroll-snap-align: center;
    /* Every pane is present and readable; none of them fades. */
    opacity: 1;
    visibility: visible;
    transition: none;
  }
  /* The arrows still work and still stay above the row. */
  .sl__arw { z-index: 4; }

  /* Every card on the strip carries its own caption, always. The rise-in
     animation belongs to a hero that shows one slide at a time: it leaves the
     caption at opacity 0 until its pane becomes the active one, which on a
     strip means every card you drag into view arrives untitled and only names
     itself once the scroll settles. Seen on a phone, that reads as broken. */
  .sl__txt > *,
  .sl.is-on .sl__txt > * { opacity: 1; transform: none; animation: none; }

  /* The photograph slides inside its card as the strip moves — the frame and
     its contents travelling at different rates, which is what gives a flat
     picture some depth behind the glass. --slide is written per pane by the
     slider from the scroll position, so it tracks the thumb and stops dead
     when the thumb does. `translate` again, so it composes with the card's
     own resting tilt instead of fighting it. */
  .sl__art img { translate: var(--slide, 0px) 0; }
}

@media (prefers-reduced-motion: reduce) {
  /* Snapping is a jump, not an animation, and it is what keeps a dragged row
     from being left half way between two cards. It stays. */
  .hero__panes { scroll-behavior: auto; }
}

/* ---- footer ------------------------------------------------------------
   Below 560px the three columns are already stacked by the grid above, which
   has no column rule until 720px. These only tighten the spacing for a phone;
   the stacking is not theirs to do. */
.foot__in { padding-block-end: 1.5rem; }
.foot__in h3 { margin-block-end: 0.5rem; }
.foot__in p { margin: 0.15rem 0; }
.foot__legal {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 0.9rem;
}
.nivo {
  display: inline-flex;
  align-items: center;
  gap: 8px;
}
.nivo span {
  font-size: 9px;
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}
.nivo img {
  inline-size: 27px;
  block-size: 27px;
  object-fit: contain;
}

/* The catalogue ended flush against the footer — measured at 0px, in both
   views. Every other page ends with a .section, which carries its own
   padding; this main does not, so it needs its own. */
.catalog__main { padding-block-end: var(--section); }

/* The tap landed. A press that changes a label and a number off the top of
   the screen does not feel like a response, and the reader presses again. */
@keyframes lu-added { 0% { transform: scale(1); } 40% { transform: scale(0.94); } 100% { transform: scale(1); } }
.add[data-just-added="true"] {
  animation: lu-added 0.32s var(--ease);
  border-color: var(--lilac);
  color: var(--chalk);
}
.add[data-in="true"] { border-color: color-mix(in srgb, var(--lilac) 45%, transparent); }
@media (prefers-reduced-motion: reduce) {
  .add[data-just-added="true"] { animation: none; }
}

/* ---- what the list is still missing -------------------------------------
   Quiet on purpose: it sits above the send button and must not compete with
   it. A suggestion, not a step. */
.complete {
  padding: 0.8rem 0 0.9rem;
  border-block-start: 1px solid var(--hair2);
}
.complete__t {
  margin: 0 0 0.55rem;
  font-size: var(--fs-small);
  font-weight: 600;
  color: var(--chalk);
}
.complete__row { display: flex; flex-wrap: wrap; gap: 0.4rem; }
.complete__row a {
  display: inline-flex;
  align-items: center;
  min-block-size: var(--tap);
  padding-inline: 0.8rem;
  border: 1px solid var(--hair2);
  border-radius: 999px;
  color: var(--dim);
  font-size: var(--fs-small);
  transition: color var(--fast) var(--ease), border-color var(--fast) var(--ease);
}
.complete__row a:hover { color: var(--chalk); border-color: var(--lilac); }
.complete__row a:focus-visible { outline: 2px solid var(--lilac); outline-offset: 3px; }
@media (prefers-reduced-motion: reduce) {
  .complete__row a { transition: none; }
}

/* ---- search suggestions -------------------------------------------------
   Only on pages with no grid of their own to filter. Anchored to the field,
   which is why .search stopped being a <label>. */
.search { position: relative; }
.sugg {
  position: absolute;
  inset-inline: 0;
  inset-block-start: calc(100% + 6px);
  z-index: 60;
  margin: 0;
  padding: 0.3rem;
  list-style: none;
  max-block-size: min(60vh, 22rem);
  overflow-y: auto;
  overscroll-behavior: contain;
  background: var(--lift);
  border: 1px solid var(--hair2);
  border-radius: var(--radius);
  box-shadow: 0 18px 40px -12px rgba(0, 0, 0, 0.6);
}
.sugg a {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.6rem;
  min-block-size: var(--tap);
  padding-inline: 0.7rem;
  border-radius: calc(var(--radius) - 2px);
  color: var(--chalk);
  font-size: var(--fs-small);
}
.sugg__t { min-inline-size: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The product name is Latin and the category beside it is Arabic; isolate
   keeps the bidi algorithm from reordering one around the other. */
.sugg__t, .sugg__k { unicode-bidi: isolate; }
.sugg__k {
  flex: none;
  font-size: var(--fs-micro);
  color: var(--faint);
}
.sugg li[aria-selected="true"] a,
.sugg a:hover { background: var(--lift2); }
.sugg a:focus-visible { outline: 2px solid var(--lilac); outline-offset: -2px; }

/* ---- review fixes, 2026-09-10 -------------------------------------------
   Three blocks in the drawer's paperwork were each taller than what they
   said. Measured on a phone with twelve items: أكمل تجميعتك 106px, the price
   line 76, the order reference 22 on a row of its own. */

/* Label and chips on one wrapping row rather than two stacked ones. */
.complete { padding-block: 0.2rem 0.1rem; border-block-start: 0; }
.complete__t { display: inline; margin: 0 0 0 0.5rem; color: var(--dim); font-weight: 400; }
.complete__row { display: inline-flex; flex-wrap: wrap; gap: 0.4rem; vertical-align: middle; }

/* With no prices entered the total is a fixed sentence — "الأسعار / نرد عليك
   بها عبر واتساب" — that will say the same thing on every list until the
   owner types some in. One line is enough for a sentence. */
.total:not(:has(b)) { display: flex; align-items: baseline; gap: 0.4rem; }
.total:not(:has(b)) span { display: flex; align-items: baseline; gap: 0.4rem; }
.total:not(:has(b)) small { display: inline; }

/* Emptying the list. Destructive, so it arms rather than fires: the first tap
   turns the word into a question, the second answers it, and four seconds or a
   touch anywhere else in the drawer stands it back down. No modal — a
   confirmation dialog over a list that is not an order and costs nothing to
   rebuild is more ceremony than the act deserves. Hidden while the list is
   empty; there is nothing to empty. */
.drawer__clear {
  font-size: var(--fs-small);
  color: var(--dim);
  padding: 0.35rem 0.55rem;
  border-radius: 0.55rem;
  min-block-size: 2.75rem;   /* 44px: it sits between two other targets */
  white-space: nowrap;
}
.drawer__clear:hover { color: var(--chalk); }
.drawer__clear[data-arm] {
  color: var(--amber);
  background: var(--amber-wash);
  font-weight: 600;
}
.drawer__clear[hidden] { display: none; }


/* ---- the builds hero ----------------------------------------------------
   Same stage, same card, same slider as the category hero. Two things differ,
   and both follow from what the picture IS.

   The card carries a PHOTOGRAPH, not a cutout. Category slides show
   manufacturer press images on a white plate, which is why .sl__art img pads
   itself with the sampled plate colour — the margin has to match the photo's
   own ground or a seam shows along the edge. A build shot on the shop's own
   bench has no plate to match. It is a picture, and a picture fills its frame:
   padding removed, cover rather than contain. The card keeps its radius, its
   shadows and its tilt, so it is still an object on a lit stage.

   And the caption is a PRODUCT, not a shelf: whose machine it is, what the
   shop itself typed into its POS, and one button that puts this exact build on
   the request list. The category slide refuses all three deliberately — it
   names a category, and the browse card below already goes there. Here the
   button does something nothing else on the page can. */
.sl--build .sl__art img {
  padding: 0;
  background: none;
  object-fit: cover;
}

/* Whose machine this is. Not var(--font-mono) and not letter-spaced, unlike
   every other kicker on the site: this one is Arabic, and letter-spacing an
   Arabic string pries apart glyphs that are supposed to join. */
.sl__kick {
  font-size: var(--fs-micro);
  color: #C6B8DA;
  margin: 0 0 0.4rem;
}

/* Smaller than a category headline, and in a wider column. A category name is
   two words — كيسات — and can afford 38px; a specification is "i7 14700KF +
   RTX 4070 Ti SUPER 16G", which at that size wrapped to two lines and took the
   caption to 230px of a 600px stage, leaving the photograph 284. The picture is
   what the hero is for, so the type gives way to it. */
.sl__txt--build { max-inline-size: 44rem; }
.sl__txt--build .sl__h {
  font-size: clamp(1.15rem, 2.3vw, 1.7rem);
  margin-block-end: 0.55rem;
}
.sl__h a { color: inherit; text-decoration: none; }
.sl__h a:hover { text-decoration: underline; text-underline-offset: 0.18em; }
.sl__h a:focus-visible { outline: 2px solid #FBF8FF; outline-offset: 4px; border-radius: 4px; }

/* The category slide is one big link. The anchor stays on the heading — so
   the accessibility tree gets ONE link with the category's name, not a picture
   and a title that say the same thing — and this stretches its hit area over
   the whole pane. `.sl` is already position:absolute, so inset:0 lands on the
   pane rather than the stage.

   Only on `.is-on`. Above 900px the coverflow leaves the neighbouring panes
   visible as scenery, and an invisible full-pane link on each of those would
   swallow taps meant for the slider's own arrows and card-to-front behaviour. */
/* .sl__art is an <a> on the category slides and a <div> on the build ones, so
   it needs the block behaviour a div had for free. */
a.sl__art { display: block; }
.sl.is-on .sl__go:hover,
.sl.is-on:hover .sl__go { text-decoration: underline; text-underline-offset: 0.18em; }

/* The two halves of the shop's own machine name, shown as two facts rather
   than one run-on string. Latin inside an Arabic block, so each chip is
   isolated — without it a name ending in a digit takes its direction from
   whatever happens to follow it. */
.sl__spec {
  display: flex; flex-wrap: wrap; justify-content: center;
  gap: 0.4rem;
  margin: 0 0 0.95rem;
  padding: 0;
  list-style: none;
}
.sl__spec li {
  unicode-bidi: isolate;
  font-family: var(--font-mono);
  font-size: var(--fs-micro);
  color: #EFE8FA;
  background: rgba(255, 255, 255, 0.09);
  border: 1px solid rgba(255, 255, 255, 0.16);
  border-radius: 999px;
  padding: 0.32rem 0.7rem;
}

/* The caption has four children now — kicker, name, specs, button — where the
   category slide had two. Without a fourth step the button arrives with no
   delay at all and lands before the line above it. */
.sl.is-on .sl__txt > *:nth-child(4) { animation-delay: 0.34s; }

/* =========================================================================
   the advertising banner
   Full bleed, right to left, pictures only. It takes the hero's place when
   the shop has switched it on and put something in it; see banner.py for why
   the hero stands until then.

   A crossfade rather than a slide: these are whole pictures with their own
   edges, and sliding them past one another reads as a filmstrip rather than
   as one announcement giving way to the next. Slow, because he asked for
   slow — seven seconds a picture, which is long enough to read a price off
   one and short enough not to look stuck.
   ========================================================================= */
.adbar { position: relative; inline-size: 100%; background: var(--deep, #0B0614); }
.adbar__track {
  position: relative; display: block; margin: 0; padding: 0; list-style: none;
  /* The stage takes the FIRST picture's shape, measured by the build; 12/5
     is only the fallback for when Pillow could not read it.

     It was a flat 12/5 on the theory that a shop banner is drawn at 1200x500.
     The shop's first real banner is a 1215x1295 portrait poster — headline at
     the top, products below — which contained to 39% of the width and left
     61% of the hero empty.

     A picture is still never cropped. On a phone this is the whole answer: a
     portrait poster at 100vw is a portrait hero, filled edge to edge. */
  /* The width is stated so the RATIO can give way instead of it. With width
     left to itself, aspect-ratio and the cap below together solved for a
     582px-wide stage — the ratio held and the banner shrank off both edges of
     a 1265px page, which is the opposite of filling it. Measured. */
  inline-size: 100%;
  aspect-ratio: var(--ad-ar, 12 / 5);
  /* What keeps that honest on a desktop, where 1215x1295 across 1265px would
     be 1348px of banner — a screen and a half before anything else. Past the
     cap the aspect-ratio gives way, the picture is centred at full height,
     and ::before fills what is left.

     820, not 620, so that a banner actually drawn for this slot FILLS it at
     every common width instead of being capped into a letterbox of its own:
     a 12:5 across 1905px is 794px tall, and a 620 cap would have contained it
     to 1488px wide with blur down both sides — the original problem, moved.
     The svh term is what protects a short laptop: at 800px tall the cap is
     640, and a 12:5 at 1265 wide is 527, so it still fills. */
  max-block-size: min(80svh, 820px);
  overflow: hidden;
}
/* The picture's own colours, blurred, filling whatever the picture does not.
   Same file the browser already has, so it costs no second request; scaled
   past the edges because a blur samples outside itself and would otherwise
   show a soft rim. Hidden from assistive tech: it is the same artwork again,
   and the <img> above it already carries the shop's alt text. */
.adbar__slide::before {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--ad-bg) center / cover no-repeat;
  filter: blur(34px) saturate(1.15) brightness(0.72);
  transform: scale(1.12);
  z-index: 0;
  pointer-events: none;
}
.adbar__slide > * { position: relative; z-index: 1; }
.adbar__slide {
  position: absolute; inset: 0;
  opacity: 0;
  transition: opacity 1.1s var(--ease, ease);
}
.adbar__slide.is-on { opacity: 1; }
/* Before the script runs, the first picture is the one on show. Without this
   a reader with no JavaScript sees an empty black stage. */
.adbar__slide:first-child { opacity: 1; }
.adbar.is-live .adbar__slide:first-child:not(.is-on) { opacity: 0; }
.adbar__slide img {
  inline-size: 100%; block-size: 100%;
  object-fit: contain;
}
.adbar__dots {
  position: absolute; inset-block-end: 12px; inset-inline: 0;
  display: flex; justify-content: center; gap: 8px;
  padding-block-end: env(safe-area-inset-bottom, 0px);
}
.adbar__dots button {
  inline-size: 10px; block-size: 10px; padding: 0;
  border-radius: 50%; border: 1px solid rgba(255, 255, 255, 0.55);
  background: rgba(10, 6, 18, 0.45); cursor: pointer;
  transition: background var(--fast, .2s) var(--ease, ease);
}
.adbar__dots button[aria-current="true"] { background: var(--lilac, #E07CFF); border-color: transparent; }
.adbar__dots button:focus-visible { outline: 3px solid var(--lilac, #E07CFF); outline-offset: 3px; }
/* A dot is a 10px target; the tappable area around it is not. */
.adbar__dots button::before { content: ''; position: absolute; inline-size: 44px; block-size: 44px; translate: -17px -17px; }
.adbar__dots button { position: relative; }

@media (max-width: 720px) {
  /* 4/3 is the FALLBACK here, not the rule. It was written when the stage was
     a flat 12:5 and a phone got a strip too thin to read; squarer gave the
     contained picture somewhere to sit.

     Now the picture sets the shape, so on a phone it simply fills: his
     1215x1295 poster becomes a 390x416 hero with no blur showing at all.
     Leaving the 4/3 in place here undid that — measured at 390 wide, the
     stage came out 390x293 and the poster filled 71% of it, which is the
     same empty-bars problem one breakpoint lower. The fallback still covers
     the case the old rule was for: when Pillow could not read the file and
     there is no ratio to follow. */
  /* The phone's own picture sets the shape when he has supplied one, falling
     back to the desktop picture's, then to 4/3. The whole chain is written
     here because a custom property cannot fall back to another custom
     property's fallback — var(--ad-ar-m, var(--ad-ar, 4 / 3)) is the only
     form that says "mobile, else desktop, else neither were measurable". */
  .adbar__track { aspect-ratio: var(--ad-ar-m, var(--ad-ar, 4 / 3)); }
  /* And the blur behind it is that same picture, so a portrait poster never
     sits on a blurred landscape. */
  .adbar__slide::before { background-image: var(--ad-bg-m, var(--ad-bg)); }
}
@media (prefers-reduced-motion: reduce) {
  .adbar__slide { transition: none; }
}

/* =========================================================================
   the homepage rows — sideways, not stacked
   Asked for directly: "المنتجات في اقسام الواجهة يجو بالطول مش بالافق". On a
   phone a stacked row of ten products is a long scroll past the same section;
   sideways, it is ten products you flick through without leaving the spot.

   The layout follows the reference the shop sent — a titled header with a
   browse-all button, a rule beneath it, cards in a track, round arrows on the
   sides. The COLOURS do not: that reference is a red storefront and this is
   LEVEL UP, whose identity issue #5 asks to keep ("مع الحفاظ على الهوية
   الحالية"). So the accent is the shop's own purple.
   ========================================================================= */
.shelfrow__head {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--s2); flex-wrap: wrap;
  padding-block-end: 0.75rem;
  border-block-end: 2px solid var(--core);
  margin-block-end: var(--s3);
}
.shelfrow__title {
  font-size: var(--fs-h2);
  margin: 0;
  /* The bar beside the title in the reference, in the shop's purple. */
  padding-inline-start: 0.7rem;
  border-inline-start: 4px solid var(--core);
  line-height: 1.2;
}
.shelfrow__all { flex: none; border-radius: 999px; }

.shelfrow { position: relative; }
.shelfrow__track {
  display: flex;
  gap: var(--s2);
  overflow-x: auto;
  scroll-snap-type: x proximity;
  scroll-behavior: smooth;
  /* The row bleeds to the edges of the screen on a phone so a card can sit
     half off it — the cue that there is more, without a scrollbar to read. */
  padding-inline: var(--gutter);
  margin-inline: calc(var(--gutter) * -1);
  padding-block: 4px;
  scrollbar-width: none;
  /* Same guard .shelves and .hero__panes carry, and for the same reason: a
     drag that reaches the end of the row must stop there rather than becoming
     a back-navigation gesture, which on a phone is the difference between a
     dead end and losing the page.

     It matters more here than anywhere. The pin is desktop-only now, so on a
     phone THIS is how the shelf is read — a thumb pushing the strip — and the
     end of the strip is reached every time. */
  overscroll-behavior-x: contain;
  -webkit-overflow-scrolling: touch;
}
.shelfrow__track::-webkit-scrollbar { display: none; }
.shelfrow__track > .card {
  flex: 0 0 auto;
  inline-size: clamp(14rem, 62vw, 17rem);
  scroll-snap-align: start;
}
@media (min-width: 900px) {
  .shelfrow__track > .card { inline-size: 15.5rem; }
}
/* While the page drives the first row on a phone (site.js, "drifts as the
   page goes by"), snap would pull every frame back to a card edge. */
.shelfrow.is-drifting .shelfrow__track { scroll-snap-type: none; }

/* Round arrows over the row, as in the reference. Pointer only: the track is
   a scroll container, so a keyboard and a screen reader already reach every
   card by scrolling it — see the note in homepage.py. */
.shelfrow__arw {
  position: absolute; inset-block-start: 50%; transform: translateY(-50%);
  z-index: 3;
  inline-size: var(--tap); block-size: var(--tap);
  display: none; place-items: center;
  border-radius: 50%; border: 1px solid rgba(255, 255, 255, 0.18);
  background: var(--core); color: #fff;
  font-size: 1.4rem; line-height: 1; cursor: pointer;
  box-shadow: 0 8px 22px -10px #000;
}
.shelfrow__arw--prev { inset-inline-start: calc(var(--gutter) * -0.5); }
.shelfrow__arw--next { inset-inline-end: calc(var(--gutter) * -0.5); }
/* Only where there is a pointer to use them, and only when the row actually
   overflows — JS adds is-scrollable. */
@media (hover: hover) and (min-width: 720px) {
  .shelfrow.is-scrollable .shelfrow__arw { display: grid; }
}
.shelfrow__arw[disabled] { opacity: 0.35; cursor: default; }

/* ---- the card inside a row -------------------------------------------
   The reference centres the name, rules a line under it, and puts the price
   and the action on ONE row. Scoped to the rows: the catalogue grid packs two
   cards across a phone, where a one-line foot has nowhere to go. */
.shelfrow__track .card__name { text-align: center; }
.shelfrow__track .card__foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s1);
  border-block-start: 1px solid var(--hair2);
  padding-block-start: 0.7rem;
}
/* Not full width here, so it can sit beside the price. It keeps its word:
   an icon alone was tried and dropped because nothing said what it added to,
   and on a site with no cart a bare trolley would promise a checkout that
   does not exist. */
.shelfrow__track .btn--add { inline-size: auto; flex: none; }
@media (prefers-reduced-motion: reduce) {
  .shelfrow__track { scroll-behavior: auto; }
}

/* ---- the trolley on a card's add button --------------------------------
   The markup always carries both the drawn trolley and the word; which one
   shows is this stylesheet's decision.

   In the catalogue grid the word stays and the trolley leads it: that grid
   packs two cards across a phone and the button is the one control a customer
   has to understand, which is why an icon ALONE was dropped here once before.
   In the homepage rows the trolley stands by itself, as the shop asked — the
   row's cards are half as many and twice as wide, the price sits beside it,
   and the button's accessible name carries the words for anyone who cannot
   see it. */
.btn--add { display: inline-flex; align-items: center; justify-content: center; gap: 0.4rem; }
.add__cart { flex: none; }
/* A product already on the list reads as done rather than as "add again". */
.btn--add[data-in="true"] .add__cart { opacity: 0.55; }

.shelfrow__track .btn--add [data-add-text] {
  /* Clipped, not display:none — the word is the button's visible label in the
     grid and its accessible name here, and a name removed from the tree is a
     button a screen reader announces as nothing. aria-label carries it too,
     so this is the belt to that brace. */
  position: absolute;
  inline-size: 1px; block-size: 1px;
  margin: -1px; padding: 0; border: 0;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}
/* A wide rounded rectangle, not a circle — the shape in the reference, which
   reads as a button rather than as a chip. Measured off it: the button is
   roughly a third of the card's width and about half as tall as it is wide,
   with the trolley centred. --tap keeps the height at the 44px every other
   square control on this site is held to. */
.shelfrow__track .btn--add {
  inline-size: 34%;
  min-inline-size: 5rem;
  block-size: var(--tap);
  padding: 0;
  border-radius: 12px;
  position: relative;
  flex: none;
}
.shelfrow__track .btn--add .add__cart { inline-size: 22px; block-size: 22px; }
/* Filled, as in the reference: there it is the one coloured thing on a white
   card and it is obviously the thing to press. The default .btn here is an
   outline, which on a card this size read as a disabled control. The fill is
   --core, the token documented as carrying white text. */
.shelfrow__track .btn--add {
  background: var(--core);
  border-color: transparent;
  color: #fff;
}
.shelfrow__track .btn--add:hover:not([disabled]) { background: var(--lilac); }
/* Already on the list: still legible, visibly done rather than inviting. The
   icon keeps full strength — dimming white on a colour is how contrast is
   lost — and the plate behind it steps back instead. */
/* A plain value rather than color-mix: the computed style of a mix comes back
   as color(srgb …), which no contrast check reads, and a colour nobody can
   measure is a colour nobody has checked. This is --core taken down to roughly
   55%, dark enough in either theme to hold a white icon. */
.shelfrow__track .btn--add[data-in="true"] { background: rgb(92, 20, 109); }
.shelfrow__track .btn--add[data-in="true"] .add__cart { opacity: 1; }
.shelfrow__track .btn--add[disabled] { background: var(--hair2); color: var(--dim); }

/* ---- the row's cards arrive in turn ------------------------------------
   The entrance itself is not written here: initReveal already watches every
   .card on the site and adds .is-in, which runs lu-rise. It covers these
   rows for free, and writing a second one meant two animations fighting over
   one opacity.

   What it does not do is stagger. The observer reveals each card as it
   crosses the line, and in a row every card sits at the same height, so they
   all cross together and arrive as one block. A delay per card is the whole
   of the difference — the rest is the mechanism that was already there.

   It stops counting at the sixth: past that it only makes the reader wait for
   cards that are off the side of the screen. Reduced motion is already
   handled where lu-rise is, which is another reason not to have written a
   second copy of it. */
.shelfrow__track .card.is-in { animation-delay: calc(var(--enter, 0) * 55ms); }

/* ---- the row's tile, after عطور شيك on al-anis.com ---------------------
   There a tile is a plate that FLOATS: a tall rounded slab with the object
   contained on it and a soft shadow under it, and the words sit on the page
   below it rather than inside a panel. That is the whole of why the row
   reads as a shelf of objects instead of a strip of boxes, so it is the part
   worth taking.

   Scoped to the rows. The catalogue grid keeps its border, because that grid
   packs two tiles across a phone and the border is what separates them.

   The colours are not taken. That site is a blush-pink storefront; this is
   LEVEL UP, and issue #5 asks to keep its identity — so the plate stays on
   --plate and the page's own two themes keep working. */
.shelfrow__track > .card,
.shelfrow__track > .card:hover {
  border-color: transparent;
  background: none;
  box-shadow: none;
  overflow: visible;             /* the plate's shadow falls outside the tile */
}
.shelfrow__track .card__plate {
  /* The plate stays SQUARE. شيك's is 3:4, and the taller slab is better for
     what that shop sells — a perfume bottle is portrait, every time.

     This catalogue is not. Measured across all 165 display photos: 50
     landscape, 54 square, 61 portrait, median ratio exactly 1.00. A 3:4 plate
     paints the 61 cases and towers larger and leaves the other 104 — the
     keyboards, the monitors, the GPUs — letterboxed in a tall slab. Square is
     the median, and it is the only proportion that treats all three groups
     alike.

     It also keeps the plate matching the catalogue grid's, which is what the
     shop's own per-photo framing is calibrated against: --photo-size,
     --photo-left and --photo-top are percentages OF THE PLATE, so two plates
     of different shapes make one set of numbers mean two things.

     The radius, the float, the shadow and the halo come across. The
     proportion does not. */
  border-radius: 22px;
  border-block-end: 0;
  box-shadow: 0 30px 50px -34px rgba(0, 0, 0, 0.85);
}
/* The glow under the object, already built: .card__plate::after is this
   site's rim light and sits exactly where شيك puts its halo. It is lifted a
   little here because the plate now floats free of a panel. */
.shelfrow__track .card__plate::after { opacity: 0.55; }
.shelfrow__track .card__plate img { transition: transform 0.5s var(--ease); }
.shelfrow__track .card:hover .card__plate img { transform: translateY(-10px) scale(1.04); }

.shelfrow__track .card__body {
  padding-inline: 0;
  align-items: center;
  text-align: center;
}
/* The foot still spans the tile: the price sits at one end and the trolley at
   the other, which align-items:center would otherwise shrink to their words. */
.shelfrow__track .card__foot { align-self: stretch; }

/* ---- and it turns toward the middle ------------------------------------
   `rotate` and `scale`, NOT `transform`. The entrance animation lu-rise runs
   `both`, so it holds `transform: none` on every card for good once it has
   played — a focal transform written here would be erased the moment a card
   finished arriving. The individual properties compose with it instead of
   being overwritten by it, so the two effects stack: the card rises, then
   keeps turning.

   The perspective lives on the TRACK, so every tile shares one vanishing
   point the way a real shelf does. (al-anis gives each bottle its own, which
   is cheaper to reason about and very slightly wrong.)

   Applied only under .is-focal, which the script adds after it has checked
   for reduced motion — so a reader who asked for stillness gets a plain flat
   row, and so does anyone with no JavaScript. */
.shelfrow.is-focal .shelfrow__track { perspective: 1100px; }
.shelfrow.is-focal .shelfrow__track > .card {
  rotate: y calc(var(--d, 0) * 26deg);
  scale: calc(1 - var(--a, 0) * 0.12);
}
/* The dim goes on the two children rather than the card, for the same reason
   the turn is not a transform: lu-rise `both` pins the card's own opacity at
   1. Two siblings that never overlap fade identically to their parent.

   0.82 at the edge, not the 0.65 شيك uses. A شيك tile is something to look
   at; this one carries a price that has to stay legible even as it leaves,
   and the button keeps full strength throughout — dimming white on a colour
   is how contrast gets lost. */
.shelfrow.is-focal .shelfrow__track .card__plate,
.shelfrow.is-focal .shelfrow__track .card__body {
  opacity: calc(1 - var(--a, 0) * 0.18);
}
.shelfrow.is-focal .shelfrow__track .btn--add { opacity: 1; }
/* Room for the shadow and for a tile scaled back up to 1 at the centre. */
.shelfrow.is-focal .shelfrow__track { padding-block: 10px 30px; }

/* ---- the filters over the shelf ----------------------------------------
   His three rows became chips over one strip. They are radio inputs, not
   buttons: a chosen filter is one state out of several, exactly one at a
   time, which is what a radio group means to a screen reader and what gives
   a keyboard arrow keys through the set without a line of script. The input
   itself is taken off screen rather than hidden — display:none would take it
   out of the tab order and out of the accessibility tree, which is the whole
   reason for using one. */
.shelffilters {
  display: flex;
  gap: 0.5rem;
  margin-block-end: var(--s3);
  /* One line that scrolls, not four lines that stack. Measured at 376px:
     «الكل», «أفضل العروض», «الأكثر مشاهدة» and «عروض الجمعة البيضاء» wrapped
     onto three rows and pushed the shelf most of a thumb further down the
     page. The names are his and can only get longer.

     The row bleeds to the screen edges like the shelf under it, so a chip
     sitting half off the edge is the cue that there are more — the same cue,
     in the same place, as the products. */
  overflow-x: auto;
  scrollbar-width: none;
  padding-inline: var(--gutter);
  margin-inline: calc(var(--gutter) * -1);
  padding-block: 2px;
  /* The chips scroll sideways too, so they need the same guard as the shelf
     under them — a flick along them must not become a back gesture. */
  overscroll-behavior-x: contain;
  -webkit-overflow-scrolling: touch;
}
.shelffilters::-webkit-scrollbar { display: none; }
.shelffilter { flex: 0 0 auto; }
.shelffilter { position: relative; white-space: nowrap; }
.shelffilter input {
  position: absolute;
  inset-block-start: 0; inset-inline-start: 0;
  inline-size: 100%; block-size: 100%;
  margin: 0; opacity: 0; cursor: pointer;
}
.shelffilter span {
  display: block;
  padding: 0.45rem 0.95rem;
  border: 1px solid var(--hair2);
  border-radius: 999px;
  font-size: 0.9rem;
  line-height: 1.4;
  color: var(--dim);
  transition: border-color var(--fast) var(--ease), color var(--fast) var(--ease),
              background var(--fast) var(--ease);
}
.shelffilter input:checked + span {
  background: var(--core);
  border-color: transparent;
  color: #fff;
}
.shelffilter input:hover:not(:checked) + span { border-color: var(--lilac); color: var(--ink); }
/* The focus ring goes on the visible chip, because the input it belongs to
   is the invisible one. */
.shelffilter input:focus-visible + span {
  outline: 2px solid var(--lilac);
  outline-offset: 2px;
}

/* A card the chosen filter excludes. Not [hidden] — these are inside a flex
   track and a class keeps the one rule in one place. */
.shelfrow__track .card.is-off { display: none; }

.shelfrow__empty {
  color: var(--dim);
  margin-block: 0 var(--s3);
}

/* ---- why a tile is on the shelf ----------------------------------------
   "a comment inside the products cards above the price". It sits over the
   price inside .card__facts, which is already a column, so it needs no
   placement of its own — only to be quieter than the price it introduces.

   Quiet on purpose: this is the shelf's filing, not the product's name or
   its price, and a row of loud labels would have every tile shouting the
   same three words. The exception is the Friday one, which is the only badge
   that marks a thing with an end date. */
.whys { display: flex; flex-wrap: wrap; gap: 0.3rem; }
.why {
  font-size: 0.7rem;
  line-height: 1.5;
  padding: 0.1rem 0.45rem;
  border-radius: 999px;
  border: 1px solid var(--hair2);
  color: var(--dim);
  white-space: nowrap;
}
.why--friday {
  border-color: transparent;
  background: var(--core);
  color: #fff;
}

/* ---- what a banner says ------------------------------------------------
   Optional, all of it. A banner he designed with the words already inside
   the picture carries no headline, and then nothing is drawn over his
   artwork — no plate, no scrim, no "تصفّح".

   When there IS a headline the text needs to stay readable over a photograph
   nobody has vetted, so the scrim is a gradient from the bottom edge rather
   than a wash over the whole picture: it darkens the band the words sit in
   and leaves the rest of his image alone. */
.adbar__link {
  position: relative;
  display: block;
  block-size: 100%;
  color: inherit;
  text-decoration: none;
}
.adbar__say {
  position: absolute;
  inset-inline: 0;
  inset-block-end: 0;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.35rem;
  padding: clamp(1rem, 4vw, 2.5rem);
  padding-block-start: 18%;
  background: linear-gradient(to top, rgba(0, 0, 0, 0.82) 0%,
                              rgba(0, 0, 0, 0.55) 45%, rgba(0, 0, 0, 0) 100%);
  color: #fff;
  text-align: start;
  pointer-events: none;       /* the whole slide is the link */
}
.adbar__h {
  font-family: var(--display, inherit);
  font-size: clamp(1.25rem, 3.6vw, 2.6rem);
  font-weight: 800;
  line-height: 1.2;
  text-wrap: balance;
}
.adbar__sub {
  font-size: clamp(0.85rem, 1.6vw, 1.15rem);
  line-height: 1.5;
  opacity: 0.92;
  max-inline-size: 48ch;
}
.adbar__cta {
  margin-block-start: 0.3rem;
  padding: 0.4rem 1.1rem;
  border-radius: 999px;
  background: var(--core);
  color: #fff;
  font-size: 0.9rem;
  font-weight: 700;
}
a.adbar__link:hover .adbar__cta { background: var(--lilac); }
a.adbar__link:focus-visible {
  outline: 3px solid var(--lilac);
  outline-offset: -3px;
}

/* ---- the first row used to travel with the page ------------------------
   It pinned: the section was made as tall as its strip was wide and held
   under the reader while that height went by, so scrolling down drove the
   shelf sideways. عطور شيك's mechanism, asked for by name.

   Removed on his instruction after the second complaint about scrolling. It
   came off phones first — a thumb already has a swipe, and the hold cost
   1289px there, 1.74 screens, 32% of the homepage. Desktop kept it and the
   complaint came back: measured live at 1280x820 with seven products, the
   hold was 1584px, 1.93 screens, 33% of a 4739px page.

   The row underneath is what it always degraded to, and what it is now: a
   scroll container with arrows on a pointer and a swipe under a thumb. The
   turn stayed; it follows the strip's own position and never needed the pin.
   .shelfrow__stick, .shelfrow__meter and --head-h went with it. DESIGN.md
   keeps the reasoning if it is ever wanted back. */

@media (prefers-reduced-motion: reduce) {
  .shelfrow__track > .card { rotate: none; scale: none; }
  .shelfrow__track .card__plate, .shelfrow__track .card__body { opacity: 1; }
  .shelfrow__track .card__plate img { transition: none; }
  .shelfrow__track .card:hover .card__plate img { transform: none; }
}
