/* Global Styles & Variable Design Tokens */
@import url('local-fonts.css');

/* ── Typography pairings [Phase 2 ✅] ──────────────────────────────────────
   The four --font-{en,ar}-* properties below are written onto <html> at
   runtime by `applyTypography()` in js/typography.js, which owns the pairing
   library. The values here are only the pre-JS fallback, so a page renders in
   the default pairing rather than an unstyled system font if config is slow.

   The old `body.font-en-*` / `body.font-ar-*` classes are gone: they let a
   merchant combine any Latin face with any Arabic face, including pairs with
   mismatched proportions, and they were applied by overwriting
   `body.className` — which wiped every other class on <body>. */
:root {
  /* [Phase 6 ✅] 'Cairo' sits inside the Latin stacks too — see the long note
     in applyTypography(). A font stack resolves per character, so Arabic typed
     inside an English string (bilingual product names, mixed descriptions) has
     no lang attribute and lands on the Latin stack. Without an Arabic family
     there, Chrome falls through to a thin system Naskh while Firefox picks
     something else — same page, two different renderings. Latin is unaffected:
     Cairo is only reached for glyphs the Latin face does not have. */
  --font-en-display: 'Outfit', 'Cairo', system-ui, sans-serif;
  --font-en-primary: 'Plus Jakarta Sans', 'Cairo', system-ui, sans-serif;
  --font-ar-display: 'Cairo', system-ui, sans-serif;
  --font-ar-primary: 'Cairo', system-ui, sans-serif;

  /* Arabic metrics — overridden per pairing (PLAN §3.2). */
  --ar-scale: 1.10;
  --ar-line-height: 1.65;
}

/* --font-heading / --font-body are Tailwind @theme tokens and are restated
   HERE, on <body>, rather than pointed at --font-display from :root. A custom
   property's value is computed at the element that declares it: writing
   `--font-heading: var(--font-display)` inside :root resolves it once against
   :root's own --font-display and then inherits that frozen string everywhere,
   so headings kept rendering the root default no matter the language. */
body {
  --font-primary: var(--font-en-primary);
  --font-display: var(--font-en-display);
  --font-heading: var(--font-en-display);
  --font-body:    var(--font-en-primary);
}

html[lang="ar"] body,
[dir="rtl"] body {
  --font-primary: var(--font-ar-primary);
  --font-display: var(--font-ar-display);
  --font-heading: var(--font-ar-display);
  --font-body:    var(--font-ar-primary);
}

/* ── Arabic script metrics [Phase 2 ✅] ────────────────────────────────────
   Previously this file swapped font-family for Arabic and compensated
   nothing, so Arabic rendered at Latin's size and leading — too small and
   too tight for a script whose diacritics sit above and below the baseline.

   Size is applied at the ROOT so every rem-based size scales together and
   nothing has to be restated. Known limitation: px-hardcoded font sizes do
   not scale — converting those to rem is Phase 3's legibility work. */
html[lang="ar"],
html[dir="rtl"] {
  font-size: calc(100% * var(--ar-scale));
}

html[lang="ar"] body,
html[dir="rtl"] body {
  line-height: var(--ar-line-height);
}

/* Arabic is a CONNECTED script: any tracking literally breaks the joins
   between letterforms. This neutralises tracking inherited from Latin-oriented
   rules anywhere in the cascade — the one sanctioned use of !important here,
   matching the existing reduced-motion backstop. PLAN §3.2. */
html[lang="ar"] *,
html[dir="rtl"] *,
[lang="ar"],
[lang="ar"] * {
  letter-spacing: 0 !important;
}

/* Arabic inside an otherwise-English page — the language toggle, bilingual
   product names, mixed descriptions — must also pick up the Arabic face and
   its leading, or it renders in a Latin font's fallback glyphs at Latin
   leading. The element-level [lang="ar"] selectors above and below are what
   make that work without the whole page being Arabic. */
[lang="ar"] {
  font-family: var(--font-ar-primary);
  line-height: var(--ar-line-height);
}
[lang="ar"] :is(h1, h2, h3, h4, h5, h6) {
  font-family: var(--font-ar-display);
}

/* ══════════════════════════════════════════════════════════════════════════
   SEMANTIC COLOUR TOKENS [Phase 4 ✅] — PLAN §3.4
   ══════════════════════════════════════════════════════════════════════════
   This block is the SINGLE SOURCE OF TRUTH for colour in the product. It used
   to be split in two: a six-value `--theme-*` set here that applyTheme() drove,
   and a thirty-value `--color-*` set inside tailwind.input.css's @theme that
   NOTHING drove. The `--color-*` half has ~580 call sites across the five
   stylesheets and the inline <style> blocks, so switching theme repainted the
   ~200 `--accent-color`/`--theme-*` sites and left the other 580 sitting on the
   static "Midnight Navy" default. That is the whole reason a theme change
   looked like it only touched a couple of surfaces.

   Now: these names are canonical, `applyTheme()` in js/database.js writes ALL
   of them inline on <html> from the theme's authored per-mode table, and the
   legacy `--theme-*` / `--bg-*` / `--card-*` / `--text-*` names are aliases
   onto them (see the alias layer below) so no existing call site had to move.

   The values here are the pre-JS fallback only — the default theme in each
   mode — so a page paints correctly in the instant before database.js runs.
   Do not restate them in tailwind.input.css; that is what drifted before.

   DARK IS THE DEFAULT. Light overrides live in the html[data-theme="light"]
   block further down this file. Every foreground/background pair below is
   machine-verified ≥4.5:1 (body) / ≥3:1 (large text and UI glyphs).

   [Phase 5 ✅] The two token blocks in this file are GENERATED from
   `buildThemeTokens({}, mode)` in js/database.js — they mirror MODE_DEFAULTS,
   which in turn mirrors the seeded default theme (`tech`). Edit the theme
   table in database.js, not these values, or the pre-JS paint will disagree
   with what the page repaints to a moment later. The @tokens sentinels mark
   the generated region. */
