/* GENERATED by tools/assemble.py — do not edit.
   Source: shared/css/style.css and the partials it imports.
   Edit those; this file is rebuilt on every run. */

/* ===== partials/_variables.css ===== */
/* =========================================================================
   GulfRabit · Design Tokens
   Single source of truth for colour, type, spacing, radius, elevation, motion.
   Every other partial and module reads from these custom properties — never
   hard-code a hex, a shadow, or a radius elsewhere.
   ========================================================================= */

:root {
  /* ---- Brand · Primary ------------------------------------------------- */
  --gr-cyan:        #1BB4D4;   /* primary — CTAs, links, active states       */
  --gr-cyan-dark:   #0E7C99;   /* hover / pressed, deep accents, gradient    */
  --gr-lime:        #9ACD3C;   /* secondary — highlights, success/sale badge */
  --gr-lime-light:  #C3E86B;   /* light accents, gradient partner            */

  /* ---- Brand · Neutrals (the premium base — 80% of the canvas) --------- */
  --gr-black:       #0A0A0A;   /* primary background (warm black)            */
  --gr-charcoal:    #151515;   /* card / section backgrounds on black        */
  --gr-graphite:    #1F1F1F;   /* elevated surfaces, input fields            */
  --gr-border:      #2A2A2A;   /* hairline borders on dark surfaces          */
  --gr-white:       #FFFFFF;   /* text on dark / background on light         */
  --gr-off-white:   #F7F8F7;   /* light-mode section backgrounds             */
  --gr-ink:         #101314;   /* primary text on light backgrounds          */
  --gr-gray-500:    #8A8F8C;   /* secondary / muted text                     */
  --gr-gray-300:    #D8DBD9;   /* dividers on light backgrounds              */

  /* ---- Surface roles · LIGHT THEME ------------------------------------- */
  /* The palette above lists brand colours; THESE are what the UI actually
     paints with. Every partial and module reads the role, never the raw hex,
     so the whole canvas can be re-themed from this block alone.             */
  --surface-page:    #FFFFFF;  /* the page canvas                           */
  --surface-raised:  #FFFFFF;  /* cards, menus, modals, drawers (+ border)  */
  --surface-sunken:  #F4F6F5;  /* inputs, image wells, hovered rows         */
  --surface-band:    #F7F8F7;  /* alternating section bands, footer         */
  --surface-inverse: #0A0A0A;  /* deliberate dark blocks (rare)             */

  --border-hairline: #E5E9E7;  /* dividers, card outlines (decorative)      */
  --border-input:    #808A85;  /* form controls — meets 3:1 for UI          */

  /* Text ramp — every value below is AA-contrast on --surface-page.        */
  --text-primary:    #101314;  /* headings + body            18.5:1         */
  --text-secondary:  #414B47;  /* supporting copy, labels     9.0:1         */
  --text-muted:      #667069;  /* captions, meta              5.1:1         */
  --text-on-accent:  #04222b;  /* text sitting ON cyan/lime fills           */
  --text-inverse:    #FFFFFF;  /* text on --surface-inverse                 */

  /* Brand colours re-cut for legibility on white. Raw --gr-cyan (2.5:1) and
     --gr-lime fail contrast as text on light — use these for anything that
     carries meaning; keep the raw brand hues for fills, borders and glows.  */
  --link:            #0E7C99;  /* links, overlines            4.8:1         */
  --link-hover:      #0A5F76;  /*                             6.4:1         */
  --accent-ink:      #0E7C99;  /* meaningful icons on light   4.8:1         */
  --gold-ink:        #8A6D22;  /* premium/VIP text            4.8:1         */
  --lime-ink:        #547C10;  /* stars, success text         4.9:1         */

  /* Washes & scrims — dark-on-light now, the inverse of the dark theme.    */
  --wash-hover:      rgba(16,19,20,.055);
  --wash-shimmer:    rgba(16,19,20,.055);
  --scrim:           rgba(16,19,20,.45);
  --glass-header:    rgba(255,255,255,.82);
  --glass-overlay:   rgba(255,255,255,.94);
  --scrollbar-thumb: #C3CAC6;

  /* ---- Semantic -------------------------------------------------------- */
  --gr-success:     #9ACD3C;   /* reuse lime — no generic green              */
  --gr-error:       #E5484D;
  --gr-warning:     #E8B342;
  --gr-gold-accent: #C9A24B;   /* ONLY Premium/Imported/VIP luxury details   */

  /* ---- Brand gradient (accents only — never a full page background) ---- */
  --gr-gradient:      linear-gradient(135deg, #1BB4D4 0%, #9ACD3C 100%);
  --gr-gradient-soft: linear-gradient(135deg, rgba(27,180,212,.14) 0%, rgba(154,205,60,.14) 100%);
  /* Gradient for TEXT on light: the raw brand gradient reads at 1.9:1 on white
     and is unreadable, so display type uses this ink-weight cut instead. */
  --gr-gradient-ink:  linear-gradient(135deg, #0E7C99 0%, #4E7A0F 100%);

  /* ---- Typography ------------------------------------------------------ */
  --font-display: 'Clash Display', 'General Sans', 'Inter', system-ui, sans-serif;
  /* The section headings only — "Shop by Category", "Premium Picks". A high
     contrast serif is what reads expensive next to a geometric sans, and the
     pairing is the point: the serif says what KIND of shop this is, the sans
     stays on the work of selling. Product names keep --font-display, because
     a product name is information a customer scans, not an atmosphere. */
  --font-luxe: 'Playfair Display', 'Cormorant Garamond', Georgia, 'Times New Roman', serif;
  /* Bengali sits in the stack so Bangla renders correctly ANYWHERE, not only
     inside a [lang="bn"] wrapper — unicode-range means it only downloads when
     Bengali codepoints are actually present. */
  --font-body:    'Inter', 'Noto Sans Bengali', 'Noto Kufi Arabic', system-ui, -apple-system, sans-serif;

  /* Modular type scale · 1.25 ratio — pull sizes from here, never ad-hoc.  */
  --fs-12: 0.75rem;   /* 12 — micro tags, legal        */
  --fs-14: 0.875rem;  /* 14 — small text, captions     */
  --fs-16: 1rem;      /* 16 — body base                */
  --fs-20: 1.25rem;   /* 20 — lead, small headings     */
  --fs-25: 1.5625rem; /* 25 — H4                       */
  --fs-31: 1.9375rem; /* 31 — H3                       */
  --fs-39: 2.4375rem; /* 39 — H2                       */
  --fs-49: 3.0625rem; /* 49 — H1                       */

  --fw-regular:  400;
  --fw-medium:   500;
  --fw-semibold: 600;
  --fw-bold:     700;

  --lh-tight:  1.15;
  --lh-snug:   1.3;
  --lh-normal: 1.6;

  /* ---- Spacing · 8px baseline grid (precision alignment, no eyeballing) */
  --space-1: 0.25rem;  /*  4 */
  --space-2: 0.5rem;   /*  8 */
  --space-3: 0.75rem;  /* 12 */
  --space-4: 1rem;     /* 16 */
  --space-5: 1.5rem;   /* 24 */
  --space-6: 2rem;     /* 32 */
  --space-7: 3rem;     /* 48 */
  --space-8: 4rem;     /* 64 */
  --space-9: 6rem;     /* 96 */
  --space-10: 8rem;    /* 128 — section rhythm on large screens */

  /* ---- Radius · exactly two, never deviate ----------------------------- */
  --radius-sm: 8px;    /* inputs, buttons, tags, small controls */
  --radius-lg: 16px;   /* cards, modals, media, large surfaces  */
  --radius-pill: 999px;

  /* ---- Elevation · soft, low-opacity, large-blur (never harsh drops) ---
     Tuned for a white canvas: on light backgrounds a shadow reads far louder
     than the same shadow on black, so these are ~a third of the dark-theme
     opacity. Depth comes from the hairline border first, shadow second.    */
  --shadow-sm: 0 1px 2px rgba(16,19,20,.05), 0 1px 3px rgba(16,19,20,.07);
  --shadow-md: 0 4px 16px rgba(16,19,20,.08), 0 2px 6px rgba(16,19,20,.06);
  --shadow-lg: 0 18px 48px rgba(16,19,20,.12), 0 6px 16px rgba(16,19,20,.08);
  /* Brand glow for hero CTAs / focus rings */
  --shadow-glow: 0 8px 24px rgba(27,180,212,.32);

  /* ---- Motion · subtle, crafted (no bounce, no gimmick easing) --------- */
  --ease-out:   cubic-bezier(0.16, 1, 0.3, 1);
  /* For scroll reveals only. Leaves fast and lands slowly with a long tail,
     so an element settles into place instead of stopping at it. */
  --ease-reveal: cubic-bezier(0.22, 0.85, 0.24, 1);
  --ease-inout: cubic-bezier(0.65, 0, 0.35, 1);
  --dur-fast:   150ms;
  --dur-base:   200ms;
  --dur-slow:   300ms;

  /* ---- Layout ---------------------------------------------------------- */
  --container-max: 1400px;
  --header-h: 76px;
  --z-header: 1000;
  --z-drawer: 1050;
  --z-toast:  1100;
  --z-modal:  1090;
}

/* ===== partials/_fonts.css ===== */
/* =========================================================================
   GulfRabit · Fonts
   One style concern, one partial. Latin comes from Google Fonts (Inter);
   Bengali is SELF-HOSTED here.
   ========================================================================= */

/* ---- Bengali ------------------------------------------------------------
   Not one of Shajgoj, Ghorer Bazar or Daraz loads a Bengali webfont. Daraz
   self-hosts Roboto with cyrillic, greek and vietnamese subsets and NO Bengali
   at all, while Bangla is their mobile default language — so Bangla text on the
   biggest storefront in the country renders in whatever the device happens to
   have. Fixing that is the cheapest visible advantage available to this project.

   Noto Sans Bengali, SIL Open Font License 1.1 — free to self-host and
   redistribute. Self-hosted rather than linked so it survives a strict CSP,
   costs no third-party connection, and cannot be broken by a CDN outage.

   ONE file covers every weight: Google serves byte-identical woff2 for
   wght@400, wght@600 and wght@100..900, so this is the variable font and the
   `font-weight: 100 900` range is the correct descriptor. Declaring discrete
   weights instead would have shipped the same 105 KB twice.

   unicode-range is the important part: the browser downloads this file ONLY
   when Bengali codepoints are actually on the page. Latin pages pay nothing.
   Range copied verbatim from Google's own bengali subset. */
@font-face {
  font-family: 'Noto Sans Bengali';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url('../../assets/fonts/noto-sans-bengali-variable.woff2') format('woff2');
  unicode-range: U+0951-0952, U+0964-0965, U+0980-09FE, U+1CD0, U+1CD2,
                 U+1CD5-1CD6, U+1CD8, U+1CE1, U+1CEA, U+1CED, U+1CF2,
                 U+1CF5-1CF7, U+200C-200D, U+20B9, U+25CC, U+A8F1;
}

/* Bangla runs slightly larger than Latin at the same px size and needs more
   leading for its ascenders and matras. Applied by language, so mixed-script
   pages get it automatically without a wrapper class. */
[lang="bn"],
.bn {
  font-family: 'Noto Sans Bengali', var(--font-body);
  line-height: 1.75;
  /* Bangla has no case distinction — uppercasing produces nonsense. */
  text-transform: none;
  letter-spacing: 0;
}

/* ===== partials/_typography.css ===== */
/* =========================================================================
   GulfRabit · Typography
   One typographic voice, applied consistently. Sizes come only from the
   scale in _variables.css. Prices use tabular-nums so they align in columns.
   ========================================================================= */

html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }

body {
  font-family: var(--font-body);
  font-size: var(--fs-16);
  line-height: var(--lh-normal);
  font-weight: var(--fw-regular);
  color: var(--text-primary);
  background-color: var(--surface-page);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
}

/* ---- Headings — display face, tight leading, balanced wrapping -------- */
h1, h2, h3, h4, h5, h6 {
  font-family: var(--font-display);
  font-weight: var(--fw-semibold);
  line-height: var(--lh-tight);
  letter-spacing: -0.01em;
  color: inherit;
  margin: 0;
  text-wrap: balance;
}

/* Tracking tightens as the type grows.
 *
 * Letter-spacing is optical, not a constant. The gaps a face needs at 16px are
 * visibly loose at 49px — the word stops reading as one shape and starts
 * reading as separate letters, which is the difference between a headline that
 * looks set and one that looks typed. Every display face is drawn on this
 * assumption; Clash Display especially.
 *
 * The values step with the size rather than being one -0.01em for everything,
 * which is what the file did before.
 */
/* ---- The serif, and exactly where it is allowed ----------------------
   Every line that NAMES something — a page, a section, a category — is set in
   Playfair. That is the whole brief: a high-contrast serif beside a geometric
   sans is what reads expensive, and it costs nothing as long as it stays off
   the text people actually read.

   So it is scoped by role, not by tag. A product's name is an h1 on the
   product page and it is excluded by name: a customer scans it against a
   price and a pack size, and Playfair's thin strokes are worse at that job
   than the sans. Same for prices, buttons and card titles, which never had
   it. If a heading is information rather than an announcement, it keeps the
   sans — add it to the :not() list rather than weakening this rule. */
h1:not([data-pdp-title]):not(.product-card__title),
.h1:not([data-pdp-title]),
.page-head,
h2.section-title {
  font-family: var(--font-luxe);
  font-weight: 600;
  letter-spacing: -0.012em;
}

h1, .h1 { font-size: clamp(2.25rem, 5vw, var(--fs-49)); font-weight: var(--fw-bold); letter-spacing: -0.028em; }
h2, .h2 { font-size: clamp(1.9rem, 4vw, var(--fs-39)); letter-spacing: -0.022em; }
h3, .h3 { font-size: clamp(1.5rem, 3vw, var(--fs-31)); letter-spacing: -0.016em; }
h4, .h4 { font-size: var(--fs-25); }
h5, .h5 { font-size: var(--fs-20); font-weight: var(--fw-semibold); }
h6, .h6 { font-size: var(--fs-16); font-weight: var(--fw-semibold); letter-spacing: 0; }

p { margin: 0; text-wrap: pretty; }
p + p { margin-top: var(--space-4); }

/* ---- Lead / secondary / captions ------------------------------------- */
.lead      { font-size: var(--fs-20); line-height: var(--lh-snug); color: var(--text-secondary); }
.text-muted-gr { color: var(--text-muted); }
.caption   { font-size: var(--fs-14); color: var(--text-muted); }
.overline  {
  font-size: var(--fs-12);
  font-weight: var(--fw-semibold);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--link);
}

/* ---- Links ----------------------------------------------------------- */
a { color: var(--link); text-decoration: none; transition: color var(--dur-fast) var(--ease-out); }
a:hover { color: var(--link-hover); }

/* ---- Prices / numerals — align in lists and carts -------------------- */
/* nowrap: formatBDT renders "৳ 1,450" with a real space, so a narrow column
   (2-up cards at 375px) would otherwise break the symbol onto its own line. */
.price, .tabular { font-variant-numeric: tabular-nums; font-feature-settings: "tnum"; }
.price { white-space: nowrap; }
.price { font-weight: var(--fw-semibold); }
.price--strike { color: var(--text-muted); text-decoration: line-through; font-weight: var(--fw-regular); }

/* ---- Gradient text (used sparingly for a single hero accent word) ---- */
.text-gradient {
  background: var(--gr-gradient-ink);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  color: transparent;
}

/* ---- Arabic content inherits the Kufi fallback automatically --------- */
[lang="ar"], .arabic { font-family: 'Noto Kufi Arabic', var(--font-body); direction: rtl; }

/* ---- The Bangla on a heading -----------------------------------------
   Same argument and the same shape as .label-gr__bn (_forms.css): the
   customers this shop is built for read Bangla first, and the page that
   takes their money should not need translating.

   Sized in `em` so one rule serves the h1 and the h4 step titles alike, and
   set at regular weight and muted, so the heading still reads as one heading
   with a gloss rather than as two headings. line-height 1 because Bangla's
   default 1.75 would tug the heading's line box taller than its Latin half
   and shift everything under it — the layout must not move for this.

   NO nowrap here, unlike .btn-bn. On a button it is right: the button is a
   flex row and a wrapped label takes the button's height with it. On a
   heading it sets a min-content floor, and a grid column cannot shrink below
   its content — "Order Summary অর্ডার সারাংশ" held the checkout's summary
   column at 473px inside a 390px phone and scrolled the whole page sideways.
   A heading is allowed to wrap; that is what headings do. */
.head-gr__bn {
  font-family: 'Noto Sans Bengali', var(--font-body);
  font-weight: var(--fw-regular);
  font-size: 0.62em;
  line-height: 1;
  color: var(--text-muted);
  letter-spacing: 0;
}

/* The same Bangla, at the English's own size and colour — a second headline
   rather than a gloss on the first. For the places where the Bangla is not a
   translation offered to the side but half of the title itself; the deals
   card is the first. Weight stays regular: Noto Sans Bengali at 700 beside a
   Latin display face reads heavier than it, not equal to it.

   Inherits colour rather than naming one, because these headlines sit on
   coloured cards as often as on the page, and --text-muted is mixed for the
   page surface only. */
.head-gr__bn--full { font-size: 1em; color: inherit; }

/* ===== partials/_buttons.css ===== */
/* =========================================================================
   GulfRabit · Buttons
   Micro-interactions: slight scale/opacity shift on hover (not colour-snap).
   One radius (--radius-sm). Reuse — never restyle buttons per page.
   ========================================================================= */

.btn-gr {
  --_bg: var(--surface-sunken);
  --_fg: var(--text-primary);
  --_bd: var(--border-hairline);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  padding: 0.75rem 1.5rem;
  min-height: 48px;
  font-family: var(--font-body);
  font-size: var(--fs-16);
  font-weight: var(--fw-semibold);
  line-height: 1;
  color: var(--_fg);
  background: var(--_bg);
  border: 1px solid var(--_bd);
  border-radius: var(--radius-sm);
  cursor: pointer;
  user-select: none;
  text-align: center;
  transition: transform var(--dur-base) var(--ease-out),
              opacity var(--dur-base) var(--ease-out),
              background-color var(--dur-base) var(--ease-out),
              box-shadow var(--dur-base) var(--ease-out),
              border-color var(--dur-base) var(--ease-out);
}
/* The lift is a POINTER affordance. On touch, :hover applies on tap and stays,
   so every button on the site sat 2px high with a shadow after being pressed —
   permanently, until something else was tapped. Read as "this button is
   waiting for you" on a form somebody had already submitted. */
@media (hover: hover) {
  .btn-gr:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); }
}
/* Touch gets the press instead, and it is the more honest feedback anyway:
   the finger pushes down, so the button goes down. */
.btn-gr:active { transform: translateY(0); opacity: 0.9; }
.btn-gr:focus-visible {
  outline: 2px solid var(--gr-cyan);
  outline-offset: 2px;
}
.btn-gr:disabled { opacity: 0.45; cursor: not-allowed; transform: none; box-shadow: none; }

/* ---- Variants -------------------------------------------------------- */
.btn-primary-gr {
  --_bg: var(--gr-cyan);
  --_fg: #04222b;
  --_bd: transparent;
}
.btn-primary-gr:hover { --_bg: var(--gr-cyan-dark); --_fg: var(--text-inverse); box-shadow: var(--shadow-glow); }

.btn-gradient-gr {
  background: var(--gr-gradient);
  color: #04222b;
  border-color: transparent;
}
.btn-gradient-gr:hover { box-shadow: var(--shadow-glow); }

.btn-outline-gr {
  --_bg: transparent;
  --_fg: var(--text-primary);
  --_bd: var(--border-hairline);
}
.btn-outline-gr:hover { --_bd: var(--gr-cyan-dark); --_fg: var(--link); }

.btn-ghost-gr {
  --_bg: transparent;
  --_fg: var(--text-secondary);
  --_bd: transparent;
  min-height: 40px;
  padding: 0.5rem 0.9rem;
}
@media (hover: hover) {
  .btn-ghost-gr:hover { --_fg: var(--text-primary); background: var(--wash-hover); box-shadow: none; transform: none; }
}

/* On light sections */
.btn-outline-gr--light { --_fg: var(--text-primary); --_bd: var(--border-hairline); }
.btn-outline-gr--light:hover { --_bd: var(--gr-cyan-dark); --_fg: var(--gr-cyan-dark); }

/* ---- Sizes ----------------------------------------------------------- */
.btn-sm-gr { min-height: 38px; padding: 0.45rem 0.9rem; font-size: var(--fs-14); }
.btn-lg-gr { min-height: 56px; padding: 1rem 2rem; font-size: var(--fs-20); }
.btn-block-gr { width: 100%; }

/* ---- The Bangla on a call to action -----------------------------------
   Same argument as the form labels (see .label-gr__bn): most of this shop's
   customers read Bangla first, and the button that takes their money is the
   last place that should need translating.

   Everything here is sized so the button does not change. `em`, not rem, so
   the gloss tracks whatever size button it lands in — 11px inside a card's
   small button, 16px inside the product page's. The pair is set tight and
   the flex gap is pulled in, because "Add to Cart" already nearly filled the
   163px card button and a second word at the default 8px gap would have
   pushed it out. No padding, height or width is touched.

   nowrap on the Latin half: the button is a flex row, so without it a
   squeezed card would break "Add to Cart" over two lines and take the
   button's height with it — which is exactly the layout change this is not
   allowed to make. */
.btn-gr__en { white-space: nowrap; }
/* ...except on a full-width button, which has nowhere to escape sideways.
   nowrap makes the button's MINIMUM width its English plus its Bangla on one
   line, and a grid column cannot shrink below its content: "Proceed to
   Checkout চেকআউট" needed 334px inside a 336px summary column and pushed the
   whole cart page to 410px on a 390px phone — the page scrolled sideways.

   A block button is already as wide as its column, so a second line costs a
   little height and nothing else, and it only ever happens on a screen too
   narrow for one. Inline buttons keep their single line, where the nowrap is
   load-bearing for the row they sit in. */
/* Any bilingual button may stack its two halves when its row squeezes it.
   This is what keeps the nowrap above affordable: with the halves on one
   line a button's MINIMUM width is English + Bangla added together, and with
   wrap allowed it is only the wider of the two. The product page's Add to
   Cart sits beside a quantity stepper and was 10px past a 390px phone on that
   sum alone. Where there is room nothing wraps and nothing moves — this only
   fires when the alternative is a page that scrolls sideways.

   A full-width button additionally lets its English wrap: it is already as
   wide as its column, so there is no narrower place for it to go, and
   "Proceed to Checkout" on its own needed 334px of a 336px summary column. */
.btn-gr { flex-wrap: wrap; }
.btn-block-gr .btn-gr__en { white-space: normal; }
.btn-bn {
  font-size: 0.76em;
  font-weight: var(--fw-regular);
  line-height: 1;
  opacity: .82;
  white-space: nowrap;
  /* Cancels most of .btn-gr's 8px gap — the two halves are one label. */
  margin-inline-start: calc(0.3em - var(--space-2));
}

/* ---- Icon-only round button (header actions, steppers) --------------- */
.btn-icon-gr {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border-radius: var(--radius-pill);
  border: 1px solid transparent;
  background: transparent;
  color: var(--text-primary);
  cursor: pointer;
  position: relative;
  transition: background-color var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out);
}
.btn-icon-gr:hover { background: var(--wash-hover); color: var(--link); }
.btn-icon-gr:focus-visible { outline: 2px solid var(--gr-cyan); outline-offset: 2px; }

/* ===== partials/_cards.css ===== */
/* =========================================================================
   GulfRabit · Cards (product card, category card, generic surface)
   Gentle lift on hover (translateY -4px + shadow, 200ms). Radius --radius-lg.
   ========================================================================= */

/* ---- Generic elevated surface ---------------------------------------- */
.surface-gr {
  background: var(--surface-raised);
  border: 1px solid var(--border-hairline);
  border-radius: var(--radius-lg);
}

/* ---- Product card ---------------------------------------------------- */
.product-card {
  position: relative;
  display: flex;
  flex-direction: column;
  background: var(--surface-raised);
  border: 1px solid var(--border-hairline);
  border-radius: var(--radius-lg);
  overflow: hidden;
  transition: transform var(--dur-base) var(--ease-out),
              box-shadow var(--dur-base) var(--ease-out),
              border-color var(--dur-base) var(--ease-out);
}
/* Hover effects are gated on a device that HAS hover.
 *
 * Without the query, a phone applies :hover on tap and then keeps it until you
 * tap something else — so a card you touched stays lifted and outlined, which
 * reads as "selected" and is not. Every hover rule below is inside the guard;
 * touch gets the press state instead, further down. */
@media (hover: hover) {
  .product-card:hover {
    transform: translateY(-4px);
    box-shadow: var(--shadow-lg);
    border-color: var(--gr-cyan-dark);
  }
}

/* The press state, which is what a phone actually has. Settling INTO the page
 * rather than lifting out of it — the gesture is a push, and matching it is
 * the difference between a card that feels physical and one that feels like a
 * div that changed colour. */
.product-card:active { transform: scale(.985); }

@media (prefers-reduced-motion: reduce) {
  .product-card, .product-card__img, .product-card__actions { transition: none; }
  .product-card:active { transform: none; }
}

/* <picture> carries only the WebP source and must not become a layout box.
   .product-card__img is width/height 100%, which resolves against its PARENT
   — so an inline-block picture makes the image size to its own intrinsic
   ratio instead of the 4/5 frame. The photographs are square, so on a phone
   (frame 1/1) it looked right and on desktop (frame 4/5) it left a 60px gap
   under every card. display:contents removes the box and the bug. */
.product-card__media picture { display: contents; }