:root {
  --font-primary: var(--font-en-primary);
  --font-display: var(--font-en-display);

  /* @tokens:dark:start */
  /* ── Surfaces, ascending elevation ─────────────────────────────────── */
  --color-background:     #080B11;
  --color-surface:        #101520;
  --color-surface-raised: #182030;
  --color-surface-pop:    #202B41;
  --color-muted:          #182030;

  /* ── Ink ───────────────────────────────────────────────────────────── */
  --color-foreground:     #EAEFF6;
  --color-on-background:  #F7FAFF;
  --color-on-surface:     #F7FAFF;
  --color-on-muted:       #F7FAFF;
  --color-text-default:   #B6C8E0;
  --color-text-muted:     #829DBF;
  --color-text-subtle:    #738EB1;

  /* ── Brand ─────────────────────────────────────────────────────────── */
  --color-primary:        #EDF2FC;
  --color-on-primary:     #080B11;
  --color-secondary:      #B3C6E4;
  --color-on-secondary:   #080B11;
  --color-accent:         #5B9CFF;
  --color-brand-accent:   #5B9CFF;
  --color-on-accent:      #080B11;

  /* ── Borders ───────────────────────────────────────────────────────── */
  --color-border-subtle:  #212B3C;
  --color-border-default: #374967;
  --color-border-strong:  #4D658F;
  --color-border:         #374967;

  /* ── Status ────────────────────────────────────────────────────────── */
  --color-success:        #39D083;
  --color-on-success:     #080B11;
  --color-warning:        #F5AE3C;
  --color-on-warning:     #080B11;
  --color-destructive:    #FF7A7A;
  --color-on-destructive: #080B11;
  --color-info:           #57BDF7;
  --color-on-info:        #080B11;

  /* ── Focus ─────────────────────────────────────────────────────────── */
  --color-ring:           #5B9CFF;
  /* @tokens:dark:end */


  /* ── Legacy alias layer ──────────────────────────────────────────────────
     Every name below predates Phase 4 and is still used across the product.
     They are now thin pointers, not independent values, so a theme change
     propagates through them automatically. Migrate call sites to the
     `--color-*` names opportunistically; do not add new uses of these. */
  --theme-background: var(--color-background);
  --theme-surface:    var(--color-surface);
  --theme-primary:    var(--color-primary);
  --theme-accent:     var(--color-accent);
  --theme-text:       var(--color-text-default);
  --theme-border:     var(--color-border-default);

  --bg-primary:   var(--color-background);
  --bg-secondary: var(--color-surface);
  --bg-tertiary:  var(--color-surface-raised);
  --accent-color: var(--color-accent);
  --accent-color-rgb: 91, 156, 255;
  --accent-hover: var(--color-accent);
  --text-primary: var(--color-foreground);
  /* [Phase 28 → Phase 4] These were bound to --theme-primary, which is a
     *brand* slot, not a text slot. In dark mode applyTheme() set
     --theme-primary to the accent, so every secondary/muted string on every
     page rendered accent-blue at 3.72:1 — below the 4.5:1 AA floor. The fix
     was a hardcoded neutral ramp per mode. Phase 4 keeps the fix and improves
     it: they now point at the theme's own authored text ramp, which is
     verified ≥4.5:1 per theme per mode rather than being one grey that
     happened to work against two backgrounds. Never rebind these to a brand
     slot. */
  --text-secondary: var(--color-text-muted);
  --text-muted:     var(--color-text-subtle);

  --card-bg:     var(--color-surface);
  --card-border: var(--color-border-default);

  --glass-blur: 16px;

  --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.1);
  --shadow-md: 0 8px 24px rgba(0, 0, 0, 0.12);
  --shadow-lg: 0 16px 48px rgba(0, 0, 0, 0.15);
  --shadow-glow: 0 0 20px rgba(var(--accent-color-rgb), 0.2);
  
  /* ── Thickness tokens [Phase 3 ✅] — PLAN §3.1 ─────────────────────────
     Thicker matters MORE than bigger: a heavy 15px label beats a thin 19px
     one. These are the floors; nothing in the product should sit below them.

     Weights. Note 500 for BOTH body and muted text — thin + grey is the
     worst possible combination on a phone in daylight, so hierarchy comes
     from colour, never from dropping weight. */
  --weight-body: 500;
  --weight-muted: 500;
  --weight-label: 600;   /* labels, nav items, chips, tabs */
  --weight-subhead: 700;
  --weight-heading: 800;
  --weight-price: 700;
  --weight-button: 700;

  /* Borders. Raw 1px is banned — a hairline disappears on a bright phone
     screen and reads as "unfinished" rather than "delicate". */
  --border-thin: 1.5px;    /* default: cards, inputs, dividers            */
  --border-strong: 2px;    /* emphasis, active, focus, structural outlines */

  /* Icon strokes. Deliberately reverses the "thin icons for dense UI"
     convention — that is intentional and requested. */
  --stroke-base: 2;
  --stroke-emphasis: 2.25;

  /* [Phase 13 ✅] Lucide `check` as a mask source, for control indicators that
     cannot hold an inline <svg> — a custom checkbox tick, for one. Used with
     `mask` so it takes the surrounding ink colour and needs no per-theme copy.
     Stroke 3 (above --stroke-emphasis) because it renders at 12px, where 2
     disappears. Keep the family Lucide (PLAN §4.2): do not paste a tick from
     another set here. */
  --icon-check: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E");

  --radius-sm: 6px;
  --radius-md: 12px;
  --radius-lg: 20px;
  --radius-full: 9999px;
  
  /* ── Motion scale [Phase 7 ✅] — PLAN §3.3 ──────────────────────────────
     One rhythm for the whole product. Every duration and easing in the app
     resolves through these; the motion block below writes no raw values, and
     neither should new code.

     --motion-scale is the MOTION PERSONALITY dial (Subtle / Standard /
     Expressive), set as data-motion on <html>. It multiplies every duration
     rather than duplicating the scale three times, so a merchant changes the
     *feel* in one move and every animation stays in proportion. It is a
     separate axis from the colour theme, deliberately — themes are colours
     only. */
  --motion-scale: 1;

  /* [Phase 13.2 ✅] The unscaled constants live on their own, so a context that
     needs its own tempo can RE-DERIVE the four tokens below instead of
     hardcoding "250ms" a second time.

     Why re-deriving is necessary at all: a custom property substitutes its
     own `var()`s at computed-value time **on the element that declares it**.
     `--motion-base` is declared here, so it bakes against :root's
     `--motion-scale` and children inherit the finished number. Setting
     `--motion-scale` further down the tree therefore changes nothing — which is
     exactly why the admin's three "Overall pace" tiles all ran at the same
     speed despite each carrying its own inline scale. Override the scale AND
     re-declare the tokens together, or don't bother overriding it. */
  --mo-unit-instant:  80ms;
  --mo-unit-fast:    150ms;
  --mo-unit-base:    250ms;
  --mo-unit-emphasis:400ms;
  --mo-unit-exit:    160ms;
  --mo-unit-stagger:  40ms;

  --motion-instant:  calc(var(--mo-unit-instant)  * var(--motion-scale));  /* state flips       */
  --motion-fast:     calc(var(--mo-unit-fast)     * var(--motion-scale));  /* micro-interaction */
  --motion-base:     calc(var(--mo-unit-base)     * var(--motion-scale));  /* the default       */
  --motion-emphasis: calc(var(--mo-unit-emphasis) * var(--motion-scale));  /* complex — the cap */

  /* Exits run at ~65% of the enter they mirror. A UI that leaves as slowly as
     it arrives feels sluggish even when the enter is well judged. */
  --motion-exit:     calc(var(--mo-unit-exit)     * var(--motion-scale));

  /* Entering decelerates, leaving accelerates. `linear` is never right for UI. */
  --ease-out:      cubic-bezier(0.22, 1, 0.36, 1);      /* enter             */
  --ease-in:       cubic-bezier(0.7, 0, 0.84, 0);       /* exit              */
  --ease-standard: cubic-bezier(0.4, 0, 0.2, 1);        /* move within view  */
  --ease-spring:   cubic-bezier(0.34, 1.56, 0.64, 1);   /* one gentle overshoot */

  --stagger-step: calc(var(--mo-unit-stagger) * var(--motion-scale));

  /* The three legacy names are kept because 57 rules already use them; they
     are aliases onto the scale now, not independent values. Do not add new
     uses — reach for --motion-* directly. */
  --transition-fast:   var(--motion-fast) var(--ease-standard);
  --transition-normal: var(--motion-base) var(--ease-standard);
  --transition-slow:   var(--motion-emphasis) var(--ease-standard);

  /* The --duration and --ease family used by the rules inside
     tailwind.input.css. Declared HERE, once, so those rules scale with the
     Motion Personality like everything else. */
  --duration-instant:    var(--motion-instant);
  --duration-fast:       var(--motion-fast);
  --duration-standard:   var(--motion-base);
  --duration-emphasized: var(--motion-emphasis);
  --duration-large:      calc(600ms * var(--motion-scale));
  --ease-emphasized: cubic-bezier(0.3, 0, 0, 1);
  --ease-decel:      var(--ease-out);
  --ease-accel:      var(--ease-in);

  --drawer-hidden-transform: translateX(100%);
}

/* The viewport's scroll behaviour comes from <html>. Setting overflow-x here
   (not just on body) is what actually clips off-canvas panels parked beyond the
   right edge and stops the whole page dragging sideways. */
/* [Phase 3 ✅] A missing weight must fall to the nearest real one, never to a
   browser-synthesised faux-bold — which looks smeared and defeats the whole
   point of the thickness pass. Matters here because several families on disk
   have genuine gaps (Tajawal has no 600, Almarai no 500/600, Amiri 400/700
   only). CSS font matching resolves an unavailable 600 upward to 700, so this
   makes text heavier, not lighter. */
* {
  font-synthesis-weight: none;
}

html {
  /* `hidden` would make <html> a scroll container, which silently breaks every
     position:sticky descendant (the admin sidebar stopped sticking). `clip`
     clips the same overflow WITHOUT creating a scroll container, so sticky
     keeps working. `hidden` stays first as the fallback for old engines. */
  overflow-x: hidden;
  overflow-x: clip;
  max-width: 100%;
  /* [Phase 5 ✅] Native controls are painted by the BROWSER, not by our tokens.
     Without this the shop declared no scheme, so every unchecked radio and
     checkbox drew as a bright white puck on the dark themes — the loudest
     thing on the page — and the same applied to scrollbars, spinners and the
     date/colour pickers in admin. `color-scheme` is the only way to reach
     them; it costs one line and it is the difference between a themed page
     and a themed page with browser chrome punched through it. */
  color-scheme: dark;
}

html[data-theme="light"] { color-scheme: light; }

/* Bind layout elements directly to --theme-* variables as requested */
body {
  background-color: var(--theme-background);
  color: var(--theme-text);
  font-family: var(--font-primary);
  /* dvh, not vh — mobile browser chrome makes 100vh taller than the visible area,
     which pushes footers and sticky CTAs below the fold on phones. */
  min-height: 100dvh;
  overflow-x: hidden;
  overflow-x: clip;   /* see <html> above — `clip` preserves position:sticky */
  /* Off-canvas panels (cart drawer, filter sheet) sit at translateX(100%), i.e.
     a full panel-width past the right edge. `visibility:hidden` stops them
     painting but they still extend the scrollable area, so the page could be
     dragged sideways ~230px. overflow-x on <body> alone does not clip this —
     the viewport takes its overflow from <html>, set below. */
  max-width: 100%;
  line-height: 1.5;
  transition: background-color var(--transition-normal);
  /* [Phase 3 ✅] THE root cause of the product reading thin: <body> declared no
     weight, so every element without an explicit font-weight inherited the
     browser default of 400. Every explicit declaration in the app was already
     500+; it was the *undeclared* majority that was light. */
  font-weight: var(--weight-body);
}

/* ── Thickness baseline [Phase 3 ✅] — PLAN §3.1 ───────────────────────────
   Deliberately low-specificity (:where has zero specificity) so any component
   rule still wins. This only raises the floor for anything that never stated
   a weight. */
:where(button, .btn, .glow-btn, .outline-btn, .add-cart-btn, .checkout-btn) {
  font-weight: var(--weight-button);
}
:where(input, select, textarea) {
  font-weight: var(--weight-body);
}
:where(label, .tab-link, .ad-nav-item, .chip, .tag-btn, .badge, .filter-chip, nav a) {
  font-weight: var(--weight-label);
}
:where(h4, h5, h6) { font-weight: var(--weight-subhead); }
:where(h1, h2, h3) { font-weight: var(--weight-heading); }

/* Prices and numeric data: heavy, and tabular so columns do not jitter as
   digits change (§3.1). */
:where(.product-price, .price, .cart-total, .stat-val, .co-total, #cart-total,
       .pd-price, .ty-receipt, .order-total) {
  font-weight: var(--weight-price);
  font-variant-numeric: tabular-nums;
}

/* Cards, option panels, and containers surface & outline binding */
.product-card, 
.customizer-form, 
.stat-card, 
.glass, 
.modal-content, 
.spreadsheet-container,
.auth-card,
.preset-preview-box,
.orders-sub-tabs,
.dev-card,
.recommendation-card {
  background: var(--theme-surface);
  border: var(--border-thin) solid var(--theme-border);
  color: var(--theme-text);
}

/* Headers, tabs, metrics, labels, text elements text/primary color binding */
h1, h2, h3, h4, h5, h6, 
.stat-val, 
label, 
.tab-link, 
.order-tag-badge, 
.contact-link,
.product-title,
.product-price,
.product-desc,
.swatch-label {
  color: var(--theme-text);
  font-family: var(--font-display);
  font-weight: var(--weight-heading);
  letter-spacing: -0.02em;
}

.tab-link {
  color: var(--theme-primary);
}

/* Main interaction controls, active states, indicators, button backgrounds */
.tab-link.active,
.tag-btn.active,
.theme-swatch.active,
.glow-btn,
.add-cart-btn,
.order-sub-link.active,
.checkout-btn,
.status-pill.completed {
  background-color: var(--theme-accent);
  border-color: var(--theme-accent);
  color: var(--color-on-accent);
}

.theme-swatch.active {
  box-shadow: 0 0 12px var(--theme-accent);
}

.outline-btn {
  background: transparent;
  color: var(--theme-text);
  border: 1.5px solid var(--theme-border);
  border-radius: var(--radius-md);
  padding: 0.75rem 1.5rem;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  font-family: var(--font-primary);
  font-weight: 700;
  font-size: 0.95rem;
  transition: all var(--transition-fast);
}

.outline-btn:hover {
  background: rgba(var(--accent-color-rgb), 0.05);
  border-color: var(--theme-accent);
  color: var(--theme-accent);
}

.glow-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  font-family: var(--font-primary);
  font-size: 0.95rem;
  font-weight: 700;
  padding: 0.75rem 1.75rem;
  border-radius: var(--radius-md);
  border: 1.5px solid transparent;
  cursor: pointer;
  transition: all var(--transition-fast);
  text-decoration: none;
  box-shadow: 0 4px 12px rgba(var(--accent-color-rgb), 0.2);
}

.glow-btn:hover {
  transform: translateY(-2px);
  box-shadow: 0 6px 20px rgba(var(--accent-color-rgb), 0.4), var(--shadow-glow);
  filter: brightness(1.1);
}

.glow-btn:active {
  transform: translateY(0);
}

/* Reset and Base Styles */
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
  -webkit-tap-highlight-color: transparent;
}

/* Scrollbars */
::-webkit-scrollbar {
  width: 8px;
  height: 8px;
}
::-webkit-scrollbar-track {
  background: var(--bg-primary);
}
::-webkit-scrollbar-thumb {
  background: var(--bg-tertiary);
  border-radius: var(--radius-full);
}
::-webkit-scrollbar-thumb:hover {
  background: var(--accent-color);
}

/* Form Groups */
.form-group {
  margin-bottom: 1.25rem;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.form-group label {
  font-size: 0.95rem;
  font-weight: 700;
  color: var(--text-primary);
}

.form-input {
  background: rgba(var(--accent-color-rgb), 0.05);
  border: 1.5px solid var(--theme-border);
  color: var(--theme-text);
  padding: 0.8rem 1.2rem;
  border-radius: var(--radius-md);
  font-family: var(--font-primary);
  font-size: 0.95rem;
  outline: none;
  width: 100%;
  transition: border-color var(--transition-fast), box-shadow var(--transition-fast), background-color var(--transition-fast);
}

.form-input:focus {
  background: var(--theme-surface);
  border-color: var(--theme-accent);
  box-shadow: 0 0 0 4px rgba(var(--accent-color-rgb), 0.15);
}

/* Modals Overlay */
.modal-overlay {
  position: fixed;
  top: 0;
  inset-inline-start: 0;
  inset-inline-end: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.7);
  backdrop-filter: blur(4px);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--transition-normal);
}

.modal-overlay.active {
  opacity: 1;
  pointer-events: auto;
}

.toast-container {
  position: fixed;
  bottom: 2rem;
  inset-inline-end: 2rem;
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  z-index: 2000;
}

.toast {
  padding: 1rem 1.5rem;
  border-radius: var(--radius-md);
  /* [Phase 6 ✅] Ink is set per VARIANT below, not once here. A toast's
     background is a status colour, and each status resolves its own readable
     ink by measurement (buildThemeTokens) — a blanket white failed on the
     lighter success/warning fills that several themes use. */
  font-weight: 500;
  display: flex;
  align-items: center;
  gap: 0.75rem;
  box-shadow: var(--shadow-lg);
  transform: translateY(1rem);
  opacity: 0;
  animation: toast-in var(--transition-normal) forwards;
}