.product-card__media {
  position: relative;
  aspect-ratio: 4 / 5;
  background: var(--surface-sunken);
  overflow: hidden;
}
.product-card__img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* subtle unifying vignette so mixed stock photography reads as one system */
  transition: transform var(--dur-slow) var(--ease-out);
}
@media (hover: hover) {
  .product-card:hover .product-card__img { transform: scale(1.04); }
}
.product-card__media::after {
  content: "";
  position: absolute; inset: 0;
  box-shadow: inset 0 0 0 1px rgba(16,19,20,.05);
  pointer-events: none;
}

/* Wishlist / quick view.
 *
 * THESE USED TO BE opacity:0 UNTIL HOVER, WHICH MEANT THEY DID NOT EXIST ON A
 * PHONE. Three actions, unreachable, on the device most of this shop is
 * browsed from. They are visible by default now and the reveal is the
 * exception, applied only where a pointer can hover.
 *
 * On touch they sit at 40px and slightly translucent so they read as secondary
 * to the product image without becoming a guessing game. */
.product-card__actions {
  position: absolute;
  /* Inset and gap follow the buttons down from 40px to 32px — leaving them at
     the old spacing would have made the smaller circles look adrift in the
     corner rather than tucked into it. */
  top: var(--space-2); right: var(--space-2);
  display: flex; flex-direction: column; gap: 6px;
  z-index: 2;
  transition: opacity var(--dur-base) var(--ease-out), transform var(--dur-base) var(--ease-out);
}

.product-card__actions .btn-icon-gr {
  /* 32px, down from 40px.
     These sit ON the product photograph — the one thing the card exists to
     show — and on a 163px phone card a 40px circle covered a noticeable share
     of it. Below the 44px guideline deliberately, and defensibly: the target
     is a circle floating over an image with nothing mis-tappable beside it,
     the same action is available full-size on the product page, and losing a
     quarter of the photo to chrome costs more sales than a slightly smaller
     wishlist button. */
  width: 32px; height: 32px;
  /* The glyphs ship at 20px, which in a 32px circle leaves 6px of breathing
     room and reads as a cramped button. Sized here rather than in the markup
     because the hand-authored cards in index.html carry the same 20px SVGs. */
  opacity: .92;
  backdrop-filter: blur(6px);
  box-shadow: 0 1px 3px rgba(16, 19, 20, .12);
  transition: transform var(--dur-fast) var(--ease-out),
              opacity var(--dur-fast) var(--ease-out);
}
.product-card__actions .btn-icon-gr svg { width: 17px; height: 17px; }
.product-card__actions .btn-icon-gr:active { transform: scale(.9); opacity: 1; }

@media (hover: hover) {
  /* Where there IS a pointer, keep the original reveal — it is cleaner, and a
     mouse can find them the moment the card is approached. */
  .product-card__actions { opacity: 0; transform: translateX(8px); }

  .product-card:hover .product-card__actions,
  .product-card:focus-within .product-card__actions { opacity: 1; transform: translateX(0); }

  .product-card__actions .btn-icon-gr:hover { transform: scale(1.08); opacity: 1; }
}

/* flex-grow so the body fills whatever height the grid gave the card. Without
   it the body is only as tall as its content and the leftover space collects
   at the BOTTOM — so a card with no "Save ৳ 80" line, or a one-line title,
   put its Add to Cart button higher than its neighbours. Three different
   button heights across one row of New Arrivals, measured at 499/520/547.

   With the body stretched, .product-card__price-row's existing margin-top:auto
   finally has slack to push against, and price, saving and button all settle
   to the bottom edge — so the buttons line up whatever is above them. */
.product-card__body { display: flex; flex-direction: column; flex-grow: 1; gap: var(--space-2); padding: var(--space-4); }
.product-card__brand { font-size: var(--fs-12); color: var(--text-muted); text-transform: uppercase; letter-spacing: .08em; }
.product-card__title {
  font-family: var(--font-body);
  font-size: var(--fs-16);
  font-weight: var(--fw-medium);
  line-height: var(--lh-snug);
  color: var(--text-primary);
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.product-card__price-row { display: flex; align-items: baseline; gap: var(--space-2); margin-top: auto; }
.product-card__price { font-size: var(--fs-20); color: var(--text-primary); }
/* The CTA's offset from the price used to be an inline style on every card —
   duplicated in productCardHTML and eight times in index.html, so the phone
   layout could not tune it without !important. It is one rule now. */
.product-card__body .btn-gr { margin-top: var(--space-2); }
/* ---- Pack size, on the card -------------------------------------------
   Sits directly above Add to Cart, which is the order the decision is made
   in: how much, then buy. Deliberately small and quiet — this is a
   confirmation for most customers and a correction for the few who want a
   different size, so it must not out-shout the price above it.

   Wraps rather than scrolls: three chips fit a 163px phone card on one line,
   and a rare fourth dropping to a second line is better than a horizontally
   scrolling strip inside a card that is itself inside a scrolling rail. */
.size-chips { display: flex; flex-wrap: wrap; gap: 4px; margin-top: var(--space-2); }
.size-chip {
  flex: 0 1 auto;
  padding: 3px 8px;
  font-family: var(--font-body);
  font-size: var(--fs-12);
  font-weight: var(--fw-medium);
  line-height: 1.4;
  color: var(--text-secondary);
  background: var(--surface-sunken);
  border: 1px solid var(--border-hairline);
  border-radius: var(--radius-pill);
  cursor: pointer;
  white-space: nowrap;
  transition: border-color var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out),
              background-color var(--dur-fast) var(--ease-out);
}
@media (hover: hover) {
  .size-chip:hover:not(:disabled) { border-color: var(--gr-cyan-dark); color: var(--link); }
}
.size-chip.is-on {
  border-color: var(--gr-cyan-dark);
  background: var(--surface-raised);
  color: var(--text-primary);
  box-shadow: inset 0 0 0 1px var(--gr-cyan-dark);
}
/* A size that is out of stock still shows: knowing 1 kg exists but is gone is
   worth more than a row that silently omits it and looks like the shop never
   sold it. */
.size-chip:disabled { opacity: .45; cursor: not-allowed; text-decoration: line-through; }
.size-chip:focus-visible { outline: 2px solid var(--gr-cyan); outline-offset: 2px; }

.product-card__stars { display: inline-flex; align-items: center; gap: 1px; }
.product-card__stars svg { display: block; }

/* ---- Premium products carry a gold hairline ---------------------------
 *
 * The shop's whole claim is that everything is imported and premium, and until
 * now the only thing saying so on a card was a small gold badge competing with
 * a lime SALE badge for the same corner.
 *
 * :has() keys the treatment off the badge that is already rendered, so no
 * markup changes and nothing new to keep in step — a product tagged premium
 * gets the border because it got the badge.
 *
 * Restraint is the point. A hairline and a barely-there warm wash, not a gold
 * card: on a grid of twelve, anything louder stops reading as "this one is
 * special" and starts reading as decoration.
 *
 * :has() is Chrome 105+ / Safari 15.4+ / Firefox 121+. On anything older the
 * rule is simply skipped and the card renders normally — the badge still says
 * premium. That is why the treatment is additive decoration and carries no
 * information of its own: a browser without :has() must not lose a fact, only
 * an emphasis.
 */
.product-card:has(.badge-premium) {
  border-color: color-mix(in srgb, var(--gold-ink) 32%, var(--border-hairline));
  background:
    linear-gradient(180deg, color-mix(in srgb, var(--gold-ink) 3.5%, transparent), transparent 38%),
    var(--surface-raised);
}

@media (hover: hover) {
  .product-card:has(.badge-premium):hover {
    border-color: color-mix(in srgb, var(--gold-ink) 60%, transparent);
    box-shadow: 0 18px 48px rgba(138, 109, 34, .16), 0 6px 16px rgba(16, 19, 20, .07);
  }
}

/* ---- The card on a phone ---------------------------------------------
 *
 * Two cards fit across a 360px screen, which makes each about 160px wide.
 * Everything below is about what still works at that size — the desktop
 * proportions were designed for a 280px card and quietly fall apart.
 *
 * Measured against a 360×800 viewport, which is the common Android size in
 * Bangladesh, not a 390px iPhone.
 */
@media (max-width: 575.98px) {
  /* 4:5 is 200px of image on a 160px card, and with the body underneath one
     card nearly fills the fold. Square gives back 40px per card — the
     difference between seeing one row and seeing two. */
  .product-card__media { aspect-ratio: 1 / 1; }

  /* ---- The body is where the height went -----------------------------
   *
   * THE IMAGE IS NOT TOUCHED BELOW THIS LINE. The square media well above is
   * the product, and shrinking it to save space would be saving space by
   * selling less. Every reduction here is type and padding.
   *
   * Measured on a 393px viewport before this block: media 162px, body 156px.
   * The photo and the paragraph about the photo were the same size, and the
   * card cleared 310px — two of them and a phone screen is full.
   *
   * The body now runs ~116px, a 26% cut. Nothing was removed: brand, title,
   * price, the struck-through original and the CTA are all still there, set at
   * the size a 164px column actually wants rather than the size a 280px
   * desktop card was designed at.
   *
   * A third of the saving is leading, not type size. At 12px the body's
   * default 1.6 line-height reserves 19px per 12px line — nine wasted pixels
   * on every row, four rows deep. Each element below gets the leading its own
   * size needs.
   */
  .product-card__body { padding: 8px 10px 9px; gap: 2px; }

  .product-card__title {
    /* 14px → 12px. Two lines still clears ~34 characters at this width —
       "Medjool Dates — Premium Jumbo (1kg)" lands without truncating. */
    font-size: .75rem;
    line-height: 1.25;
    letter-spacing: -0.002em;
    -webkit-line-clamp: 2;
  }

  /* The price stays the loudest thing in the body — it drops by the same
     proportion as everything else, not more, so the hierarchy is unchanged. */
  .product-card__price { font-size: .875rem; }
  .product-card__price-row { gap: 5px; line-height: 1.15; }
  .price--strike { font-size: .75rem; }

  /* "Save ৳ 350" is the discount stated a THIRD time — the badge on the photo
     already says -19% and the struck-through ৳ 1,800 already says what it was.
     On a 280px desktop card that repetition is emphasis; in a 240px rail card
     on a phone it is a whole extra line of body for no new fact, and it is the
     line that pushed the premium card past the grid card's height. The PDP
     keeps it (.pdp-price .price-saving), where there is room to labour it. */
  .product-card .price-saving { display: none; }

  .product-card__brand {
    font-size: 9px;
    line-height: 1.25;
    letter-spacing: .07em;
    /* The origin is the whole proposition on an import shop — "Saudi Arabia"
       earns its line even at this size. One line, ellipsis if it runs. */
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }

  /* "Add to Cart" wraps to two lines in a 136px button and the card grows a
     ragged extra row. Smaller type, tighter tracking, one line.
     32px rather than 38: this is a secondary path — the whole card is a link
     to the PDP and the cart is reachable from the header — so it can sit under
     the 44px floor that primary controls hold to. It is still a 144×32 target,
     comfortably past WCAG 2.5.8's 24×24 minimum. */
  .product-card__body .btn-gr {
    font-size: 11px;
    min-height: 32px;
    margin-top: 6px;
    padding-inline: var(--space-2);
    letter-spacing: .01em;
    white-space: nowrap;
  }

  /* Stars are decoration at this size and the review count is unreadable.
     The rating still reaches assistive tech through the aria-label. */
  .product-card__stars svg { width: 11px; height: 11px; }
  /* .caption is 14px — larger than the product title once the title is 12px,
     which put the loudest type in the body on "(44)". The leading matters as
     much as the size: at 1.6 a 10px caption still reserves a 16px row. */
  .product-card__body .caption { font-size: 10px; line-height: 1.2; }

  .product-card__badges { top: 6px; left: 6px; gap: 3px; }
  .badge-gr { padding: 2px 6px; font-size: 9px; letter-spacing: .03em; }

  .product-card__actions { top: 6px; right: 6px; gap: 6px; }
  .product-card__actions .btn-icon-gr { width: 34px; height: 34px; }
}

/* ---- Category card --------------------------------------------------- */
/* The pack size the card price is for. Muted, and on the price row rather than
   in the title, because the size is a property of the price — the title is the
   same product at every rung of the ladder. */
.product-card__size {
  margin-left: auto;
  font-size: var(--fs-12);
  color: var(--text-muted);
  white-space: nowrap;
}

.category-card {
  position: relative;
  display: flex; flex-direction: column; justify-content: flex-end;
  min-height: 180px;
  padding: var(--space-5);
  border-radius: var(--radius-lg);
  border: 1px solid var(--border-hairline);
  background: var(--surface-raised);
  overflow: hidden;
  transition: transform var(--dur-base) var(--ease-out), box-shadow var(--dur-base) var(--ease-out), border-color var(--dur-base) var(--ease-out);
}
@media (hover: hover) {
  .category-card:hover { transform: translateY(-4px); box-shadow: var(--shadow-lg); border-color: var(--gr-cyan-dark); }
}
.category-card:active { transform: scale(.985); }

/* ---- The tile's picture, and the icon underneath it -------------------
   The photograph is the tile. The line icon that used to be the tile is
   still in the markup, centred in the same well, and the picture is laid
   over it — so an image that 404s, or a phone that gave up on the request,
   leaves the icon standing rather than an empty box. CSS and DOM order put
   the icon underneath; uncovering it needs the img's onerror, because a
   broken image is still painted and would sit on top of the thing it is
   supposed to fall back to.

   The well is square because the art is cropped square (see
   tools/gen-category-images.py) — no second crop happens here. */