@keyframes toast-in {
  to {
    transform: translateY(0);
    opacity: 1;
  }
}

.toast.success {
  background: var(--color-success);
  color: var(--color-on-success);
  border-inline-start: 4px solid var(--color-success);
}

.toast.error {
  background: var(--color-destructive);
  color: var(--color-on-destructive);
  border-inline-start: 4px solid var(--color-destructive);
}

.toast.info {
  background: var(--color-surface-raised);
  color: var(--color-text-default);
  border-inline-start: 4px solid var(--color-accent);
  border: var(--border-thin) solid var(--color-border-default);
}

/* ----------------- LIGHT MODE STYLING & ADAPTIVE SYSTEM ----------------- */
/* [Phase 4 ✅] The light half of the canonical token set — same names, same
   roles, authored independently rather than inverted from dark (PLAN §3.4).
   Pre-JS fallback only: applyTheme() overwrites these inline from the active
   theme's `light` table the moment database.js runs.

   Note what is deliberately GONE from this block: it used to force
   `--accent-color: #64748b` — slate grey — in light mode, which threw away the
   merchant's accent on every light-mode page. The alias layer now carries the
   real accent through in both modes. The dead `.theme-summer-resort` block that
   sat here was also removed: no code path ever added that class. */
html[data-theme="light"] {
  /* @tokens:light:start */
  /* ── Surfaces, ascending elevation ─────────────────────────────────── */
  --color-background:     #F2F4F8;
  --color-surface:        #FFFFFF;
  --color-surface-raised: #FFFFFF;
  --color-surface-pop:    #FFFFFF;
  --color-muted:          #E6EAF2;

  /* ── Ink ───────────────────────────────────────────────────────────── */
  --color-foreground:     #151C2B;
  --color-on-background:  #0C1424;
  --color-on-surface:     #0C1424;
  --color-on-muted:       #0C1424;
  --color-text-default:   #2A3956;
  --color-text-muted:     #48597B;
  --color-text-subtle:    #556585;

  /* ── Brand ─────────────────────────────────────────────────────────── */
  --color-primary:        #0C1424;
  --color-on-primary:     #FFFFFF;
  --color-secondary:      #2B3E5C;
  --color-on-secondary:   #FFFFFF;
  --color-accent:         #1D4ED8;
  --color-brand-accent:   #1D4ED8;
  --color-on-accent:      #FFFFFF;

  /* ── Borders ───────────────────────────────────────────────────────── */
  --color-border-subtle:  #DEE3EC;
  --color-border-default: #ABB8D0;
  --color-border-strong:  #788CB0;
  --color-border:         #ABB8D0;

  /* ── Status ────────────────────────────────────────────────────────── */
  --color-success:        #12734A;
  --color-on-success:     #FFFFFF;
  --color-warning:        #A65A07;
  --color-on-warning:     #FFFFFF;
  --color-destructive:    #C42B2B;
  --color-on-destructive: #FFFFFF;
  --color-info:           #0B69C4;
  --color-on-info:        #FFFFFF;

  /* ── Focus ─────────────────────────────────────────────────────────── */
  --color-ring:           #1D4ED8;
  /* @tokens:light:end */

  --accent-color-rgb: 29, 78, 216;
  --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.05);
  --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08);
  --shadow-lg: 0 10px 30px rgba(0, 0, 0, 0.12);
  --shadow-glow: 0 0 15px rgba(var(--accent-color-rgb), 0.12);
}

/* Light mode form focus / specific controls input text readability */
html[data-theme="light"] .form-input {
  background: var(--color-surface);
  color: var(--color-foreground);
}
html[data-theme="light"] .form-input:focus {
  background: var(--color-surface);
  border-color: var(--accent-color);
}
html[data-theme="light"] select.form-input option {
  background-color: var(--color-surface);
  color: var(--color-foreground);
}
html[data-theme="light"] .preset-img-btn {
  background: var(--color-surface);
  border-color: var(--color-border-default);
}
html[data-theme="light"] .preset-img-btn:hover {
  background: var(--color-muted);
}
html[data-theme="light"] .preset-img-btn svg {
  fill: var(--color-text-muted);
}
html[data-theme="light"] .tag-btn {
  background: var(--color-surface);
  color: var(--color-text-muted);
}
html[data-theme="light"] .tag-btn:hover {
  color: var(--color-foreground);
  background: var(--color-surface);
}
html[data-theme="light"] .tag-btn.active {
  background: var(--accent-color);
  color: var(--color-on-accent);
}

/* In light mode, some tables need specific border colors */
html[data-theme="light"] table,
html[data-theme="light"] th,
html[data-theme="light"] td {
  border-color: var(--color-border-default);
}
html[data-theme="light"] tr {
  border-bottom: var(--border-thin) solid var(--color-border-subtle);
}
html[data-theme="light"] tr:hover {
  background-color: color-mix(in srgb, var(--color-foreground) 2%, transparent);
}
html[data-theme="light"] th {
  background-color: var(--color-surface);
  color: var(--color-text-muted);
}
html[data-theme="light"] td {
  color: var(--color-text-muted);
}
html[data-theme="light"] .contact-link {
  color: var(--accent-color);
}
html[data-theme="light"] .order-tag-badge {
  background: var(--color-muted);
  color: var(--color-text-muted);
}
html[data-theme="light"] .outline-btn:hover {
  background: var(--color-surface);
}

/* ══════════════════════════════════════════════════════════════════════════
   CUSTOMIZABLE ANIMATIONS ENGINE [Phase 7 ✅] — PLAN §3.3
   ══════════════════════════════════════════════════════════════════════════
   Every duration and easing below comes from the motion scale. Nothing here
   animates width/height/top/left — transform and opacity only, so the whole
   engine stays on the compositor.

   SIX OPTIONS WERE REMOVED, judged as a designer rather than kept for the
   sake of a longer dropdown (§3.3: "a bad option is worse than no option"):

     hover  focus-blur  blurred every OTHER product card on hover — it hid the
                        merchandise the shopper was scanning, and ran a
                        filter:blur over N elements at once
     hover  skew        skewX distorted the product photograph itself
     load   blur-up     800ms, animated filter:blur (not compositor-friendly at
                        that radius), and started at opacity 0.2 — a technique
                        for progressive image loading, misapplied to card entry
     load   flip        a 3D rotateY on every card in the grid; disorienting
     load   bounce-in   scale(0.3) cartoon pop, far outside the 0.95–1.05 band
     nav    flip-page   a full-page 3D flip on every navigation
     nav    pulse       a brightness() flash on page enter, expressing nothing

   Stored configs naming a removed option are ALIASED to the nearest survivor
   in shop.js — a live tenant's saved value must never break (PLAN rule 10).

   MOTION PERSONALITIES scale the whole system rather than redefining it. */
html[data-motion="subtle"]     { --motion-scale: 0.7; }
html[data-motion="standard"]   { --motion-scale: 1; }
html[data-motion="expressive"] { --motion-scale: 1.35; }

/* Subtle also drops the one gentle overshoot, so "subtle" really is subtle
   rather than merely faster. */
html[data-motion="subtle"] { --ease-spring: var(--ease-out); }

/* ── 2. Entrance / load ──────────────────────────────────────────────────
   Grid items only. Enter from below = "arriving from deeper in the page". */
.load-stagger-fade {
  opacity: 0;
  transition: opacity var(--motion-base) var(--ease-out);
}
.load-stagger-fade.loaded { opacity: 1; }

.load-stagger-slide {
  opacity: 0;
  transform: translateY(18px);
  transition: opacity var(--motion-base) var(--ease-out),
              transform var(--motion-base) var(--ease-out);
}
.load-stagger-slide.loaded {
  opacity: 1;
  transform: translateY(0);
}

.load-stagger-zoom {
  opacity: 0;
  /* [Phase 13.2 ✅] 0.94 → 0.90. At 6% "Zoom in" was not readably different
     from "Fade in" — on the card OR in the picker. 10% still enters gently
     and is unmistakably a zoom. */
  transform: scale(0.90);
  transition: opacity var(--motion-base) var(--ease-out),
              transform var(--motion-base) var(--ease-spring);
}
.load-stagger-zoom.loaded {
  opacity: 1;
  transform: scale(1);
}

/* A wipe reads as "being written", so it is the one entrance that earns the
   emphasis duration — but 800ms was twice the cap and felt broken. */
.load-text-reveal {
  clip-path: inset(0 100% 0 0);
  transition: clip-path var(--motion-emphasis) var(--ease-out);
}
.load-text-reveal.loaded { clip-path: inset(0 0 0 0); }

/* Logical inset, so the wipe runs right-to-left in Arabic instead of
   uncovering the text backwards. */
html[dir="rtl"] .load-text-reveal { clip-path: inset(0 0 0 100%); }
html[dir="rtl"] .load-text-reveal.loaded { clip-path: inset(0 0 0 0); }

.load-parallax-slide {
  opacity: 0;
  /* [Phase 13.2 ✅] A floor under the percentage. 6% of a 340px product card is
     20px and reads correctly; 6% of a short card — or of a picker tile — is
     2-3px and the option looked identical to a fade. `max()` keeps the
     proportional feel on a tall card and guarantees the movement exists on a
     small one. The distinguishing feature is still the longer duration. */
  transform: translateY(max(14px, 6%));
  transition: opacity var(--motion-base) var(--ease-out),
              transform var(--motion-emphasis) var(--ease-out);
}
.load-parallax-slide.loaded {
  opacity: 1;
  transform: translateY(0);
}

/* 1. Hover Card Animations
   ─────────────────────────────────────────────────────────────────────────
   [Phase 10 ✅] Each option's EFFECT is declared once, as custom properties on
   the option class. Two appliers below turn it on: `:hover` on the storefront,
   and `.is-hover-demo` for the admin panel's motion previews.

   This matters because the `:hover` applier is wrapped in
   `@media (hover: hover) and (pointer: fine)` — load-bearing, see the comment
   on that block — so on the merchant's phone, which is the device Phase 10
   targets, a hover preview driven by `:hover` can never fire. Declaring the
   effect once and giving the demo its own applier means the preview animates
   on touch AND is guaranteed to be the same animation the shop performs: there
   is no second copy of the values to drift.

   `trace` and `slide-details` act on a child rather than the card, so they
   keep their own rules further down. */
.hover-lift {
  --hv-transform: translateY(-4px);
  --hv-shadow: var(--shadow-2);
  --hv-border: rgba(var(--accent-color-rgb), 0.15);
  --hv-img: scale(1.03);
  transition: transform var(--transition-normal), box-shadow var(--transition-normal), border-color var(--transition-normal);
}
.hover-scale {
  --hv-transform: scale(1.03);
  --hv-shadow: var(--shadow-md);
  --hv-img: scale(1.06);
  transition: transform var(--transition-normal), box-shadow var(--transition-normal);
}
.hover-glow {
  --hv-shadow: 0 0 20px rgba(var(--accent-color-rgb), 0.35);
  --hv-border: var(--color-accent);
  --hv-img: scale(1.06);
  transition: box-shadow var(--transition-normal), border-color var(--transition-normal);
}
.hover-tilt {
  --hv-transform: perspective(1000px) rotateX(4deg) rotateY(-4deg) translateY(-4px);
  --hv-shadow: var(--shadow-md);
  --hv-border: color-mix(in srgb, var(--color-accent) 20%, transparent);
  transition: transform var(--transition-normal), box-shadow var(--transition-normal), border-color var(--transition-normal);
}
/* tilt / trace / slide-details keep the 1.05 image zoom they were effectively
   getting from the deleted generic rule in shop.css — now declared, not
   inherited by accident. `none` declares no --hv-img at all, so a shop set to
   "Disabled (Static)" is finally static. */
.hover-tilt { --hv-img: scale(1.05); }
.hover-trace {
  --hv-img: scale(1.05);
  position: relative;
  transition: border-color var(--transition-normal), box-shadow var(--transition-normal);
}
.hover-trace::after {
  content: '';
  position: absolute;
  inset: -1px;
  border: 2px solid var(--theme-accent);
  border-radius: inherit;
  opacity: 0;
  transition: opacity var(--transition-fast);
  pointer-events: none;
  z-index: 1;
}
.product-card.hover-slide-details {
  --hv-img: scale(1.05);
  position: relative;
  overflow: hidden;
}
.hover-slide-details .product-actions {
  position: absolute;
  bottom: 0;
  inset-inline-start: 0;
  inset-inline-end: 0;
  background: var(--card-bg);
  border-top: var(--border-thin) solid var(--card-border);
  padding: 1rem;
  transform: translateY(102%);
  transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
  z-index: 5;
  box-shadow: 0 -4px 15px rgba(0,0,0,0.1);
}
.product-card.hover-focus-blur {
  transition: opacity var(--transition-normal), filter var(--transition-normal), transform var(--transition-normal), box-shadow var(--transition-normal);
}
/* The hover guard below is load-bearing: without it a touch device latches the
   :hover state on tap and the card stays lifted until you tap elsewhere. */
@media (hover: hover) and (pointer: fine) {
  /* Applier A — the storefront. Reads the values declared on the option class
     above; nothing is restated here, so tuning an effect is a one-place edit.

     Each selector list names ONLY the options that declare that property. A
     blanket `border-color: var(--hv-border, …)` would repaint the border of
     `scale`, which deliberately does not touch it — a fallback cannot express
     "leave this property alone", so the selector has to. */
  .hover-lift:hover,
  .hover-scale:hover,
  .hover-glow:hover,
  .hover-tilt:hover {
    transform: var(--hv-transform, none);
    box-shadow: var(--hv-shadow);
  }
  .hover-lift:hover,
  .hover-glow:hover,
  .hover-tilt:hover {
    border-color: var(--hv-border);
  }
  /* All six options: `var(--hv-img)` with no fallback is invalid-at-computed-
     value on an option that declares none, which resolves to the initial
     `none` — so "no zoom declared" means "no zoom", not "inherit someone
     else's". */
  .hover-lift:hover .product-img,
  .hover-scale:hover .product-img,
  .hover-glow:hover .product-img,
  .hover-tilt:hover .product-img,
  .hover-trace:hover .product-img,
  .hover-slide-details:hover .product-img {
    transform: var(--hv-img);
  }
  .hover-trace:hover::after {
    opacity: 1;
  }
  .hover-trace:hover {
    box-shadow: var(--shadow-sm);
  }
  .hover-slide-details:hover .product-actions {
    transform: translateY(0);
  }
}

/* Applier B — the admin panel's motion previews [Phase 10 ✅].
   Deliberately OUTSIDE the hover guard: a preview has to animate on a touch
   screen, and `.is-hover-demo` is only ever added by admin.js to a demo card,
   never to a real product card, so it cannot reintroduce the latched-hover bug
   the guard exists to prevent. */
.hover-lift.is-hover-demo,
.hover-scale.is-hover-demo,
.hover-glow.is-hover-demo,
.hover-tilt.is-hover-demo {
  transform: var(--hv-transform, none);
  box-shadow: var(--hv-shadow);
}
.hover-lift.is-hover-demo,
.hover-glow.is-hover-demo,
.hover-tilt.is-hover-demo {
  border-color: var(--hv-border);
}
.is-hover-demo .product-img { transform: var(--hv-img); }
.hover-trace.is-hover-demo::after { opacity: 1; }
.hover-trace.is-hover-demo { box-shadow: var(--shadow-sm); }
.hover-slide-details.is-hover-demo .product-actions { transform: translateY(0); }

/* ── 3. Navigation / page-enter ──────────────────────────────────────────
   Each of these is the *from* state; the page script removes the class to
   land on the natural position, so they all run as ENTER animations. */