.category-card__media {
  position: relative;
  display: flex; align-items: center; justify-content: center;
  width: 100%; aspect-ratio: 1 / 1;
  margin-bottom: var(--space-4);
  border-radius: var(--radius-lg);
  background: var(--surface-sunken);
  overflow: hidden;
}
.category-card__media picture { position: absolute; inset: 0; }
.category-card__img {
  display: block; width: 100%; height: 100%;
  object-fit: cover;
  /* Opaque, so the icon behind cannot show through a photo with soft edges. */
  background: var(--surface-sunken);
}
.category-card__icon { width: 40px; height: 40px; color: var(--link); }
/* Outside a media well the icon is the tile's whole visual, and needs the
   gap the photo's well supplies. */
.category-card > .category-card__icon { margin-bottom: var(--space-3); }
.category-card__title { font-family: var(--font-display); font-size: var(--fs-20); font-weight: var(--fw-semibold); }
.category-card__count { font-size: var(--fs-14); color: var(--text-muted); }

/* ---- Badges / tags --------------------------------------------------- */
.badge-gr {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 3px 9px;
  font-size: var(--fs-12);
  font-weight: var(--fw-semibold);
  letter-spacing: .04em;
  text-transform: uppercase;
  border-radius: var(--radius-sm);
  line-height: 1.4;
}
.badge-sale     { background: var(--gr-lime); color: var(--gr-ink); }
.badge-new      { background: rgba(27,180,212,.12); color: var(--link-hover); border: 1px solid rgba(27,180,212,.32); }
.badge-premium  { background: rgba(201,162,75,.16); color: var(--gold-ink); border: 1px solid rgba(201,162,75,.45); }
.badge-out      { background: var(--surface-sunken); color: var(--text-muted); border: 1px solid var(--border-hairline); }
.badge-origin   { background: var(--wash-hover); color: var(--text-secondary); border: 1px solid var(--border-hairline); text-transform: none; letter-spacing: 0; }

.product-card__badges { position: absolute; top: var(--space-3); left: var(--space-3); display: flex; flex-direction: column; gap: var(--space-1); z-index: 2; }

/* ---- Elevation on a white canvas -------------------------------------
   Card and page are both white now, so the hairline border does the
   separating and a very soft shadow supplies the depth. */
.surface-gr, .product-card, .category-card { box-shadow: var(--shadow-sm); }

/* ---- Absolute saving ---------------------------------------------------
   Sits with the price as a second signal to the percentage badge. Uses the
   accessible green (--lime-ink, 4.9:1) rather than raw --gr-lime, which is
   1.9:1 on white and unreadable as text. */
.price-saving {
  display: inline-block;
  font-size: var(--fs-12);
  font-weight: var(--fw-semibold);
  color: var(--lime-ink);
  font-variant-numeric: tabular-nums;
}
.pdp-price .price-saving { margin-left: var(--space-2); }

/* ===== partials/_navigation.css ===== */
/* =========================================================================
   GulfRabit · Navigation (header, mega-menu, mobile drawer, announcement bar)
   Sticky glass on scroll — backdrop blur + semi-transparent black once past
   the hero. This is a key "expensive" detail.
   ========================================================================= */

/* ---- Announcement bar ------------------------------------------------ */
.announce-bar {
  background: var(--gr-gradient);
  color: #04222b;
  font-size: var(--fs-14);
  font-weight: var(--fw-medium);
  text-align: center;
}
/* Block-centered (not flex) so long text wraps within the bar instead of
   forcing the document wider than the viewport on mobile. */
.announce-bar__inner { position: relative; text-align: center; padding: 8px 3rem; line-height: 1.35; max-width: var(--container-max); margin-inline: auto; }
/* The ✕ glyph is 11×14. Sized as a button rather than left to shrink-wrap it,
   because a bare glyph is what it was: an 11×14 target on every page of the
   site, under even WCAG 2.5.8's 24×24 minimum, sitting in the corner a thumb
   reaches for first. The mark stays the same size — only the box around it
   grows — and .announce-bar__inner's side padding went 2.75rem → 3rem so a
   44px box clears the centred text instead of landing on top of it. */
.announce-bar__close {
  position: absolute; right: 2px; top: 50%; transform: translateY(-50%);
  width: 44px; height: 44px;
  display: grid; place-items: center;
  background: none; border: 0; color: #04222b; cursor: pointer; opacity: .7; line-height: 1;
}
@media (hover: hover) {
  .announce-bar__close:hover { opacity: 1; }
}
.announce-bar[hidden] { display: none; }

/* ---- Header shell ---------------------------------------------------- */
.site-header {
  position: sticky; top: 0; z-index: var(--z-header);
  background: transparent;
  border-bottom: 1px solid transparent;
  transition: background-color var(--dur-slow) var(--ease-out),
              backdrop-filter var(--dur-slow) var(--ease-out),
              border-color var(--dur-slow) var(--ease-out),
              box-shadow var(--dur-slow) var(--ease-out);
}
/* toggled by header-nav.js once scrolled past a threshold */
.site-header.is-scrolled {
  /* Near-opaque by default. The glass value below is an ENHANCEMENT, applied
     only where backdrop-filter actually works — without the blur, 82% white
     is not frosted glass, it is a header you can read the page through, and
     the section overline ends up printed across the wordmark. */
  background: rgba(255,255,255,.97);
  border-bottom-color: var(--border-hairline);
  box-shadow: var(--shadow-md);
}
@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .site-header.is-scrolled {
    background: var(--glass-header);
    -webkit-backdrop-filter: blur(16px) saturate(140%);
    backdrop-filter: blur(16px) saturate(140%);
  }
}
.site-header__inner {
  display: flex; align-items: center; gap: var(--space-5);
  height: var(--header-h);
  max-width: var(--container-max);
  margin-inline: auto;
  padding-inline: var(--space-5);
}
/* Horizontal lockup: the square rabbit mark + a styled wordmark (the source
   logo is a square stack, so it reads tiny as a wide header image). */
.site-header__logo { display: inline-flex; align-items: center; gap: 9px; flex-shrink: 0; }
.site-header__logo img { height: 38px; width: 38px; display: block; }
.site-header__wordmark { font-family: var(--font-display); font-weight: var(--fw-bold); font-size: var(--fs-20); color: var(--link); letter-spacing: -0.01em; line-height: 1; }

/* Mobile header: trim to essentials so nothing overflows on small screens. */
@media (max-width: 991px) {
  .header-action-optional { display: none !important; }
  .site-header__inner { gap: var(--space-3); padding-inline: var(--space-4); }
  .site-header__wordmark { font-size: 1.1rem; }
}
@media (max-width: 360px) { .site-header__wordmark { display: none; } }

/* Small-desktop header (992–1399): the full nav appears at 992, and at full
   size its row is ~1140px of fixed-width content — nothing shrank, so the
   whole PAGE scrolled sideways on 1024–1120 laptops. Measured, not guessed.
   Everything stays visible; it just stops pretending the viewport is 1280:
   tighter run of gaps, smaller nav type, and a search box that is finally
   allowed to give ground (its input's min-width:auto was the anchor that
   forced the overflow — a flex child cannot shrink past its content without
   an explicit floor). */
@media (min-width: 992px) and (max-width: 1399.98px) {
  .site-header__inner { gap: var(--space-3); }
  .primary-nav { gap: 2px; }
  .nav-link { padding: 0.5rem 0.55rem; font-size: var(--fs-14); }
  .header-search { min-width: 150px; }
  .header-search__input { min-width: 0; }
}

/* The narrowest desktop slice cannot hold every ornament at once even
   tightened, so two give way in priority order: the wordmark (the rabbit
   mark still says who we are) and the account/wishlist icons (both live in
   the account pages and the footer; cart stays). Nav labels and the search
   box — the things a shopper actually uses — never degrade. */
@media (min-width: 992px) and (max-width: 1179.98px) {
  .site-header__wordmark { display: none; }
  .header-action-optional { display: none !important; }
}

/* ---- The category shortcuts give way before anything else ---------------
   The nav row was measured to be exactly full at six items. Adding Track
   Order made it seven and the whole PAGE scrolled sideways again — 3px at
   1440, 24px at 1280, 82px at 1024 — the same overflow the block above was
   written to kill.

   These three go first because they are the only items in the row that are
   duplicated: every one of them is inside the Shop mega-menu two pixels
   away. Track Order, Flash Sale and About are each reachable from nowhere
   else in the header, so losing one of those would cost a destination while
   losing these costs a shortcut.

   1500px, not 1400: the container caps at 1400 and the seven-item row
   measured 1423, so it overflowed its own container even on a 1440 laptop.

   Then Home joined the row as an eighth item and put 43px back on it: at
   exactly 1500 the page scrolled sideways by 3px again — the same bug, found
   the same way, by measuring every width from 992 to 1920. The threshold
   moves up with the row it guards. 1540 rather than the measured 1503, so a
   Bengali fallback face or a notch of browser zoom cannot walk it back over
   the edge.

   1580 now, not 1540: the 1540 figure was measured while the Home glyph was
   silently being crushed to 0px, so the row it was sized against was 21px
   narrower than the real one. With the glyph holding its width the row needs
   1544, and the threshold it sat exactly on gave back 4px of sideways
   scroll. Re-measured at every width from 992 to 2560 with the icon
   incompressible. */
@media (max-width: 1579.98px) {
  .nav-item-optional { display: none; }
}
.site-header__actions { display: flex; align-items: center; gap: var(--space-1); margin-left: auto; }

/* ---- Primary nav + mega-menu ----------------------------------------- */
/* Desktop nav appears at Bootstrap's lg (992px) so it lines up with the
   hamburger's .d-lg-none — no dead zone where neither shows. */
.primary-nav { display: none; }
@media (min-width: 992px) { .primary-nav { display: flex; align-items: center; gap: var(--space-2); } }

.nav-item { position: relative; }
.nav-link {
  display: inline-flex; align-items: center; gap: 6px;
  white-space: nowrap;   /* a nav label that wraps mid-phrase reads as a bug */
  padding: 0.6rem 0.9rem;
  font-size: var(--fs-16); font-weight: var(--fw-medium);
  color: var(--text-secondary);
  border-radius: var(--radius-sm);
  cursor: pointer; background: none; border: 0;
  transition: color var(--dur-fast) var(--ease-out);
}
.nav-link:hover, .nav-item.is-open .nav-link { color: var(--text-primary); }
/* animated gradient underline for the active section */
.nav-link[aria-current="page"]::after {
  content: ""; position: absolute; left: 0.9rem; right: 0.9rem; bottom: 6px; height: 2px;
  background: var(--gr-gradient); border-radius: var(--radius-pill);
}

/* A nav item that is a glyph rather than a word — Home. Square-ish padding so
   it reads as an icon button and not as a label that lost its text, and the
   active underline pulled in to match, since the 0.9rem inset above was
   written for a word's worth of box and would leave a 2px dash under a 21px
   icon. */
.nav-link--icon { padding: 0.6rem 0.7rem; }
.nav-link--icon[aria-current="page"]::after { left: 0.55rem; right: 0.55rem; }
/* A glyph is not a flexible thing, and this one was the softest item in the
   row. The header inner wants ~1453px inside a container that caps at 1400,
   so the nav is over-constrained at EVERY width where the category shortcuts
   show — widening the window never helps, because the container stops
   growing. Every label carries white-space:nowrap and every icon button has
   a width, which left an inline <svg> as the one child with no min-content
   floor: it absorbed the entire deficit and rendered 0px wide from 1540
   upward, leaving the active underline sitting under nothing at all. Seen on
   the live site, reproduced at 1540, 1600, 1800, 1920 and 2560. */
.nav-link--icon > svg { flex: none; }

/* Anchored to the left edge of the Shop item, not centred on it.
   Centred, an 880px panel hung ~180px off the left of a 1280px window and the
   entire first column was outside the viewport — measured: column one sat at
   x = -179, so "Imported Food" and its three links were simply not on screen.
   The width formula reserves the nav item's own offset so the panel cannot
   run off the right either at the 992px breakpoint where it first appears. */
.mega-menu {
  position: absolute; top: calc(100% + 10px); left: 0;
  transform: translateY(8px);
  width: min(816px, calc(100vw - 15rem));
  background: var(--surface-raised);
  border: 1px solid var(--border-hairline);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  padding: var(--space-6);
  opacity: 0; visibility: hidden; pointer-events: none;
  transition: opacity var(--dur-base) var(--ease-out), transform var(--dur-base) var(--ease-out), visibility var(--dur-base);
}
.nav-item.is-open .mega-menu { opacity: 1; visibility: visible; pointer-events: auto; transform: translateY(0); }
.mega-menu__grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--space-6); }
.mega-menu__col-title { font-size: var(--fs-12); text-transform: uppercase; letter-spacing: .1em; color: var(--link); margin-bottom: var(--space-3); }
.mega-menu__link { display: block; padding: 6px 0; color: var(--text-secondary); font-size: var(--fs-14); }
.mega-menu__link:hover { color: var(--text-primary); padding-left: 6px; transition: padding var(--dur-fast) var(--ease-out); }