/* [Phase 13.2 ✅] "Fade" is now a pure fade. The 2% scale it used to carry was
   too small to perceive as motion, but it did make Fade and Zoom look like the
   same option with different labels. */
.nav-transition-fade {
  opacity: 0;
  transition: opacity var(--motion-base) var(--ease-out),
              transform var(--motion-base) var(--ease-out);
}
/* Logical, not physical: `translateX(40px)` slid in from the right even in
   Arabic, which reads as going backwards. */
.nav-transition-slide {
  opacity: 0;
  transform: translateX(40px);
  transition: opacity var(--motion-base) var(--ease-out),
              transform var(--motion-base) var(--ease-out);
}
html[dir="rtl"] .nav-transition-slide { transform: translateX(-40px); }

.nav-transition-zoom {
  opacity: 0;
  transform: scale(0.90);   /* [Phase 13.2] was 0.94 — see load-stagger-zoom */
  transition: opacity var(--motion-base) var(--ease-out),
              transform var(--motion-base) var(--ease-spring);
}
.nav-transition-drawer {
  opacity: 0;
  transform: var(--drawer-hidden-transform);
  transition: opacity var(--motion-base) var(--ease-out),
              transform var(--motion-base) var(--ease-out);
}
.nav-transition-scroll-snap {
  opacity: 0;
  transform: translateY(28px);
  transition: opacity var(--motion-base) var(--ease-out),
              transform var(--motion-base) var(--ease-out);
}
.nav-transition-cart-bounce {
  opacity: 0;
  transform: scale(0.96) translateY(14px);
  transition: opacity var(--motion-base) var(--ease-out),
              transform var(--motion-base) var(--ease-spring);
}
/* `pulse` survives as an OPTION but lost its page-enter effect. The old rule
   flashed the whole page with filter: brightness(1.2) on arrival, which
   expressed nothing. What the option is actually for is the pulsing CTA on the
   product page (`.pulse-anim`).

   [Phase 13.2 ✅] It used to enter "like fade" — with the identical declaration
   block, so the two options were literally the same animation under two names
   and no preview could ever tell them apart. It now enters from slightly *over*
   size and settles, which is the visual rhyme of the CTA pulse it is named for
   and is what the option's own hint promises. */
.nav-transition-pulse {
  opacity: 0;
  transform: scale(1.06);
  transition: opacity var(--motion-base) var(--ease-out),
              transform var(--motion-emphasis) var(--ease-spring);
}

/* [Phase 13.4 ✅] Apply the from-state without animating INTO it.

   `.product-detail-container`, `.tk-main` and `.da-outer` each already carry a
   `transition`, so adding `.nav-transition-fade` did not snap the page to
   opacity 0 — it started a 250ms fade-OUT, which the class removal two frames
   later immediately reversed. Net effect: a flicker, and `getComputedStyle`
   reporting opacity 1 while a CSSTransition was quietly running. `#catalog-shell`
   on the shop happens to have no transition of its own, which is the only reason
   that page appeared to work and hid the bug.

   `!important` is deliberate and is the narrow case where it is correct: the
   competing `transition` lives in a PAGE stylesheet, which loads after this file
   and so wins on source order at equal specificity. The class exists for a
   single frame — `playNavTransition()` removes it before the entrance plays —
   so it cannot suppress anything else, and it cannot defeat reduced motion,
   which wants no transition in the first place. */
.nav-enter-snap { transition: none !important; }

.drawer-transition {
  transform: var(--drawer-hidden-transform);
  transition: transform var(--motion-base) var(--ease-out);
}
.drawer-transition.active {
  transform: translateX(0);
}
/* Exit faster than enter (§3.3): the drawer leaves in ~65% of the time it
   took to arrive, which is what makes dismissal feel responsive. */
.drawer-transition:not(.active) {
  transition: transform var(--motion-exit) var(--ease-in);
}

/* ── 4. Functional feedback ──────────────────────────────────────────────
   This is the motion that actually converts: it confirms that something the
   shopper did has landed. Decorative motion is capped; this is not. */
@keyframes cartBounce {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.18) translateY(-5px); }
}
.cart-bounce-anim {
  animation: cartBounce var(--motion-emphasis) var(--ease-spring) forwards;
}

/* Was `btnPulse 2s infinite` — a permanent halo on a button. An animation
   that never ends expresses no cause and no effect, and keeps the compositor
   awake on a phone in someone's pocket. It now runs twice and stops. */
@keyframes btnPulse {
  0%   { box-shadow: 0 0 0 0    rgba(var(--accent-color-rgb), 0.55); }
  70%  { box-shadow: 0 0 0 10px rgba(var(--accent-color-rgb), 0); }
  100% { box-shadow: 0 0 0 0    rgba(var(--accent-color-rgb), 0); }
}
.pulse-anim {
  animation: btnPulse calc(var(--motion-emphasis) * 2) var(--ease-out) 2;
}

/* Press feedback — §3.3 wants 0.95–1.05, restored on release. Applies to the
   real controls, not every clickable div, and never on a disabled one. */
:where(button, .btn, .add-cart-btn, .checkout-btn, .glow-btn, .tag-btn,
       .filter-chip, .qv-add-cart-btn):not(:disabled):active {
  transform: scale(0.97);
  transition-duration: var(--motion-instant);
}

/* Async feedback: a control that is waiting must say so. Both states are
   opacity/transform only and neither blocks input elsewhere on the page. */
@keyframes btnSpin { to { transform: rotate(360deg); } }
.is-loading {
  position: relative;
  color: transparent !important;
  pointer-events: none;
}
.is-loading::after {
  content: '';
  position: absolute;
  inset: 50% auto auto 50%;
  width: 1.125rem;
  height: 1.125rem;
  margin: -0.5625rem 0 0 -0.5625rem;
  border: var(--border-strong) solid color-mix(in srgb, currentColor 25%, transparent);
  border-top-color: var(--color-on-accent);
  border-radius: var(--radius-full);
  animation: btnSpin calc(var(--motion-emphasis) * 1.8) linear infinite;
}

@keyframes fbPop {
  0%   { transform: scale(0.6); opacity: 0; }
  60%  { transform: scale(1.05); opacity: 1; }
  100% { transform: scale(1); opacity: 1; }
}
.is-success {
  background: var(--color-success) !important;
  color: var(--color-on-success) !important;
  border-color: var(--color-success) !important;
}
.is-success > * { animation: fbPop var(--motion-base) var(--ease-spring); }

/* Skeletons for anything over ~300ms. The sheen is a transform, so it costs
   nothing; the surface underneath is a state layer, so it follows the theme. */
@keyframes skelSheen { to { transform: translateX(200%); } }
.skeleton {
  position: relative;
  overflow: hidden;
  background: color-mix(in srgb, var(--color-foreground) 8%, transparent);
  border-radius: var(--radius-sm);
}
.skeleton::after {
  content: '';
  position: absolute;
  inset: 0;
  transform: translateX(-100%);
  background: linear-gradient(90deg, transparent,
              color-mix(in srgb, var(--color-foreground) 10%, transparent), transparent);
  animation: skelSheen calc(var(--motion-emphasis) * 3) var(--ease-standard) infinite;
}

/* Inline form validation feedback — a short shake, then done. */
@keyframes fieldShake {
  0%, 100% { transform: translateX(0); }
  25%      { transform: translateX(-4px); }
  75%      { transform: translateX(4px); }
}
.field-invalid {
  animation: fieldShake var(--motion-base) var(--ease-standard);
  border-color: var(--color-destructive) !important;
}

/* ----------------- COMPONENT & TYPOGRAPHY SYSTEM EXTENSIONS ----------------- */

/* Global form element font inheritances */
button, input, select, textarea {
  font-family: var(--font-primary);
}

/* Every button/input in this system is custom-styled — strip native OS/browser
   chrome so custom backgrounds/radii render cleanly instead of fighting a
   default appearance underneath (this was the cause of the mismatched-radius
   "floating pill" look on the loyalty points stepper). */
button {
  appearance: none;
  -webkit-appearance: none;
  -moz-appearance: none;
}
input[type="number"] {
  appearance: textfield;
  -moz-appearance: textfield;
}
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
  appearance: none;
  -webkit-appearance: none;
  margin: 0;
}

/* Premium Modal Layout and Animation System */
.modal-overlay {
  position: fixed;
  top: 0;
  inset-inline-start: 0;
  inset-inline-end: 0;
  bottom: 0;
  background: rgba(11, 15, 25, 0.75);
  backdrop-filter: blur(8px);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--transition-normal);
  padding: 1rem;
}

.modal-overlay.active {
  opacity: 1;
  pointer-events: auto;
}

.modal-content {
  background: var(--theme-surface);
  border: var(--border-thin) solid var(--theme-border);
  color: var(--theme-text);
  max-width: 550px;
  width: 100%;
  padding: 2.5rem;
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg), var(--shadow-glow);
  backdrop-filter: blur(var(--glass-blur));
  position: relative;
  max-height: 90vh;
  overflow-y: auto;
  transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.3s ease;
  transform: scale(0.95) translateY(10px);
  opacity: 0;
}

.modal-overlay.active .modal-content {
  transform: scale(1) translateY(0);
  opacity: 1;
}

/* Premium modal close button */
.modal-close-btn {
  position: absolute;
  top: 1.5rem;
  inset-inline-end: 1.5rem;
  background: transparent;
  border: none;
  color: var(--theme-primary);
  opacity: 0.5;
  cursor: pointer;
  padding: 0.5rem;
  border-radius: var(--radius-full);
  display: flex;
  align-items: center;
  justify-content: center;
  transition: all var(--transition-fast);
}

.modal-close-btn:hover {
  background: rgba(var(--accent-color-rgb), 0.1);
  color: var(--theme-accent);
  opacity: 1;
  transform: rotate(90deg);
}

/* Premium Option Checkbox Cards */
.checkout-gift-card {
  display: flex;
  align-items: center;
  gap: 1rem;
  padding: 1rem 1.25rem;
  background: rgba(var(--accent-color-rgb), 0.03);
  border: 1.5px solid var(--theme-border);
  border-radius: var(--radius-md);
  cursor: pointer;
  transition: all var(--transition-fast);
}

.checkout-gift-card:hover {
  background: rgba(var(--accent-color-rgb), 0.06);
  border-color: var(--theme-accent);
}

.checkout-gift-card input[type="checkbox"] {
  width: 1.2rem;
  height: 1.2rem;
  accent-color: var(--theme-accent);
  cursor: pointer;
}

html[dir="rtl"] {
  font-family: var(--font-primary);
  --drawer-hidden-transform: translateX(-100%);
}

/* YouTube-Style Text Blending (Plaintext Bidi) */
.product-title,
.product-desc,
.prod-tag,
#shop-name-title,
.hub-title,
.hub-subtitle,
.cart-item-title,
.bidi-text {
  unicode-bidi: plaintext;
  text-align: start;
}

/* Global Responsive Breakpoints - Mobile First */
/* Base styles apply to mobile viewport (<768px) */

@media (min-width: 768px) {
  /* Tablet viewport styles */
}

@media (min-width: 1024px) {
  /* Desktop viewport styles */
}

/* Cookie Consent Banner Styling */
.cookie-consent-banner {
  position: fixed;
  bottom: 1.5rem;
  left: 50%;
  transform: translateX(-50%);
  width: calc(100% - 3rem);
  max-width: 500px;
  background: var(--bg-secondary);
  border: var(--border-thin) solid var(--border-color);
  border-radius: var(--radius-md);
  padding: 1.25rem;
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  z-index: 9999;
  box-shadow: var(--shadow-lg);
  box-sizing: border-box;
}

/* Header Currency Selector Dropdown */
.currency-selector-wrapper {
  position: relative;
  display: inline-block;
}
.currency-select {
  background: var(--bg-secondary);
  border: var(--border-thin) solid var(--border-color);
  border-radius: var(--radius-sm);
  color: var(--text-primary);
  padding: 0.35rem 0.65rem;
  font-size: 0.85rem;
  font-weight: 600;
  cursor: pointer;
  transition: border-color 0.2s;
}
.currency-select:hover {
  border-color: var(--accent-color);
}

/* Premium Responsive Social Embed System */
.social-embed-wrapper {
  width: 100%;
  height: 100%;
  background: var(--color-background);
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  position: relative;
}

.social-embed-iframe {
  border: none;
  background: transparent;
  transform-origin: center center;
}




/* ══════════════════════════════════════════════════════════════════════
   [Mobile hardening] Applies to every page (common.css loads everywhere)
   ══════════════════════════════════════════════════════════════════════ */
@media (max-width: 768px) {
  /* iOS Safari zooms the viewport whenever a focused control is under 16px.
     Base .form-input is 0.95rem (~15.2px) and .tk-input 0.93rem (~14.9px), so
     every field on the storefront was triggering it. Phones get 16px; the
     desktop type scale is untouched. */
  input:not([type="checkbox"]):not([type="radio"]):not([type="range"]),
  textarea,
  select,
  .form-input,
  .tk-input {
    font-size: 1rem;
  }

  /* Comfortable touch height to match the larger text (skill §8 touch-friendly-input). */
  input:not([type="checkbox"]):not([type="radio"]):not([type="range"]),
  select,
  .form-input,
  .tk-input {
    min-height: 46px;
  }

  /* Kill the legacy 300ms tap delay on anything interactive. */
  a, button, input, select, textarea, label, [role="button"], [onclick] {
    touch-action: manipulation;
  }

  /* Never let a phone scroll sideways. `clip` not `hidden` — see note above. */
  html, body { overflow-x: hidden; overflow-x: clip; max-width: 100%; }

  /* Long unbroken strings (order IDs, coupon codes, URLs) must wrap. */
  body { overflow-wrap: break-word; }
}

/* Respect the notch / home indicator on any fixed page furniture.
   [Phase 13 ✅] Keyed off `(pointer: coarse)` as well as width, for the same
   reason as the touch-target block above: turn the phone sideways and the
   width clause stops matching while the home indicator is still there. In
   landscape the cutout moves to a SIDE, so the fixed bars need inline insets
   too — bottom-only was portrait-shaped thinking. */
@supports (padding: max(0px)) {
  @media (max-width: 768px), (pointer: coarse) {
    .bottom-nav,
    .mobile-bottom-nav {
      padding-bottom: max(0px, env(safe-area-inset-bottom));
      padding-inline-start: max(0px, env(safe-area-inset-left));
      padding-inline-end: max(0px, env(safe-area-inset-right));
    }
  }
}
/* The three page headers set their own inline padding later in the cascade
   (account.css, and an inline <style> in shop.html — which outranks every
   stylesheet). A rule here could not win, so each one folds the inset into its
   own `max()` at the site where it declares the padding. Do not add
   .shop-header / .ac-header / .tk-header to the block above; it would look
   correct and do nothing. */

/* ══════════════════════════════════════════════════════════════════════
   [Phase 28 ✅] Global accessibility layer — loads on every page
   ══════════════════════════════════════════════════════════════════════ */

/* ── Focus visibility (WCAG 2.4.7) ──────────────────────────────────────
   The audit found 41 of 62 focusable controls on the catalog with no focus
   indicator at all — every product-card action, the compare checkbox, the
   bundle CTA. Component-level :focus-visible rules stay authoritative; this
   is the floor beneath them, so nothing can be reached by keyboard invisibly.
   Deliberately NOT :focus — pointer users should not see rings on click.   */