/* ---- Search field in header ------------------------------------------ */
.header-search { display: none; flex: 1; max-width: 520px; }
@media (min-width: 992px) { .header-search { display: block; } }
.header-search__field {
  display: flex; align-items: center; gap: var(--space-2);
  height: 46px; padding: 0 var(--space-4);
  background: var(--surface-sunken);
  border: 1px solid var(--border-hairline);
  border-radius: var(--radius-pill);
  transition: border-color var(--dur-fast) var(--ease-out), box-shadow var(--dur-fast) var(--ease-out);
}
.header-search__field:focus-within { border-color: var(--gr-cyan-dark); box-shadow: 0 0 0 3px rgba(27,180,212,.15); }
/* min-width:0 at EVERY width, not only on the small-desktop slice below,
   because that is where the row's give has to come from. A flex child cannot
   shrink past its own content without an explicit floor, and the input's
   default min-width:auto made the search box the one rigid thing in a row
   that does not fit — so the deficit went looking for something else to
   crush and found the Home glyph. A search field that is 12px narrower than
   it wants is invisible to a shopper; a missing icon is not. */
.header-search__input { flex: 1; min-width: 0; background: none; border: 0; color: var(--text-primary); font-size: var(--fs-14); outline: none; }
.header-search__input::placeholder { color: var(--text-muted); }

/* ---- Count badge on cart / wishlist icons ---------------------------- */
.action-badge {
  position: absolute; top: 4px; right: 4px;
  min-width: 18px; height: 18px; padding: 0 5px;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 11px; font-weight: var(--fw-bold); line-height: 1;
  color: #04222b; background: var(--gr-lime);
  border-radius: var(--radius-pill);
  transform: scale(0); transition: transform var(--dur-base) var(--ease-out);
}
.action-badge.is-active { transform: scale(1); }

/* ---- Mobile offcanvas drawer ------------------------------------------
   A narrow category rail, not a full-width menu. It holds one flat list —
   the live categories, then the two account/company links — because the
   drawer it replaced spent its first row on a "Shop" accordion that hid the
   categories behind a tap, and the categories are the whole point of it.

   Sizing lives here rather than inline on the <aside> so it is one edit for
   all 26 pages the header is inlined into. Everything the JS toggles
   (transform via #mobileNav.is-open) still works off the class. */
.gr-offcanvas {
  position: fixed; top: 0; left: 0;
  height: 100%;
  width: clamp(208px, 56vw, 304px);
  z-index: 1050;
  display: flex; flex-direction: column;
  background: var(--surface-raised);
  color: var(--text-primary);
  border-right: 1px solid var(--border-hairline);
  /* Long and soft: at this width the drawer reads as a panel lifted off the
     page, and a tight shadow would read as a border with a smudge. */
  box-shadow: 24px 0 60px -20px rgba(10, 19, 22, .28);
  transform: translateX(-100%);
  transition: transform .34s var(--ease-out);
  overscroll-behavior: contain;
}
/* A single wash of brand colour behind the logo. The panel is white on white
   otherwise, and white on white at this width looks unfinished rather than
   minimal. */
.gr-offcanvas::before {
  content: ""; position: absolute; inset: 0 0 auto; height: 240px;
  pointer-events: none;
  background: linear-gradient(180deg, rgba(27,180,212,.075), rgba(154,205,60,.035) 42%, transparent);
}

.mobile-nav__head {
  position: relative; z-index: 1;
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: var(--space-2);
  padding: 14px 14px 16px 16px;
}
.mobile-nav__logo { width: 80px; height: auto; }
.mobile-nav__head .btn-icon-gr { margin-top: -2px; flex: none; }

.mobile-nav__body {
  position: relative; z-index: 1;
  flex: 1; min-height: 0;
  display: flex; flex-direction: column;
  overflow-y: auto;
  padding-bottom: var(--space-5);
}
.mobile-nav__list { list-style: none; margin: 0; padding: 0; }

.mobile-nav__link {
  position: relative;
  display: flex; align-items: center; gap: var(--space-2);
  min-height: 44px;                 /* the AAA target size, and the row rhythm */
  padding: 9px 16px;
  font-size: .9375rem;              /* 15 — one step under the body size */
  font-weight: var(--fw-semibold);
  letter-spacing: -.006em;
  line-height: 1.25;
  color: var(--text-primary);
  transition: background var(--dur-fast) var(--ease-out),
              padding-left var(--dur-base) var(--ease-out),
              color var(--dur-fast) var(--ease-out);
}
/* Hairlines between rows, inset to the text column. Ruled edge to edge they
   look like a table; inset they look like a considered list. */
.mobile-nav__list > li + li > .mobile-nav__link::before {
  content: ""; position: absolute; top: 0; left: 16px; right: 16px;
  height: 1px; background: var(--border-hairline);
}
/* Brand spine, scaled from nothing on press. Cheaper than a colour change and
   it points at the row the finger is on. */
.mobile-nav__link::after {
  content: ""; position: absolute; left: 0; top: 7px; bottom: 7px;
  width: 3px; border-radius: 0 3px 3px 0;
  background: var(--gr-gradient);
  transform: scaleY(0);
  transition: transform var(--dur-base) var(--ease-out);
}
@media (hover: hover) {
  .mobile-nav__link:hover { background: var(--wash-hover); padding-left: 21px; color: var(--link-hover); }
  .mobile-nav__link:hover::after { transform: scaleY(1); }
}
.mobile-nav__link:active { background: var(--wash-hover); }
.mobile-nav__link:active::after { transform: scaleY(1); }
.mobile-nav__link[aria-current="page"] { color: var(--link); }
.mobile-nav__link[aria-current="page"]::after { transform: scaleY(1); }

/* The lead row — Home — above the categories, ruled off from them. Its own
   block rather than a first row of the category list: it is the one row in
   the drawer that carries an icon, and a lone glyph in a column of plain
   labels reads as a stray until a hairline says it is a different thing. */
.mobile-nav__list--lead::after {
  content: ""; display: block; height: 1px; margin: var(--space-2) 16px;
  background: var(--border-hairline);
}
/* Never let the glyph squeeze: the label beside it can wrap, the icon cannot
   shrink. Brand cyan so the row reads as the way out to the front page. */
.mobile-nav__icon { flex: none; color: var(--link); }

/* Account / company. Pushed to the foot of the panel and set quieter, so the
   categories own the list and these stay reachable without competing. */
.mobile-nav__list--minor { margin-top: auto; padding-top: var(--space-3); }
.mobile-nav__list--minor::before {
  content: ""; display: block; height: 1px; margin: 0 16px var(--space-2);
  background: var(--border-hairline);
}
.mobile-nav__list--minor .mobile-nav__link {
  min-height: 40px;
  font-size: var(--fs-14);
  font-weight: var(--fw-medium);
  color: var(--text-secondary);
}
.mobile-nav__list--minor > li + li > .mobile-nav__link::before { display: none; }

/* The Bangla beside a drawer link. Same treatment as the buttons (.btn-bn):
   quieter, smaller, and on the same line — a second voice, not a second row.
   `em` so it tracks whatever size the link is. */
.mobile-nav__bn {
  font-size: 0.82em;
  font-weight: var(--fw-regular);
  color: var(--text-muted);
  margin-inline-start: .35em;
  /* Bangla's 1.75 default leading would make this row taller than its
     neighbours and break the drawer's even rhythm. */
  line-height: 1;
}

/* Rows arrive just behind the panel, ~26ms apart, so the drawer resolves into
   a list instead of appearing whole. --i is set per row in the markup and by
   category-menu.js when it re-renders from the API. */
#mobileNav.is-open .mobile-nav__link {
  animation: navRowIn .32s var(--ease-out) backwards;
  animation-delay: calc(var(--i, 0) * 26ms + 70ms);
}
@keyframes navRowIn {
  from { opacity: 0; transform: translateX(-10px); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .gr-offcanvas { transition: none; }
  #mobileNav.is-open .mobile-nav__link { animation: none; }
  .mobile-nav__link { transition: none; }
}

/* Kept for the API-rendered child rows in category-menu.js. */
.mobile-nav__sublink { display: block; padding: var(--space-2) var(--space-4); color: var(--text-secondary); font-size: var(--fs-16); }

/* ---- Search autocomplete panel --------------------------------------- */
.header-search { position: relative; }
.search-suggest {
  position: absolute; top: calc(100% + 8px); left: 0; right: 0; z-index: 5;
  background: var(--surface-raised);
  border: 1px solid var(--border-hairline);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  padding: var(--space-2);
  overflow: hidden;
}
.search-suggest__item { display: flex; align-items: center; gap: var(--space-3); padding: var(--space-2) var(--space-3); border-radius: var(--radius-sm); color: var(--text-secondary); }
.search-suggest__item:hover { background: var(--surface-sunken); color: var(--text-primary); }
.search-suggest__item img { border-radius: var(--radius-sm); object-fit: cover; background: var(--surface-sunken); }
.search-suggest__title { display: block; font-size: var(--fs-14); font-weight: var(--fw-medium); color: var(--text-primary); }

/* Overlay search variant sits inside .search-overlay__box (full-screen) */
.search-overlay .search-suggest { position: static; margin-top: var(--space-3); box-shadow: none; }

/* ---- Breadcrumb ------------------------------------------------------ */
.breadcrumb-gr { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); font-size: var(--fs-14); color: var(--text-muted); }
.breadcrumb-gr a { color: var(--text-muted); }
.breadcrumb-gr a:hover { color: var(--link); }
.breadcrumb-gr__sep { opacity: .5; }

/* ---- Search: popular queries + merchandised placeholder ----------------
   Rotation timing is driven from search-autocomplete.js; the transition and
   the resting look live here (context.md §2). */
.search-suggest__heading {
  padding: var(--space-2) var(--space-3) var(--space-1);
  font-size: var(--fs-12);
  font-weight: var(--fw-semibold);
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.search-suggest__item--query { color: var(--text-secondary); }
.search-suggest__item--query svg { color: var(--text-muted); flex-shrink: 0; }

.header-search__input::placeholder,
.search-overlay__box input::placeholder {
  transition: opacity var(--dur-base) var(--ease-out);
}
.header-search__input.is-swapping::placeholder,
.search-overlay__box input.is-swapping::placeholder { opacity: 0; }

/* ===== partials/_footer.css ===== */
/* =========================================================================
   GulfRabit · Footer

   The footer used to be styled entirely by inline attributes plus one stray
   <style> block in the partial itself, which meant it had exactly two
   layouts: a single stacked column, and a four-column desktop row. On a phone
   that produced a ~1,100px ribbon of link text — three headings and fourteen
   links, one per line, below every page on the site.

   Phones get two columns instead. Not because two is a magic number, but
   because these lists are short words: "About", "FAQ", "Home Decor" all fit a
   half-width column at 14px, so a second column costs nothing in wrapping and
   halves the scrolling.
   ========================================================================= */

.footer-grid {
  display: grid;
  gap: var(--space-6) var(--space-5);
  /* Two columns from the smallest phone up. The brand block spans both. */
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

/* The logo, the promise and the newsletter are one unit and never share a
   row with a link column — the email input needs the full width to stay a
   comfortable target. */
.footer-grid__brand { grid-column: 1 / -1; }

/* Shop is the longest list and the one people actually use, so on a phone it
   takes the full width and runs in two columns of its own, keeping Company
   and Customer Care side by side underneath rather than pushing them a
   screen further down. */
.footer-grid > nav[aria-label="Shop"] { grid-column: 1 / -1; }
.footer-grid > nav[aria-label="Shop"] .footer-links {
  columns: 2;
  column-gap: var(--space-5);
}
/* A list item must not be split across the column break. */
.footer-grid > nav[aria-label="Shop"] .footer-links > li {
  break-inside: avoid;
  -webkit-column-break-inside: avoid;
}
/* .stack-2's `* + *` margin lands on the first item of the second column too,
   which knocks the two columns half a line out of alignment. */
.footer-grid > nav[aria-label="Shop"] .footer-links > li + li { margin-top: 0; }
.footer-grid > nav[aria-label="Shop"] .footer-links > li { padding-block: 5px; }

.footer-links { font-size: var(--fs-14); }
/* The helpline and WhatsApp rows in Customer Care — icon and label on one
   line, revealed by footer-social.js only when site-config carries a value. */
.footer-contact { display: inline-flex; align-items: center; gap: .5rem; }
.footer-contact svg { flex: none; opacity: .8; }
/* Generous rows: at 14px the text is ~19px tall, and a footer link that is
   only as tall as its own text is a miss waiting to happen on a phone. */
.footer-links a { display: inline-block; padding-block: 5px; }

@media (min-width: 768px) {
  /* Desktop returns to the original arrangement: brand, then three columns. */
  .footer-grid { grid-template-columns: 2fr 1fr 1fr 1fr; gap: var(--space-7); }
  .footer-grid__brand,
  .footer-grid > nav[aria-label="Shop"] { grid-column: auto; }
  .footer-grid > nav[aria-label="Shop"] .footer-links { columns: 1; }
  .footer-grid > nav[aria-label="Shop"] .footer-links > li { padding-block: 0; }
  .footer-grid > nav[aria-label="Shop"] .footer-links > li + li { margin-top: var(--space-2); }
}

/* ---- Bottom bar ------------------------------------------------------- */
.footer-bottom {
  display: flex; flex-wrap: wrap; align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
}
.footer-pay { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; }

/* ---- Payment marks -----------------------------------------------------
   A white tile under every one of them, and not for decoration: these logos
   are drawn to sit on white, and the footer band is grey in the day themes
   and near-black in Noor. Visa's #222357 on Noor's lagoon would be a dark
   shape on a dark ground. The tile makes the row theme-proof and matches how
   every checkout page in the market presents the same five. */
.pay-mark {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  height: 34px; min-width: 56px; padding: 0 10px;
  background: #fff;
  border: 1px solid var(--border-hairline);
  border-radius: 7px;
}
.pay-mark img { height: 15px; width: auto; display: block; }
/* A symbol carries more optical weight per pixel than a wordmark. Set to the
   same 15px as the wordmarks, Mastercard's circles read as a smaller logo
   sitting in a bigger box — the eye measures the ink, not the line. */
.pay-mark--symbol img { height: 21px; }
/* Cash on delivery is a method, not a brand, so it is set rather than drawn.
   The colour is literal because the tile is white in every theme, and a
   --text-* token is mixed for the page surface, which here it is not on. */
.pay-mark--cod {
  font-size: var(--fs-12); font-weight: var(--fw-semibold);
  letter-spacing: .01em; white-space: nowrap; color: #04222b;
}
.footer-social { display: flex; gap: .75rem; align-items: center; }
.footer-legal { margin-top: var(--space-3); display: flex; flex-wrap: wrap; gap: 1rem; }

@media (max-width: 639.98px) {
  /* Centred and ordered by what a customer is actually looking for down
     here: the payment methods they can pay with, then who to follow, then
     the small print. The copyright goes last — it is a legal necessity, not
     information anybody came for. */
  .footer-bottom { justify-content: center; text-align: center; gap: var(--space-3); }
  .footer-pay { order: 1; justify-content: center; width: 100%; }
  .footer-social { order: 2; justify-content: center; width: 100%; }
  .footer-bottom__copy { order: 3; width: 100%; }
  .footer-legal { justify-content: center; }
}

/* ---- The footer brand lockup ------------------------------------------
   Mark plus type, horizontal, the same construction as the header's — one
   brand, drawn one way, in the two places it appears. It replaced the whole
   720×720 stacked logo squeezed into 104px, where the wordmark inside the
   raster came out around 8px tall and its Arabic line around 5px: unreadable
   at the size, and soft at every zoom, because it was a photograph of
   lettering rather than lettering.

   Sized in CSS rather than by attributes, because the reset forces
   height:auto — an attribute pair can only ever declare the intrinsic ratio,
   never the display size. */
.footer-logo {
  display: inline-flex; align-items: center; gap: 11px;
  /* The anchor wraps type now, so it would inherit the link colour and paint
     the whole lockup cyan. Each part states its own. */
  color: inherit;
}
/* 52px, not the header's 38: the footer column is wider and quieter, and the
   mark is the only picture in it. flex:none because a mark that shrinks is
   not a mark — the same lesson the nav's Home glyph taught. */
.footer-logo__mark { width: 52px; height: 52px; flex: none; display: block; }
.footer-logo__type { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
/* The header wordmark's face, weight and colour exactly, one step up in size
   because it is not competing with a nav row here. */
.footer-logo__word {
  font-family: var(--font-display); font-weight: var(--fw-bold);
  font-size: var(--fs-24); line-height: 1;
  color: var(--link); letter-spacing: -0.01em;
}
/* The slogan the raster used to hide. Small, letterspaced and muted: it is a
   signature under the name, not a second line of shouting. */
.footer-logo__tag {
  font-size: var(--fs-12); line-height: 1;
  color: var(--text-muted); letter-spacing: .07em;
}
@media (min-width: 768px) {
  .footer-logo { gap: 13px; }
  .footer-logo__mark { width: 60px; height: 60px; }
  .footer-logo__word { font-size: var(--fs-31); }
}

/* ===== partials/_forms.css ===== */
/* =========================================================================
   GulfRabit · Forms & inputs
   Consistent radius (--radius-sm), visible focus, clear error state.
   ========================================================================= */

.field-gr { display: flex; flex-direction: column; gap: var(--space-2); margin-bottom: var(--space-4); }
.label-gr { font-size: var(--fs-14); font-weight: var(--fw-medium); color: var(--text-secondary); }
.label-gr .req { color: var(--link); }

/* ---- The Bengali gloss beside a field label ---------------------------
   Most of this shop's customers read Bangla first, and "Thana / Upazila" in
   Latin script is a translation of a Bangla word back into English. The
   Bangla sits ON the label's own line as a quieter second voice — lighter
   weight, muted, a notch smaller — so the label is still one row and still
   scans as one thing. Anything bigger and the form reads as two forms.

   Inline, not flex: the label is plain inline content, so this needs no
   layout change and cannot disturb the ~200 other .label-gr on the site.
   .bn (see _fonts.css) brings Noto Sans Bengali with it. */
.label-gr__bn {
  font-weight: var(--fw-regular);
  font-size: 0.8125rem;
  color: var(--text-muted);
  /* Bangla's default 1.75 leading would tug the label's line box taller than
     its Latin neighbours and stagger the field tops. */
  line-height: 1;
}

.input-gr,
.select-gr,
.textarea-gr {
  width: 100%;
  min-height: 48px;
  padding: 0.75rem 1rem;
  font-family: var(--font-body);
  font-size: var(--fs-16);
  color: var(--text-primary);
  background: var(--surface-sunken);
  border: 1px solid var(--border-input);
  border-radius: var(--radius-sm);
  transition: border-color var(--dur-fast) var(--ease-out), box-shadow var(--dur-fast) var(--ease-out);
}
.textarea-gr { min-height: 120px; resize: vertical; line-height: var(--lh-normal); }
.input-gr::placeholder, .textarea-gr::placeholder { color: var(--text-muted); }
.input-gr:focus, .select-gr:focus, .textarea-gr:focus {
  outline: none;
  border-color: var(--gr-cyan-dark);
  box-shadow: 0 0 0 3px rgba(27,180,212,.15);
}

/* Select caret */
.select-gr {
  appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23667069' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 1rem center;
  padding-right: 2.5rem;
}

/* ---- The "Sort by" control -------------------------------------------
   Shared rather than owned by catalog, because three modules render the same
   control — the category listing, search results and deals — and the first
   version of this lived in catalog.css, which deals does not load. A control
   that exists on a page whose stylesheet cannot style it is the same coupling
   bug as .summary-row in checkout; once is a mistake, twice is a pattern.

   width:auto so it is as wide as its widest option rather than the full width
   every other select on the site wants to be. */
.sort-field { display: flex; align-items: center; gap: .5rem; }
.sort-field .select-gr { min-height: 40px; width: auto; }

/* On a phone the word "Sort" goes and the control shrinks to the size of the
   button beside it. The meaning does not go with the word — the select keeps
   its aria-label, so a screen reader still announces "Sort by".

   Capped rather than auto-width: "Price: Low to High" is the widest option,
   and sizing to it would make the control twice the button it sits next to.
   The closed select ellipsises at the cap, which is what a phone's native
   picker does anyway the moment it opens. */
@media (max-width: 767px) {
  .sort-field { margin-left: auto; flex: none; }
  .sort-field__label { display: none; }
  .sort-field .select-gr {
    min-height: 38px; height: 38px;
    max-width: 132px;
    padding: 0 1.9rem 0 .7rem;
    font-size: var(--fs-14);
    background-position: right .6rem center;
    background-color: var(--surface-raised);
    border-color: var(--border-hairline);
    text-overflow: ellipsis;
  }
}

/* Validation states (toggled by utils/validate-form.js) */
.field-gr.is-invalid .input-gr,
.field-gr.is-invalid .select-gr,
.field-gr.is-invalid .textarea-gr { border-color: var(--gr-error); box-shadow: 0 0 0 3px rgba(229,72,77,.14); }
.field-error { font-size: var(--fs-12); color: var(--gr-error); min-height: 1em; }
.field-gr.is-valid .input-gr { border-color: var(--gr-success); }

/* Checkbox / radio */
.check-gr { display: inline-flex; align-items: flex-start; gap: var(--space-2); cursor: pointer; font-size: var(--fs-14); color: var(--text-secondary); }
.check-gr input { width: 20px; height: 20px; accent-color: var(--gr-cyan); margin-top: 1px; }

/* Inputs on light sections (checkout, contact on off-white) */
.field-gr--light .label-gr { color: var(--text-primary); }
.field-gr--light .input-gr,
.field-gr--light .select-gr,
.field-gr--light .textarea-gr {
  color: var(--text-primary); background: var(--surface-raised); border-color: var(--border-input);
}
.field-gr--light .input-gr::placeholder { color: var(--text-muted); }

/* Quantity stepper */
.qty-stepper { display: inline-flex; align-items: center; border: 1px solid var(--border-input); border-radius: var(--radius-sm); overflow: hidden; background: var(--surface-sunken); }
.qty-stepper__btn { width: 42px; height: 46px; display: grid; place-items: center; background: none; border: 0; color: var(--text-primary); cursor: pointer; font-size: var(--fs-20); transition: background var(--dur-fast) var(--ease-out); }
.qty-stepper__btn:hover:not(:disabled) { background: var(--wash-hover); color: var(--link); }
.qty-stepper__btn:disabled { opacity: .4; cursor: not-allowed; }
.qty-stepper__input { width: 48px; height: 46px; text-align: center; background: none; border: 0; color: var(--text-primary); font-variant-numeric: tabular-nums; font-weight: var(--fw-semibold); outline: none; }

/* ---- Honeypot -----------------------------------------------------------
   A field for bots. Offscreen rather than display:none, because the cruder
   form-fillers skip anything display:none but happily complete what is merely
   out of view. aria-hidden + tabindex=-1 keep it out of every human path:
   screen readers never announce it, Tab never lands on it. */
.hp-field {
  position: absolute !important;
  left: -9999px !important;
  width: 1px; height: 1px;
  overflow: hidden;
  opacity: 0;
  pointer-events: none;
}

/* ===== partials/_modals-offcanvas.css ===== */
/* =========================================================================
   GulfRabit · Cart drawer & modals (quick view)
   Re-skins Bootstrap offcanvas/modal so nothing looks "Bootstrap default".
   ========================================================================= */

/* ---- Cart drawer (offcanvas from the right) -------------------------- */
.cart-drawer { width: min(420px, 92vw); background: var(--surface-raised); color: var(--text-primary); border-left: 1px solid var(--border-hairline); }
.cart-drawer__header { display: flex; align-items: center; justify-content: space-between; padding: var(--space-5); border-bottom: 1px solid var(--border-hairline); }
.cart-drawer__title { font-family: var(--font-display); font-size: var(--fs-20); font-weight: var(--fw-semibold); }
.cart-drawer__body { flex: 1; overflow-y: auto; padding: var(--space-4) var(--space-5); }
.cart-drawer__footer { padding: var(--space-5); border-top: 1px solid var(--border-hairline); background: var(--surface-page); }

.cart-line { display: grid; grid-template-columns: 64px 1fr auto; gap: var(--space-3); padding: var(--space-3) 0; border-bottom: 1px solid var(--border-hairline); }
.cart-line__thumb { width: 64px; height: 64px; border-radius: var(--radius-sm); object-fit: cover; background: var(--surface-sunken); }
.cart-line__title { font-size: var(--fs-14); font-weight: var(--fw-medium); line-height: var(--lh-snug); }
.cart-line__meta { font-size: var(--fs-12); color: var(--text-muted); }
.cart-line__price { font-variant-numeric: tabular-nums; font-weight: var(--fw-semibold); white-space: nowrap; }
.cart-line__remove { background: none; border: 0; color: var(--text-muted); cursor: pointer; font-size: var(--fs-12); }
.cart-line__remove:hover { color: var(--gr-error); }

.cart-summary-row { display: flex; justify-content: space-between; font-size: var(--fs-14); color: var(--text-secondary); margin-bottom: var(--space-2); }
.cart-summary-row--total { font-size: var(--fs-20); color: var(--text-primary); font-weight: var(--fw-semibold); padding-top: var(--space-3); border-top: 1px solid var(--border-hairline); margin-top: var(--space-3); }

/* ---- Empty cart state ------------------------------------------------ */
.cart-empty { text-align: center; padding: var(--space-8) var(--space-4); color: var(--text-muted); }
.cart-empty__icon { width: 56px; height: 56px; margin: 0 auto var(--space-4); color: var(--border-input); }

/* ---- Quick-view / generic modal -------------------------------------- */
.gr-modal .modal-content { background: var(--surface-raised); border: 1px solid var(--border-hairline); border-radius: var(--radius-lg); color: var(--text-primary); box-shadow: var(--shadow-lg); }
.gr-modal .modal-header, .gr-modal .modal-footer { border-color: var(--border-hairline); }

/* ---- Backdrop (shared) ----------------------------------------------- */
.offcanvas-backdrop.show, .modal-backdrop.show { background: var(--scrim); opacity: 1; -webkit-backdrop-filter: blur(2px); backdrop-filter: blur(2px); }

/* ---- Full-screen search overlay -------------------------------------- */
.search-overlay {
  position: fixed; inset: 0; z-index: var(--z-modal);
  background: var(--glass-overlay);
  -webkit-backdrop-filter: blur(20px); backdrop-filter: blur(20px);
  display: flex; flex-direction: column; align-items: center;
  padding-top: 12vh;
  opacity: 0; visibility: hidden;
  transition: opacity var(--dur-slow) var(--ease-out), visibility var(--dur-slow);
}
.search-overlay.is-open { opacity: 1; visibility: visible; }
.search-overlay__box { width: min(720px, 92vw); }

/* ===== partials/_animations.css ===== */
/* =========================================================================
   GulfRabit · Animations, skeletons, scrollbar, toasts
   Subtle only — 150–300ms, no bounce. Respects prefers-reduced-motion.
   ========================================================================= */

/* ---- Scroll reveal (IntersectionObserver adds .is-visible) -----------
   Scoped to html:not(.no-js). An inline script in <head> drops .no-js before
   first paint, so with JavaScript enabled nothing changes — but with it off,
   these rules never apply and the content is simply visible. Unscoped, this
   was opacity:0 with nothing left to remove it, which hid most of the Sourcing
   page, the About values and much of the home page from anyone without JS. */
html:not(.no-js) [data-reveal] {
  opacity: 0;
  /* A shade of scale with the rise. On its own, translateY reads as content
     sliding into place; with the scale it reads as content arriving — the
     difference is small enough that nobody names it and large enough that the
     page feels made rather than assembled. .98, not .9: anything more and a
     grid of eight tiles pulses, which is a distraction, not a flourish. */
  transform: translateY(18px) scale(.98);
  /* Longer than --dur-slow and on its own curve: a reveal that finishes in
     300ms is a flicker at the moment a section comes into view. This one
     decelerates hard and settles, which is what makes it feel expensive. */
  transition: opacity 620ms var(--ease-reveal), transform 620ms var(--ease-reveal);
  will-change: opacity, transform;
  transition-delay: var(--reveal-delay, 0ms);   /* optional stagger, set inline */
}
html:not(.no-js) [data-reveal].is-visible { opacity: 1; transform: translateY(0) scale(1); }

/* ---- Skeleton shimmer ------------------------------------------------ */
.skeleton {
  position: relative;
  overflow: hidden;
  background: var(--surface-sunken);
  border-radius: var(--radius-sm);
}
.skeleton::after {
  content: "";
  position: absolute; inset: 0;
  transform: translateX(-100%);
  background: linear-gradient(90deg, transparent, rgba(255,255,255,.9), transparent);
  animation: gr-shimmer 1.4s infinite;
}
@keyframes gr-shimmer { 100% { transform: translateX(100%); } }
.skeleton--text { height: 0.8em; margin-bottom: 0.5em; }
.skeleton--media { aspect-ratio: 4/5; border-radius: var(--radius-lg); }

/* ---- Toast ----------------------------------------------------------- */
.toast-stack {
  position: fixed; z-index: var(--z-toast);
  right: var(--space-5); bottom: var(--space-5);
  display: flex; flex-direction: column; gap: var(--space-3);
  width: min(360px, calc(100vw - 2rem));
}
@media (max-width: 767px) { .toast-stack { left: 50%; right: auto; transform: translateX(-50%); bottom: var(--space-4); align-items: center; } }
.toast-gr {
  display: flex; align-items: center; gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  background: var(--surface-sunken);
  border: 1px solid var(--border-hairline);
  border-left: 3px solid var(--gr-cyan);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-lg);
  color: var(--text-primary);
  font-size: var(--fs-14);
  transform: translateX(120%);
  animation: gr-toast-in var(--dur-slow) var(--ease-out) forwards;
}
.toast-gr.is-leaving { animation: gr-toast-out var(--dur-base) var(--ease-out) forwards; }
.toast-gr--success { border-left-color: var(--gr-lime); }
.toast-gr--error   { border-left-color: var(--gr-error); }
.toast-gr__icon { flex-shrink: 0; width: 20px; height: 20px; }
@keyframes gr-toast-in  { to { transform: translateX(0); } }
@keyframes gr-toast-out { to { transform: translateX(120%); opacity: 0; } }

/* ---- Fade/scale in for modals & overlays ----------------------------- */
@keyframes gr-fade-up { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } }
.animate-fade-up { animation: gr-fade-up var(--dur-slow) var(--ease-out) both; }

/* ---- Custom scrollbar (default browser bars look cheap) -------------- */
* { scrollbar-width: thin; scrollbar-color: var(--gr-cyan-dark) transparent; }
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: var(--surface-page); }
::-webkit-scrollbar-thumb { background: var(--scrollbar-thumb); border-radius: var(--radius-pill); border: 2px solid var(--surface-page); }
::-webkit-scrollbar-thumb:hover { background: var(--gr-cyan-dark); }