:where(a, button, input, select, textarea, summary, [tabindex], [role="button"]):focus-visible {
  outline: 2px solid var(--accent-color, var(--color-accent));
  outline-offset: 2px;
  border-radius: inherit;
}
/* Checkboxes/radios sit inside padded labels; pull the ring out so it reads. */
:where(input[type="checkbox"], input[type="radio"]):focus-visible {
  outline-offset: 3px;
}
/* The ring must survive on accent-coloured surfaces too. */
:where(.glow-btn, .checkout-btn, .btn-primary, .co-full-submit):focus-visible {
  outline-color: var(--text-primary, var(--color-border-subtle));
}

/* ── Screen-reader-only text ────────────────────────────────────────────
   For headings and labels the design carries visually (a brand mark, an icon)
   but that the accessibility tree still needs. Clip-based, so the text stays
   in the a11y tree — `display:none` / `visibility:hidden` would remove it.  */
.visually-hidden:not(:focus):not(:active) {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ── Skip link (WCAG 2.4.1 Bypass Blocks) ───────────────────────────────
   Every page puts 10-30 header/nav controls before the content. Keyboard and
   switch users had no way past them. Off-screen until focused.              */
.skip-to-content {
  position: absolute;
  inset-inline-start: 0;
  top: 0;
  z-index: 100000;
  padding: 0.75rem 1.25rem;
  background: var(--accent-color, var(--color-accent));
  color: var(--color-on-accent);
  font-family: var(--font-primary);
  font-weight: 700;
  font-size: 0.95rem;
  text-decoration: none;
  border-end-end-radius: var(--radius-md);
  transform: translateY(-120%);
  transition: transform 0.18s cubic-bezier(0.22, 1, 0.36, 1);
}
.skip-to-content:focus {
  transform: translateY(0);
}

/* ── Touch targets (Apple HIG 44pt / Material 48dp) ─────────────────────
   Storefront chrome was still shipping 38px icon buttons, 36px filter chips
   and a 19px-tall "Details" link on phones. Sized up at the phone breakpoint
   only, so the desktop density is untouched.

   [Phase 13 ✅] `(pointer: coarse)` added. Width is the wrong question here —
   what makes 44px necessary is a FINGER, not a narrow screen. A phone turned
   sideways is 812px wide, cleared `max-width: 768px`, and every control in
   this list silently dropped back to its 38px mouse size on the same device
   that needed 44 a second earlier: 89 sub-44 targets in landscape against 0
   in portrait. The width clause stays for narrow mouse-driven windows.       */
@media (max-width: 768px), (pointer: coarse) {
  .icon-btn,
  .pill-btn,
  .outline-btn,
  .mobile-filter-chip,
  .add-cart-btn,
  .details-btn,
  .compare-btn,
  .buy-bundle-btn,
  .filter-rail-collapse-btn,
  .filter-sheet-close,
  .cart-close-btn,
  .modal-close-btn,
  .qty-btn,
  .pd-qty-btn,
  .tk-ctrl-btn,
  .au-pwd-toggle,
  .qv-close-btn,
  /* [Phase 13 ✅] The currency <select> and the two admin login fields were
     missing from this list. They passed in portrait only because the phone
     breakpoints padded them; in landscape they measured 34px and 23px. */
  .currency-select,
  .tk-ctrl-select,
  #carousel-prev,
  #carousel-next {
    min-height: 44px;
    min-width: 44px;
  }
  /* Fields are wide already, so they need the height floor, not the width one:
     min-width:44px on a full-bleed input does nothing, and on a narrow one it
     would fight the grid. */
  input:not([type="checkbox"]):not([type="radio"]):not([type="hidden"]),
  select,
  textarea,
  .form-input,
  .al-field-shell input {
    min-height: 44px;
  }
  /* Inline text links in the footer were 15px tall — under the 24px WCAG 2.5.8
     minimum. Padding grows the hit area without changing the type scale. */
  .footer-links a {
    display: inline-block;
    padding-block: 0.5rem;
  }
  /* [Phase 8 ✅] Measured offenders on a 390px screen. Each grows its HIT AREA
     with padding and a min-height rather than its type size, so the header and
     footer look unchanged while becoming thumb-safe.
       .header-brand / .ac-header-brand  were 96x26 and 105x21
       footer nav links outside .footer-links were 135x37 and 123x21 */
  .header-brand,
  .ac-header-brand,
  .tk-back,
  .shop-brand > a,
  .footer-nav a,
  .footer-col a,
  .shop-footer nav a {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
  }
  /* `.footer-links a` above already gets padding-block, but at 0.5rem it lands
     at 37px — still short of 44. Padding alone cannot be pushed further without
     spreading the footer, so the list item carries the floor instead. */
  .footer-links li > a {
    display: flex;
    align-items: center;
    min-height: 44px;
    padding-block: 0;
  }
  /* An icon-only control in the account header: 38px wide because its own rule
     in account.css sets a smaller min-width and that file loads later. This
     wins on specificity without touching the shared .tk-ctrl-btn sizing. */
  .ac-header-controls .tk-ctrl-btn {
    min-width: 44px;
    min-height: 44px;
  }
  /* The quantity readout sits between two steppers and is typed into. */
  #qty-val {
    min-width: 44px;
    min-height: 44px;
  }
  /* Tiny native checkboxes can't grow without wrecking the layout, so the
     padded label around them carries the hit area instead. */
  label.filter-checkbox,
  label.filter-sheet-checkbox,
  label.compare-top-check {
    min-height: 44px;
    display: flex;
    align-items: center;
  }
  label.compare-top-check {
    min-width: 44px;
    justify-content: center;
  }
}

/* ── Autofilled fields (contrast) ───────────────────────────────────────
   Chrome paints :-webkit-autofill with a hard-coded light blue background and
   black text via a UA-internal rule that normal `background` cannot override —
   on the dark login/checkout/track forms that rendered as a white box of black
   text in the middle of a dark panel. The inset-shadow trick is the only way to
   repaint it; -webkit-text-fill-color is the only way to recolour the text.    */
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
textarea:-webkit-autofill,
select:-webkit-autofill {
  -webkit-text-fill-color: var(--text-primary);
  -webkit-box-shadow: 0 0 0 1000px var(--card-bg, var(--bg-secondary)) inset;
  box-shadow: 0 0 0 1000px var(--card-bg, var(--bg-secondary)) inset;
  caret-color: var(--text-primary);
  transition: background-color 100000s ease-in-out 0s;
}

/* ── Reduced motion (WCAG 2.3.3) ────────────────────────────────────────
   Individual stylesheets each carry their own block, but common.css had none,
   so anything styled here — plus every inline-styled animation — ignored the
   preference. This is the global backstop.                                  */
/* [Phase 7 ✅] Reduced motion. The blanket rule alone is not enough here:
   every `.load-*` option parks its element at opacity 0 and a transform, and
   only the `.loaded` class brings it back. Collapsing the duration makes that
   instant, but if the observer that adds `.loaded` never fires — offscreen
   content, a scroll listener that lost its element — the shopper is left with
   an invisible product grid. So the from-states are explicitly neutralised
   too: with reduced motion the content is simply *there*, class or not. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }

  /* [Phase 13.1 ✅] `drawer` and `pulse` were missing from this list. Both are
     from-states carrying `opacity: 0`, so under reduced motion their admin
     preview tile stayed permanently blank — the ticker is deliberately not
     running, and nothing else was neutralising them. */
  .load-stagger-fade, .load-stagger-slide, .load-stagger-zoom,
  .load-text-reveal, .load-parallax-slide,
  .nav-transition-fade, .nav-transition-slide, .nav-transition-zoom,
  .nav-transition-drawer, .nav-transition-pulse,
  .nav-transition-scroll-snap, .nav-transition-cart-bounce {
    opacity: 1 !important;
    transform: none !important;
    clip-path: none !important;
  }

  /* A frozen sheen is worse than none — it reads as a rendering artefact. */
  .skeleton::after { display: none !important; }

  /* The one exception: a spinner that does not spin cannot say "waiting".
     Rotation is the entire message, so it is kept and slowed instead. */
  .is-loading::after {
    animation-duration: 1.2s !important;
    animation-iteration-count: infinite !important;
  }
}