/* ---- Reduced motion -------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: .001ms !important; animation-iteration-count: 1 !important; transition-duration: .001ms !important; scroll-behavior: auto !important; }
  [data-reveal] { opacity: 1; transform: none; }
}

/* ---- Images settle in, rather than popping -----------------------------
 *
 * Driven by shared/js/components/image-settle.js. The selector deliberately
 * requires BOTH classes to hide anything: `.settle` is added by the script, so
 * an image is only ever hidden by code that is running and will un-hide it. If
 * the script fails or never loads, nothing here applies and every photo shows
 * normally — the same no-js contract as [data-reveal].
 */
img.settle {
  opacity: 0;
  transform: scale(1.015);
  /* Slower fade than movement: the eye reads the opacity change as "arriving"
     and the scale as "settling", and matching their durations makes it look
     like one mechanical slide instead. */
  transition: opacity 420ms var(--ease-out), transform 620ms var(--ease-out);
}

img.settle.is-settled {
  opacity: 1;
  transform: none;
}

@media (prefers-reduced-motion: reduce) {
  /* The script bails before adding the class, but a stylesheet that only works
     because a script agreed with it is a stylesheet waiting to be wrong. */
  img.settle { opacity: 1; transform: none; transition: none; }
}

/* ===== partials/_utilities.css ===== */
/* =========================================================================
   GulfRabit · Utilities & layout primitives
   Small, reusable helpers that Tailwind/Bootstrap don't express cleanly.
   ========================================================================= */

/* ---- Container & section rhythm -------------------------------------- */
.gr-container { max-width: var(--container-max); margin-inline: auto; padding-inline: var(--space-5); }
@media (min-width: 1024px) { .gr-container { padding-inline: var(--space-6); } }

/* ---- Section rhythm ---------------------------------------------------
   64px top AND 64px bottom meant 128px of nothing between every pair of
   sections on a phone — a third of the viewport, repeated six times down the
   home page, so a customer scrolled past more blank than shop. Desktop, where
   there is room for that rhythm, is unchanged.

   24/16 rather than a single number: the gap between two sections is what was
   wrong (128px -> 40px, the ~70% asked for), and splitting it that way gives
   a section's heading more air above it than its last row needs below —
   which is the direction the eye reads anyway. */
/* Section rhythm. The number that matters is the one BETWEEN two sections,
   which is this padding twice — and at --space-10 that was 256px of nothing
   on a desktop screen, so the page read as a sequence of separate pages
   rather than one shop. Cut by three quarters: 64px between sections at
   desktop, 32px at tablet.

   Phones keep theirs. They were already at 16+24 = 40px between sections
   (tuned when the category grid was rebuilt for 390px), and taking three
   quarters off THAT leaves 10px, which is not a gap — it is a mistake. */
.section-gr { padding-block: var(--space-5) var(--space-4); }
@media (min-width: 768px)  { .section-gr { padding-block: var(--space-4); } }
@media (min-width: 1024px) { .section-gr { padding-block: var(--space-6); } }

/* A section that carries less weight than its neighbours — a logo wall, a
   newsletter strip — and says so with less room. Only at desktop sizes: the
   phone rhythm above is already tight, and taking more off it would make the
   section read as attached to the one before it. */
/* --tight only means anything relative to the base, and the base moved. At
   --space-7 it was 48px against a 128px section — tighter. Against the 32px
   section it became LOOSER than the thing it modifies, which is the modifier
   doing the opposite of its name. Cut with the base. */
@media (min-width: 768px) { .section-gr--tight { padding-block: var(--space-5); } }

/* ---- Spacing that has to respond, and therefore cannot be inline ------
   These three replaced `style="padding-block:…"` / `style="margin-top:…"`
   attributes repeated across 21 page fragments. A style attribute beats every
   stylesheet rule and answers no media query, so those pages kept their
   desktop spacing on a 390px phone no matter what the section rhythm said —
   96px of nothing above the footer, 96px between a product's tabs and its
   related rail. The values below are the authored desktop ones; the phone
   gets roughly a third of them, in line with .section-gr above. */
.page-main { padding-block: var(--space-5) var(--space-6); }
.page-head { margin-bottom: var(--space-4); }
.page-head--lg { margin-bottom: var(--space-5); }
.section-sep { margin-top: var(--space-6); }
.section-sep--lg { margin-top: var(--space-6); }
@media (min-width: 768px) {
  .page-main { padding-block: var(--space-6) var(--space-9); }
  .page-head { margin-bottom: var(--space-6); }
  .page-head--lg { margin-bottom: var(--space-7); }
  .section-sep { margin-top: var(--space-8); }
  .section-sep--lg { margin-top: var(--space-9); }
}

/* Light / dark section canvases (80/20 neutral system) */
.canvas-dark  { background: var(--surface-page);     color: var(--text-primary); }
.canvas-panel { background: var(--surface-band);   color: var(--text-primary); }
.canvas-light { background: var(--surface-band);   color: var(--text-primary); }
.canvas-light .text-muted-gr { color: var(--text-muted); }

/* ---- Section heading block ------------------------------------------- */
.section-head { display: flex; flex-wrap: wrap; align-items: end; justify-content: space-between; gap: var(--space-4); margin-bottom: var(--space-6); }
/* The serif lands HERE and nowhere else — on the line that names a section,
   on every page. Not on product names, not on prices, not on buttons: those
   are read, and Playfair's thin strokes cost legibility at the sizes and
   weights they use. Clash Display's tight tracking was cut for a geometric
   sans; a serif at -0.022em collides, so the letter-spacing is relaxed and
   the optical size stepped up a little to match Clash's x-height. */
.section-head h2,
.section-head .h2 {
  font-family: var(--font-luxe);
  font-weight: 600;
  letter-spacing: -0.012em;
  font-size: clamp(2rem, 4.2vw, 2.7rem);
  line-height: 1.12;
}
/* The eyebrow above it: brand colour, and spaced out so it reads as a label
   for the serif rather than a small line of the same voice. */
.section-head .overline { letter-spacing: .16em; }
.section-head__titles { display: flex; flex-direction: column; gap: var(--space-2); }

/* ---- The section header on a phone ------------------------------------
 *
 * Every shelf opened with a header sized for a 1400px canvas: a 12px eyebrow
 * over a 1.9rem title, 24px of air beneath it. At 393px that block was taller
 * than the product photo it was introducing — the header announced the shelf
 * more loudly than the shelf sold anything.
 *
 * The whole thing comes down to 75%. Not the title alone: shrinking the h2 and
 * leaving the eyebrow, the trailing link and the gaps at full size just moves
 * the imbalance around. Type, tracking, gaps and the chrome beside it all step
 * down together, which is the only way a smaller header still reads as one
 * deliberate object rather than a title someone typed in the wrong size.
 *
 * .home-cat overrides this further for the category directory (higher
 * specificity, modules/home/home.css) — that block still wins where it applies.
 */
@media (max-width: 575.98px) {
  /* align-items stays `end` from the base rule. Baseline is the tempting
     choice for "put the link on the title's line" and it is wrong here:
     .section-head__titles is a flex column, so its baseline is its FIRST
     line — the eyebrow. Everything beside it then hangs off "CURATED FOR
     YOU" and floats a full line above the heading it belongs to. */
  .section-head {
    gap: var(--space-3);
    margin-bottom: var(--space-4);
  }
  .section-head__titles { gap: 1px; min-width: 0; }

  /* 12px → 10px. Uppercase at 10px needs its tracking eased back or the
     letters stop cohering into words; .12em is the measured floor here. */
  .section-head .overline {
    font-size: .625rem;
    letter-spacing: .12em;
  }

  /* 1.9rem → 1.425rem, exactly 75%, matching the category directory. */
  .section-head h2 {
    font-size: 1.425rem;
    line-height: 1.18;
    letter-spacing: -0.018em;
  }

  /* "Shop all →" at 16px in a bordered pill now outweighs the title it sits
     beside. A text link with a padded tap area carries the same job — the
     same trade .home-cat already made, applied to every shelf. */
  .section-head > .btn-gr {
    flex: none;
    min-height: 0;
    padding: .4rem 0;
    border: 0;
    background: none;
    box-shadow: none;
    font-size: var(--fs-14);
    font-weight: var(--fw-semibold);
    color: var(--link);
    white-space: nowrap;
  }
  .section-head > .btn-gr:hover,
  .section-head > .btn-gr:active { background: none; box-shadow: none; transform: none; }

  /* Rail arrows follow the title down — 44px circles either side of a
     1.425rem heading read as the primary control on the row. */
  .section-head .btn-icon-gr { width: 34px; height: 34px; }
  .section-head .btn-icon-gr svg { width: 17px; height: 17px; }
}

/* ---- Gradient divider ------------------------------------------------ */
.divider-gradient { height: 2px; border: 0; background: var(--gr-gradient); border-radius: var(--radius-pill); opacity: .8; }
.divider-hairline { height: 1px; border: 0; background: var(--border-hairline); }

/* ---- Horizontal scroll-snap rail (product carousels) ----------------- */
.snap-rail { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(240px, 1fr); gap: var(--space-4); overflow-x: auto; scroll-snap-type: x mandatory; padding-bottom: var(--space-3); scrollbar-width: none; }
.snap-rail::-webkit-scrollbar { display: none; }
.snap-rail > * { scroll-snap-align: start; }
@media (min-width: 1024px) { .snap-rail { grid-auto-columns: minmax(280px, 1fr); } }

/* ---- Responsive product grid ----------------------------------------- */
.product-grid { display: grid; gap: var(--space-4); grid-template-columns: repeat(2, 1fr); }
@media (min-width: 768px)  { .product-grid { grid-template-columns: repeat(3, 1fr); gap: var(--space-5); } }
@media (min-width: 1024px) { .product-grid { grid-template-columns: repeat(4, 1fr); } }

/* ---- Trust / feature strip ------------------------------------------- */
.trust-strip { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--space-4); }
@media (min-width: 768px) { .trust-strip { grid-template-columns: repeat(4, 1fr); } }
.trust-item { display: flex; align-items: center; gap: var(--space-3); min-width: 0; }
.trust-item > div { min-width: 0; }               /* let text wrap instead of overflowing the grid track */
.trust-item__icon { width: 32px; height: 32px; color: var(--gold-ink); flex-shrink: 0; }
.trust-item__title { font-size: var(--fs-14); font-weight: var(--fw-semibold); }
.trust-item__sub { font-size: var(--fs-12); color: var(--text-muted); }

/* ---- Accessibility helpers ------------------------------------------- */
.visually-hidden { position: absolute !important; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
.skip-link { position: absolute; left: var(--space-4); top: -60px; z-index: calc(var(--z-header) + 1); background: var(--gr-cyan); color: #04222b; padding: 0.6rem 1rem; border-radius: var(--radius-sm); font-weight: var(--fw-semibold); transition: top var(--dur-base) var(--ease-out); }
.skip-link:focus { top: var(--space-4); }

/* ---- Empty-state block ----------------------------------------------- */
.empty-state { text-align: center; padding: var(--space-9) var(--space-4); }
.empty-state__icon { width: 64px; height: 64px; margin: 0 auto var(--space-4); color: var(--border-input); }
.empty-state__title { font-family: var(--font-display); font-size: var(--fs-25); margin-bottom: var(--space-2); }
.empty-state__text { color: var(--text-muted); max-width: 40ch; margin: 0 auto var(--space-5); }

/* ---- Compare tray (floating, global) --------------------------------- */

/* ---- No-JS fallback note --------------------------------------------- */
.noscript-note {
  display: block;
  padding: var(--space-5);
  border: 1px dashed var(--border-hairline);
  border-radius: var(--radius-lg);
  background: var(--surface-raised);
  color: var(--text-secondary);
  text-align: center;
}
.noscript-note a { font-weight: var(--fw-semibold); }

/* ---- Spacing helpers on the 8px grid --------------------------------- */
.stack-2 > * + * { margin-top: var(--space-2); }
.stack-3 > * + * { margin-top: var(--space-3); }
.stack-4 > * + * { margin-top: var(--space-4); }
.stack-5 > * + * { margin-top: var(--space-5); }
.stack-6 > * + * { margin-top: var(--space-6); }

/* ===== partials/_tabbar.css ===== */
/* =========================================================================
   GulfRabit · Bottom tab bar (phones)
   Injected by shared/js/components/mobile-tabbar.js on browse pages only —
   pages that pin their own bottom bar (cart, express, product) never get it.
   ========================================================================= */

.tabbar {
  position: fixed;
  inset-inline: 0;
  bottom: 0;
  /* Under the header's dropdowns and every overlay: drawer 1050, modal 1090,
     toast 1100. Above page content, including the sticky compare-style rails. */
  z-index: 990;
  display: none;
  grid-template-columns: repeat(4, 1fr);
  background: rgba(255, 255, 255, .96);
  -webkit-backdrop-filter: blur(14px) saturate(140%);
  backdrop-filter: blur(14px) saturate(140%);
  border-top: 1px solid var(--border-hairline);
  padding-bottom: env(safe-area-inset-bottom, 0px);
}
/* The glass needs the same honesty as the header: without backdrop-filter,
   96% white is a page showing through the nav. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .tabbar { background: rgba(255, 255, 255, .99); }
}

.tabbar__item {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  min-height: 56px;
  padding: 6px 2px 7px;
  border: 0;
  background: none;
  font: inherit;
  font-size: 11px;
  font-weight: var(--fw-medium);
  line-height: 1;
  color: var(--text-muted);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.tabbar__item svg { width: 22px; height: 22px; }
.tabbar__item.is-active { color: var(--link); }
.tabbar__item:active { color: var(--gr-cyan-dark); }

/* The Bangla under the English, both tiny — same pairing as the buttons. */
.tabbar__bn { font-size: 9px; line-height: 1; opacity: .8; }

.tabbar__iconwrap { position: relative; display: inline-flex; }
.tabbar__badge {
  position: absolute;
  top: -5px;
  right: -9px;
  min-width: 16px;
  height: 16px;
  padding: 0 4px;
  border-radius: var(--radius-pill);
  background: var(--gr-cyan);
  color: #fff;
  font-size: 10px;
  font-weight: var(--fw-semibold);
  line-height: 16px;
  text-align: center;
}

/* Phones and small tablets — exactly the range where the header collapses to
   a hamburger (991px, see _navigation.css). Where the full nav exists the
   bar would be a second navigation saying the same thing. */
@media (max-width: 991.98px) {
  .tabbar { display: grid; }
  body.has-tabbar { padding-bottom: calc(58px + env(safe-area-inset-bottom, 0px)); }
}

/* ===== style.css (own rules) ===== */
/* =========================================================================
   GulfRabit · style.css — master stylesheet
   Imports every design-system partial in cascade order, then holds the base
   reset and print styles. Load this ONCE per page after Tailwind/Bootstrap so
   our tokens win over their defaults.

   Order matters: variables first (tokens), then base, then components.
   ========================================================================= */


/* ---- Base reset -------------------------------------------------------
 *
 * THIS USED TO SAY "light-touch; Tailwind's preflight handles most", and it
 * was telling the truth — which is the problem. The site loads
 * cdn.tailwindcss.com, a CSS compiler that runs in the browser before first
 * paint, and an inventory of every class in every page (tools/tailwind-
 * inventory.py) found it is used for exactly ONE utility: `sr-only`, twice.
 *
 * The rest of what the CDN was doing for us was invisible: preflight's reset.
 * Remove the script without replacing that and the whole site shifts — form
 * controls revert to the browser's font, buttons and tables grow default
 * borders, and every unstyled list and figure gets its margins back.
 *
 * So the reset below is now complete on its own. Everything here is what
 * preflight was silently providing. With that in place, dropping the CDN is a
 * one-line change — see §7e in context.md.
 */
*, *::before, *::after { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body { margin: 0; min-height: 100vh; overflow-x: hidden; }

img, picture, svg, video { max-width: 100%; display: block; }
img { height: auto; }

ul[role="list"], ol[role="list"] { list-style: none; margin: 0; padding: 0; }

/* ---- What Tailwind preflight was providing ----------------------------
   Each of these is load-bearing. They are listed separately from the block
   above so it stays obvious which came from where. */

/* Default margins on flow content. Headings and <p> are already zeroed in
   _typography.css; these are the ones nothing else covers. */
figure, blockquote, dl, dd, pre, fieldset { margin: 0; }
fieldset, legend { padding: 0; }

/* Form controls do NOT inherit the page font by default — a <select> or
   <input> reverts to the browser's UI font, which on Android is a different
   family at a different size, and the difference is obvious next to a label
   set in Inter. */
button, input, optgroup, select, textarea {
  font-family: inherit;
  font-size: 100%;
  font-weight: inherit;
  line-height: inherit;
  color: inherit;
  margin: 0;
  padding: 0;
}

/* Buttons and tables carry a UA border otherwise. Our components set their
   own where they want one; this stops the ones that do not from sprouting a
   grey 2px bevel.

   :where() ZEROES THE SPECIFICITY, and it is load-bearing, not tidiness.
   `[type="button"]` is an attribute selector — specificity (0,1,0), the same
   as a class. So this rule TIED with .btn-primary-gr / .btn-gradient-gr /
   .btn-gr's own `background: var(--_bg)`, and because style.css's own rules
   are concatenated after every partial (see bundle_css in tools/assemble.py),
   the tie broke in favour of the reset. Every `<button type="button">` and
   `<button type="submit">` carrying a .btn-gr variant rendered with a
   transparent background — 142 of them across the site, including Place Order
   on the cart. Anchors and bare <button>s were unaffected, which is why it
   survived: the buttons people looked at hardest were the styled <a>s.

   A reset must lose every argument with a component. :where() guarantees it. */
:where(button, [type="button"], [type="reset"], [type="submit"]) {
  appearance: button;
  background-color: transparent;
  background-image: none;
}
button, select { text-transform: none; }

table { border-collapse: collapse; border-color: inherit; text-indent: 0; }

/* Chrome shows a resize handle and a spinner that no design here asks for. */
textarea { resize: vertical; }
::-webkit-inner-spin-button, ::-webkit-outer-spin-button { height: auto; }
::-webkit-search-decoration { -webkit-appearance: none; }

/* Placeholder colour is otherwise a browser opinion, and Safari's is much
   darker than Chrome's — the same field reads as filled on one and empty on
   the other. */
input::placeholder, textarea::placeholder { opacity: 1; color: var(--text-muted); }

/* ---- What Bootstrap's Reboot was providing ----------------------------
   Same story as preflight: the grid and the components are unused, but the
   reset underneath was load-bearing. */

/* THE IMPORTANT ONE.
 *
 * `hidden` is an attribute, and the UA stylesheet implements it as
 * `[hidden] { display: none }` — a rule any class with a `display` beats,
 * because they have the same specificity and ours comes later.
 *
 * `.acat-new { display: flex }` is used as `<form class="acat-new" hidden>`.
 * So the New category, New product and New coupon forms were hidden ONLY
 * because Reboot adds `!important` to that rule. Remove Bootstrap without
 * this line and all three sit permanently open.
 *
 * 231 elements across the site carry the attribute. Three components had
 * already patched it individually — .tab-panel[hidden], .checkout-step[hidden]
 * and .announce-bar[hidden] — which is the same bug being found and fixed
 * locally three times instead of once here.
 *
 * !important is right for this. `hidden` is the platform's own way of saying
 * "not rendered"; a component that wants to override it should not be using
 * the attribute at all. */
[hidden] { display: none !important; }

/* Reboot styles these; the UA defaults are inconsistent between browsers and
   in Safari's case ugly. */
hr {
  margin: var(--space-6) 0;
  border: 0;
  border-top: 1px solid var(--border-hairline);
  opacity: 1;                 /* Firefox and Chrome disagree on the default */
}

caption {
  padding: var(--space-3) 0;
  color: var(--text-muted);
  text-align: left;           /* the UA centres it, which reads as a title */
}

label { display: inline-block; }
[role="button"] { cursor: pointer; }
summary { display: list-item; cursor: pointer; }

/* Visually hidden, still read aloud. The ONLY Tailwind utility this site
   actually used, so it moves here rather than the CDN staying for it. */
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border-width: 0;
}

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

::selection { background: rgba(27,180,212,.3); color: var(--text-primary); }

/* Keep the sticky header offset when jumping to in-page anchors */
:target { scroll-margin-top: calc(var(--header-h) + var(--space-4)); }

/* ---- Print --------------------------------------------------------------
   Order confirmation / invoices should print clean on white. */
@media print {
  .site-header, .site-footer, .cart-drawer, .toast-stack, .announce-bar,
  .no-print { display: none !important; }
  body { background: #fff; color: #000; }
  .canvas-dark, .canvas-panel { background: #fff !important; color: #000 !important; }
  a { color: #000; text-decoration: underline; }
}
