/* IRANSans cdn kept 404ing, using Vazirmatn under the same font-family name so
   we don't have to touch every page that references 'IRANSans' */
@font-face{
  font-family: 'IRANSans';
  src: url('fonts/Vazirmatn-Regular.woff2') format('woff2');
  font-weight: normal;
  font-display: swap;
}
@font-face{
  font-family: 'IRANSans';
  src: url('fonts/Vazirmatn-Bold.woff2') format('woff2');
  font-weight: bold;
  font-display: swap;
}
/* =====================================================================
   PORTED FROM FIGMA — "Design System (Community)", file IgKUhP2kXiGBDxTyTRVMq7
   version 2380644803925071189, read via the REST API (session 11).
   Every hex, size, line-height, radius, spacing step and shadow below is a
   value that exists in that file, not an approximation of it.

   HOW IT WAS READ: /v1/files/:key carries both the style registry
   (file.styles: id -> {name, styleType}) and every node that consumes a style
   (node.styles: {fill|text|effect: styleId}), so values are recoverable by
   walking the tree. /v1/variables/local returned 403 (needs the
   file_variables:read scope, Enterprise-only) and /v1/styles returned an empty
   list because it only reports PUBLISHED team-library styles — this file's
   styles are local. Neither was needed.

   WHAT WAS NOT TAKEN: the file's Primary ramp (#4E61F6, an indigo) — our brand
   is purple -> pink and stays. Its Inter typeface — Persian needs Vazirmatn.
   Its 1.5 body leading — see the leading note below.

   FOUR SWATCHES IN THE FILE ARE WRONG and were repaired rather than copied:
   Primary/200, Blue/500, Green/500, Red/500 and Yellow/500 are each a verbatim
   duplicate of their own /50 (or, for Primary/200, of /500), which breaks the
   ramp's monotonicity. The 500 steps below are interpolated between the file's
   own 400 and 600 so the ramps actually ramp.
   ===================================================================== */
:root{
  /* brand — overridden at runtime by head.php from site_settings
     (theme_color_purple / theme_color_pink), which hold #7C3AED / #E8339A.
     These fallbacks now match, so the stylesheet alone is self-consistent. */
  --purple:#7C3AED; --purple-rgb:124,58,237;
  --pink:#E8339A; --pink-rgb:232,51,154;
  --lav:#F1EAFA; --pink-tint:#FBE9F0;   /* brand tints, not Figma neutrals */

  /* ---- Figma "Grey" ramp, verbatim ---- */
  --grey-50:#F9FAFB;  --grey-100:#F3F4F6; --grey-200:#E5E7EA; --grey-300:#D2D5DB;
  --grey-400:#9EA2AE; --grey-500:#6D717F; --grey-600:#4D5461; --grey-700:#394050;
  --grey-800:#212936; --grey-900:#131927;

  /* ---- Figma status ramps, verbatim (the Colors frame labels them
     Green/Success, Red/Danger, Yellow/Warning, Blue/Info) ---- */
  --green-50:#ECF8EF;  --green-100:#C5E9CD; --green-400:#69C57D;
  --green-500:#53B669; --green-600:#3DA755; --green-700:#308242; --green-800:#256533;
  --red-50:#FDECEC;    --red-100:#FAC5C3;   --red-400:#F16965;
  --red-500:#E5544F;   --red-600:#D93E39;   --red-700:#A9302D;   --red-800:#832523;
  --yellow-50:#FFF7E6; --yellow-100:#FFE5B0; --yellow-400:#FFBB33;
  --yellow-500:#F4AB19; --yellow-600:#E89B00; --yellow-700:#B57900; --yellow-800:#8C5E00;
  --blue-50:#E6F4FF;   --blue-100:#B0DEFF;  --blue-400:#33AAFF;
  --blue-500:#0D99F5;  --blue-600:#0088E8;  --blue-700:#006AB5;  --blue-800:#00528C;

  /* ---- the site's long-standing neutral aliases, now pointing at the ported
     ramp instead of hand-picked hexes. This is the whole leverage of the port:
     every rule that already says var(--ink) / var(--gray) / var(--cream)
     re-themes onto the Figma neutrals without being touched. ---- */
  --ink:var(--grey-900);        /* #131927 — 17.6:1 on white */
  --ink-soft:var(--grey-700);   /* #394050 — 10.4:1 */
  --gray:var(--grey-500);       /* #6D717F —  4.9:1, marginally better than the
                                   #6B7280 it replaces, so no contrast regression */
  --cream:var(--grey-50);       /* #F9FAFB — the app background */
  --yellow:var(--yellow-400); --yellow-tint:var(--yellow-50);
  --hairline:rgba(19,25,39,.08);  /* Grey/900 at the same alpha the file's own
                                     shadows use, instead of a different ink */
  --grad: linear-gradient(90deg,var(--purple),var(--pink));
  --radius:16px;   /* legacy single-radius alias, kept for old rules */
  --glass-border: 1px solid rgba(255,255,255,.35);

  --success:var(--green-600); --success-bg:var(--green-50);
  --danger:var(--red-600);    --danger-bg:var(--red-50);
  --warning:var(--yellow-600);--warning-bg:var(--yellow-50);
  --info:var(--blue-600);     --info-bg:var(--blue-50);
  --disabled-bg:var(--grey-200); --disabled-text:var(--grey-400);
  --success-on-dark:var(--green-400); --danger-on-dark:var(--red-400);

  /* ---- spacing: the file's autolayout steps.
     4/8/12/16/24/32/48/64 were already right; 2/10/14/20 are half-steps the
     Figma components genuinely use (card padding is 20, navbar 10, card 14). ---- */
  --space-0-5:2px;
  --space-1:4px; --space-2:8px; --space-2-5:10px; --space-3:12px;
  --space-3-5:14px; --space-4:16px; --space-4-5:20px;
  --space-5:24px; --space-6:32px; --space-6-5:40px; --space-7:48px; --space-8:64px;

  /* ---- type scale: the file's own steps (10/12/14/16/18/24/28/32/40/48).
     The names are unchanged so no component has to be re-pointed; the three
     that were off-scale (13, 15, 20) snap onto the nearest real step, and 36/60
     become the file's 32/48. ---- */
  --text-3xs:10px;                        /* Figma C3. Caption */
  --text-xs:12px;                         /* C1 / C2 / LABEL */
  --text-sm:12px;                         /* was 13 — not a step in the file */
  --text-base:14px;                       /* B3 / B4 */
  --text-md:14px;                         /* was 15 — not a step in the file */
  --text-lg:16px;                         /* B1 / B2 / S2 */
  --text-xl:18px;                          /* was 20 — file uses 18 (S1) */
  --text-2xl:24px;                        /* H5 */
  --text-3xl:28px;                        /* H4 */
  --text-4xl:32px;                        /* was 36 — file's H3 */
  --text-5xl:40px;                        /* was 60 — file's H2 */
  --text-6xl:48px;                        /* H1 */

  /* ===== motion tokens — borrowed from Material's easing/duration discipline,
     kept meaningful (enter/exit/emphasis) rather than decorative. transform+opacity
     only; every consumer has a prefers-reduced-motion escape hatch. ===== */
  --ease-standard: cubic-bezier(0.4, 0, 0.2, 1);   /* symmetric, on-screen moves */
  --ease-decelerate: cubic-bezier(0, 0, 0.2, 1);   /* entering — fast in, gentle stop */
  --ease-accelerate: cubic-bezier(0.4, 0, 1, 1);   /* exiting — gentle start, fast out */
  --ease-emphasized: cubic-bezier(0.2, 0, 0, 1);   /* hero moments, expressive */
  --dur-1:100ms; --dur-2:150ms; --dur-3:200ms; --dur-4:250ms; --dur-5:300ms; --dur-6:400ms;

  /* ===== elevation — PORTED. The file's "Shadow" frame defines eight levels
     (100…800), each a TWO-layer drop shadow with a negative spread: a wide soft
     layer for the ambient falloff plus a tight one for the contact edge. That
     second layer is what the previous single-layer shadows were missing, and it
     is why raised surfaces now read as sitting on the page rather than floating
     over it. All eight are exposed; --elevation-1..5 alias the five the site
     actually consumes, so no existing rule needs editing.
     Figma blur radius maps to the CSS blur, spread to spread, ink #131927. ===== */
  --shadow-100: 0 4px 4px -2px rgba(19,25,39,.08),  0 2px 4px -2px rgba(19,25,39,.12);
  --shadow-200: 0 8px 8px -4px rgba(19,25,39,.08),  0 4px 6px -4px rgba(19,25,39,.12);
  --shadow-300: 0 8px 16px -6px rgba(19,25,39,.08), 0 6px 8px -6px rgba(19,25,39,.12);
  --shadow-400: 0 8px 24px -4px rgba(19,25,39,.08), 0 6px 12px -6px rgba(19,25,39,.12);
  --shadow-500: 0 10px 32px -4px rgba(19,25,39,.10),0 6px 14px -6px rgba(19,25,39,.12);
  --shadow-600: 0 12px 42px -4px rgba(19,25,39,.12),0 8px 18px -6px rgba(19,25,39,.12);
  --shadow-700: 0 14px 64px -4px rgba(19,25,39,.12),0 8px 22px -6px rgba(19,25,39,.12);
  --shadow-800: 0 18px 88px -4px rgba(19,25,39,.14),0 8px 28px -6px rgba(19,25,39,.12);

  --elevation-0: none;
  --elevation-1: var(--shadow-100);   /* resting cards */
  --elevation-2: var(--shadow-200);   /* strips, subtle raise */
  --elevation-3: var(--shadow-300);   /* card hover, dropdowns */
  --elevation-4: var(--shadow-500);   /* toasts, search suggest */
  --elevation-5: var(--shadow-700);   /* mega-menu, modals */
  --elevation-6: var(--shadow-800);   /* the file's top step */
  /* brand-tinted lift, for interactive purple/pink surfaces on hover — not from
     the file (it has no brand-tinted shadow), kept because it is our identity */
  --elevation-brand: 0 12px 28px rgba(var(--purple-rgb),.16);
}
*{box-sizing:border-box;}

/* lucide svgs, self-hosted in assets/icons */
.icon{width:20px;height:20px;stroke-width:2px;flex-shrink:0;display:inline-block;vertical-align:middle;}
.icon-sm{width:16px;height:16px;}
.icon-lg{width:26px;height:26px;}
.icon-xl{width:34px;height:34px;}
body{margin:0;font-family:var(--font-family,'IRANSans'),sans-serif;background:var(--cream);color:var(--ink);}
a{text-decoration:none;color:inherit;}
ul{list-style:none;margin:0;padding:0;}

a, button, input, textarea, select, .card, .pill, .icon-btn, .btn-grad, .btn-primary, .btn-ghost,
.btn-sm, .admin-nav a, .footer-links div, .socials a, .sub-row button, .pd-opt {
  transition: transform .18s ease, box-shadow .18s ease, background-color .18s ease, color .18s ease, border-color .18s ease, filter .18s ease;
}
a:focus-visible, button:focus-visible, input:focus-visible, textarea:focus-visible, select:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px rgba(var(--purple-rgb),.35);
  border-color: var(--purple);
}
.btn-grad:hover, .btn-primary:hover{ filter:brightness(1.08); transform:translateY(-2px); box-shadow:0 14px 28px rgba(var(--purple-rgb),.32); }
.btn-grad:active, .btn-primary:active{ transform:scale(.98); filter:brightness(.97); }
.btn-ghost:hover{ border-color:var(--purple); color:var(--purple); transform:translateY(-2px); }
.btn-ghost:active{ transform:scale(.98); }
.icon-btn:hover{ background:var(--pink-tint); transform:translateY(-2px); box-shadow:0 10px 20px rgba(var(--purple-rgb),.18); }
/* .mega-open gets toggled by app.js on hover-intent, :focus-within is the css-only
   fallback for keyboard nav */
.mega-trigger{position:relative;}
.mega-panel{
  --mega-shift-x: 0px;
  position:absolute; top:100%; left:50%; transform:translate(calc(-50% + var(--mega-shift-x)), 0);
  /* width:max-content bounded by min/max — a 1-2 subcategory group shrinks to a snug
     box instead of sitting fixed-width with a lot of dead space; a big group (or one
     with a promo image, which forces its own flex-basis) grows up to the cap. */
  width:max-content;
  /* min/max both account for the 188px media tile every panel now carries: the floor
     keeps a 1-subcategory group from squeezing its link column down beside the tile,
     the ceiling gives a big group the same link width it had before the tile existed. */
  min-width:380px;
  max-width:min(768px, calc(100vw - 48px));
  max-height:min(480px, calc(100vh - 120px));
  overflow-y:auto;
  background:rgba(255,255,255,.98);
  backdrop-filter:blur(20px);
  -webkit-backdrop-filter:blur(20px);
  border:var(--glass-border);
  border-radius:20px;
  box-shadow:var(--elevation-5), inset 0 1px 0 rgba(255,255,255,.5);
  margin-top:var(--space-2);
  opacity:0; visibility:hidden; pointer-events:none;
  transition:opacity var(--dur-3) var(--ease-standard), transform var(--dur-3) var(--ease-emphasized), visibility var(--dur-3);
  z-index:200;
  display:flex; flex-direction:column;
}
.mega-panel::before{content:'';position:absolute;top:0;right:var(--space-6);left:var(--space-6);height:4px;background:var(--grad);border-radius:0 0 4px 4px;}
.mega-trigger.mega-open .mega-panel, .mega-trigger:focus-within .mega-panel{
  opacity:1; visibility:visible; pointer-events:auto; transform:translate(calc(-50% + var(--mega-shift-x)), 6px);
}
.mega-panel-body{display:flex;}
.mega-panel-inner{display:flex; flex-wrap:wrap; align-content:flex-start; gap:var(--space-5) var(--space-6); padding:var(--space-5) var(--space-6); flex:1 1 auto;}
.mega-col{display:flex; flex-direction:column; gap:var(--space-1); min-width:118px; flex:1 1 118px;}
.mega-col-title{font-size:var(--text-xs); font-weight:700; color:var(--purple); letter-spacing:.02em; margin-bottom:var(--space-2); padding-bottom:var(--space-2); border-bottom:1.5px solid var(--lav);}
.mega-col a{display:block; font-size:var(--text-md); color:var(--ink); padding:var(--space-2) 0; transition:color .15s ease, padding-right .15s ease;}
.mega-col a:hover{color:var(--pink); padding-right:var(--space-1);}
/* category media tile, sazkala-style — sits flush at the panel's outer edge so the
   panel's own overflow+border-radius clips its outer corner to match, no extra radius
   needed here. EVERY panel renders one now: an admin-uploaded photo when the group has
   one, otherwise the group's icon over the brand gradient, so the panels read as one
   consistent set instead of one promo image and five blank edges. */
.mega-panel-media{
  flex:0 0 188px; align-self:stretch; min-height:220px;
  position:relative; display:flex; flex-direction:column; overflow:hidden;
  background:linear-gradient(var(--panel-angle,140deg), var(--purple), var(--pink));
  color:#fff;
}
/* soft top-corner highlight so the fallback tile has some depth instead of reading
   as a flat swatch. purely decorative, sits under both the photo and the caption. */
.mega-panel-media::before{
  content:''; position:absolute; inset:0; z-index:0;
  background:radial-gradient(120% 80% at 72% 0%, rgba(255,255,255,.22), transparent 62%);
}
.mega-panel-media img{
  position:absolute; inset:0; z-index:0;
  width:100%; height:100%; object-fit:cover; display:block;
  transition:transform var(--dur-5) var(--ease-standard);
  /* The banner art is 1376x768 landscape in a 188x220 portrait tile, so cover already
     shows only the middle ~48% of the width at full height. Nudging the window toward the
     top-left moves it a further 5% off the bottom-right, where the source art carries a
     small sparkle watermark: measured, the visible slice now ends at 68.6% of the width
     and the watermark starts at 90%. The vertical value is inert at this aspect ratio —
     there is no vertical overflow to slide — and is here only so a shorter, wider tile
     would stay clear too. */
  object-position:40% 40%;
}
/* display:contents so the <picture> wrapper generates no box of its own: the img is
   positioned against .mega-panel-media and has to keep resolving to it, and the tile is a
   flex column that must not gain a stray zero-height item. */
.mega-panel-media picture{ display:contents; }
/* scrim, photo tiles only — keeps the caption legible over an arbitrary upload */
.mega-panel-media.has-photo::after{
  content:''; position:absolute; inset:0; z-index:1;
  background:linear-gradient(to top, rgba(26,29,38,.75), rgba(26,29,38,.12) 55%, transparent);
}
.mega-media-art{
  flex:1 1 auto; position:relative; z-index:1;
  display:flex; align-items:center; justify-content:center;
  padding-top:var(--space-5);
  transition:transform var(--dur-5) var(--ease-standard);
}
.mega-media-art .icon{width:54px;height:54px;stroke-width:1.5px;}
.mega-media-art .icon-nav-img{width:54px;height:54px;object-fit:contain;}
/* margin-top:auto pins the caption to the bottom in the photo case too, where there's
   no .mega-media-art above it to push it down */
.mega-media-cap{
  margin-top:auto; position:relative; z-index:2;
  display:flex; flex-direction:column; gap:2px; padding:var(--space-4);
  text-shadow:0 1px 3px rgba(26,29,38,.35);
}
.mega-media-title{font-size:var(--text-base);font-weight:700;line-height:1.45;}
.mega-media-meta{font-size:var(--text-xs);opacity:.88;}
.mega-panel-media:hover img{transform:scale(1.06);}
.mega-panel-media:hover .mega-media-art{transform:scale(1.08);}
.mega-panel-media:hover .mega-media-title{text-decoration:underline;text-underline-offset:3px;}
@media (prefers-reduced-motion: reduce){
  .mega-panel-media img, .mega-media-art{transition:none;}
  .mega-panel-media:hover img, .mega-panel-media:hover .mega-media-art{transform:none;}
}
.mega-panel-footer{padding:var(--space-4) var(--space-7); border-top:1.5px solid var(--lav); text-align:center;}
.mega-view-all{font-weight:700; color:var(--purple) !important;}
.pill:hover{ border-color:var(--purple); color:var(--purple); transform:translateY(-2px); }
.pill.active:hover{ color:#fff; }
.card .heart:hover{ transform:scale(1.12); box-shadow:0 6px 14px rgba(26,29,38,.18); }
.card .add:hover{ filter:brightness(1.1); transform:scale(1.08); }
.btn-sm:hover{ filter:brightness(.96); transform:translateY(-1px); }
.admin-nav a:hover:not(.active){ background:#2E2245; color:#fff; }
.socials a{ cursor:pointer; }
/* cursor:pointer moved from the wrapper div onto the anchor itself — the div is not
   the clickable thing, and pointing at padding that does nothing is a small lie */
.footer-links div a{ cursor:pointer; }
.footer-links div a:hover{ color:#fff; }
.socials a:hover{ background:var(--grad); transform:translateY(-2px); }
.sub-row button:hover{ filter:brightness(1.06); transform:translateY(-1px); }
.pd-opt:hover{ border-color:var(--purple); }
header.site.is-scrolled{ box-shadow:0 10px 26px rgba(26,29,38,.12); }

@media (prefers-reduced-motion: reduce){
  a, button, input, textarea, select, .card, .pill, .icon-btn, .btn-grad, .btn-primary, .btn-ghost,
  .btn-sm, .admin-nav a, .footer-links div, .socials a, .sub-row button, .pd-opt {
    transition: none;
  }
  .btn-grad:hover, .btn-primary:hover, .btn-ghost:hover, .icon-btn:hover, .pill:hover, .card:hover { transform:none; }
}

/* overflow-x:clip (with overflow-y left visible, so the open mega-panel can still hang
   below the header) stops the closed mega-panels from widening the document.
   They're visibility:hidden but still laid out, and until app.js runs clampPosition()
   an edge trigger's panel sits centered on that trigger and spills past the viewport —
   producing a horizontal scrollbar for the first few frames of every page load. The JS
   clamp fixes it after paint; this makes the layout correct before paint, so the clamp
   is now belt-and-braces rather than the only defence. */
header.site{background:rgba(255,255,255,.82);backdrop-filter:blur(16px) saturate(160%);-webkit-backdrop-filter:blur(16px) saturate(160%);border-bottom:1px solid var(--hairline);padding:0 64px;box-shadow:0 1px 3px rgba(26,29,38,.04);position:sticky;top:0;z-index:40;isolation:isolate;overflow-x:clip;overflow-y:visible;}
.header-main{display:flex;align-items:center;gap:28px;padding:16px 0;}
.logo{display:flex;align-items:center;gap:10px;font-weight:700;font-size:var(--text-2xl);flex-shrink:0;letter-spacing:-.01em;}
.mark{width:42px;height:42px;border-radius:12px;background:var(--grad);display:flex;align-items:center;justify-content:center;color:#fff;font-size:18px;}
.mark.logo-image{object-fit:cover;}
.menu-toggle{display:none;align-items:center;justify-content:center;width:40px;height:40px;background:none;border:none;font-size:22px;cursor:pointer;color:var(--ink);}
/* margin-right:auto (not justify-content:space-between on .header-main) is deliberate:
   .search-box-wrap already has flex-grow, and once it hits its own max-width the leftover
   free space needs to collapse onto ONE side (before .actions) rather than getting spread
   into every gap in the row, which would shove .logo away from the true right edge too.
   this is RTL: main-start is the right edge, so the auto margin that absorbs space and
   pushes .actions toward the far LEFT (main-end) goes on its right (start-facing) side —
   margin-left:auto here would do the opposite and push it away from the edge. */
.actions{display:flex;align-items:center;gap:14px;flex-shrink:0;margin-right:auto;}
.btn-grad{background:linear-gradient(90deg,rgba(var(--purple-rgb),.92),rgba(var(--pink-rgb),.92));background-size:160% 160%;backdrop-filter:blur(10px) saturate(160%);-webkit-backdrop-filter:blur(10px) saturate(160%);color:#fff;border:var(--glass-border);padding:11px 22px;border-radius:999px;font-family:inherit;font-weight:600;font-size:14px;cursor:pointer;box-shadow:0 8px 20px rgba(var(--purple-rgb),.25), inset 0 1px 0 rgba(255,255,255,.25);white-space:nowrap;}
.icon-btn{width:44px;height:44px;border-radius:999px;background:rgba(var(--purple-rgb),.1);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);border:var(--glass-border);display:flex;align-items:center;justify-content:center;font-size:17px;color:var(--purple);position:relative;cursor:pointer;flex-shrink:0;}
.icon-btn.js-wishlist-toggle.active{background:var(--pink);color:#fff;}
.badge{position:absolute;top:-4px;left:-4px;width:19px;height:19px;border-radius:999px;background:var(--pink);color:#fff;font-size:10px;font-weight:700;display:flex;align-items:center;justify-content:center;border:2px solid #fff;}

/* row 2 — thin secondary links, sits between the announce bar and the main header */
.header-topline{background:var(--cream);border-bottom:1px solid var(--hairline);padding:8px 64px;}
.topline-links{display:flex;justify-content:flex-end;gap:22px;overflow-x:auto;}
.topline-links a{font-size:12.5px;color:var(--gray);white-space:nowrap;}
.topline-links a:hover{color:var(--purple);}

/* row 3 — search sits between logo and the action icons, grows to fill whatever's left.
   split into a positioning wrapper + the pill itself so the autocomplete dropdown has a
   stable, non-transformed ancestor to anchor to (same reasoning as the mega-panel: don't
   position an overlay off an element that itself has hover/focus-driven CSS changing). */
.search-box-wrap{flex:1 1 auto;position:relative;max-width:640px;}
.search-box{display:flex;align-items:center;background:#fff;border:1.5px solid var(--hairline);border-radius:999px;padding:4px 6px 4px 18px;width:100%;transition:border-color .18s ease, box-shadow .18s ease;}
.search-box input{flex:1;border:none;outline:none;font-family:inherit;font-size:14px;background:none;padding:8px 4px;}
/* the naked input has no border-radius, so the generic input:focus-visible ring (a sharp
   rectangle) looked broken clipped inside this rounded pill — move the ring to the pill itself */
.search-box input:focus-visible{outline:none;box-shadow:none;border-color:transparent;}
.search-box button{width:38px;height:38px;border-radius:999px;border:none;background:var(--grad);color:#fff;display:flex;align-items:center;justify-content:center;cursor:pointer;flex-shrink:0;}
.search-box:focus-within{border-color:var(--purple);box-shadow:0 0 0 3px rgba(var(--purple-rgb),.35);}

.search-suggest{position:absolute;top:calc(100% + 8px);right:0;left:0;background:#fff;border:1px solid var(--hairline);border-radius:20px;box-shadow:var(--elevation-4);z-index:60;overflow:hidden;}
.search-suggest[hidden]{display:none;}
.search-suggest-item{display:flex;align-items:center;gap:12px;padding:10px 16px;color:var(--ink);}
.search-suggest-item:hover, .search-suggest-item.is-active{background:var(--lav);}
.search-suggest-thumb{width:40px;height:40px;border-radius:10px;flex-shrink:0;display:flex;align-items:center;justify-content:center;font-size:18px;color:var(--purple);overflow:hidden;}
.search-suggest-thumb img{width:100%;height:100%;object-fit:cover;}
.search-suggest-info{flex:1;min-width:0;}
.search-suggest-name{font-size:13.5px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.search-suggest-price{font-size:12px;color:var(--gray);margin-top:2px;}
.search-suggest-empty{padding:18px 16px;text-align:center;color:var(--gray);font-size:13px;}
.search-suggest-viewall{display:block;padding:12px 16px;text-align:center;font-weight:700;font-size:13px;color:var(--purple);border-top:1.5px solid var(--lav);background:var(--cream);}
.search-suggest-viewall:hover{color:var(--pink);}
/* skeleton shimmer while the debounced request is in flight */
.search-suggest-skel{padding:10px 16px;display:flex;align-items:center;gap:12px;}
.search-suggest-skel .sk-thumb, .search-suggest-skel .sk-line{background:linear-gradient(90deg,var(--lav) 25%,#fbf6ff 37%,var(--lav) 63%);background-size:400% 100%;animation:skelShimmer 1.4s ease infinite;border-radius:8px;}
.search-suggest-skel .sk-thumb{width:40px;height:40px;flex-shrink:0;border-radius:10px;}
.search-suggest-skel .sk-lines{flex:1;display:flex;flex-direction:column;gap:6px;}
.search-suggest-skel .sk-line{height:10px;}
.search-suggest-skel .sk-line.short{width:55%;}
@keyframes skelShimmer{0%{background-position:100% 0;}100%{background-position:0 0;}}
@media (prefers-reduced-motion: reduce){
  .search-suggest-skel .sk-thumb, .search-suggest-skel .sk-line{animation:none;}
}

.phone-btn{display:flex;align-items:center;gap:8px;padding:6px 4px;border-radius:10px;color:var(--purple);font-weight:700;font-size:14px;white-space:nowrap;direction:ltr;}
.phone-btn .icon{color:var(--pink);}

/* row 4 — icon-above-label category tabs, reuses .mega-trigger/.mega-panel hover-intent
   from the old text mega-menu, just restyled.
   overflow-x:auto here used to also force overflow-y:auto per spec (you can't have one
   axis visible and the other not) which clipped the absolutely-positioned .mega-panel
   to this row's height — that's what was truncating the dropdown. Scroll is only needed
   once the panel is already display:none (see the max-width:860px block), so it's scoped
   there instead of sitting on the row unconditionally. */
.icon-nav{display:flex;justify-content:space-between;gap:4px;padding:2px 0 12px;}
.icon-nav-item{flex:1 1 0;min-width:104px;}
.icon-nav-link{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;height:76px;padding:8px 10px;border-radius:14px 14px 0 0;font-size:12.5px;font-weight:600;color:var(--ink);text-align:center;line-height:1.35;}
.icon-nav-link .icon{color:var(--purple);flex-shrink:0;}
.icon-nav-img{width:26px;height:26px;object-fit:contain;flex-shrink:0;}
.icon-nav-item:hover .icon-nav-link, .icon-nav-item.mega-open .icon-nav-link, .icon-nav-link.active{background:var(--lav);color:var(--purple);}
.icon-nav-link.active{color:var(--purple);}

main{max-width:1440px;margin:0 auto;padding:36px 64px 80px;}
h1.pagetitle{font-size:var(--text-3xl);font-weight:700;margin:6px 0 4px;letter-spacing:-.02em;}
p.pagesub{color:var(--gray);font-size:14px;margin:0 0 26px;}

.listing-layout{display:flex;gap:32px;align-items:flex-start;}
.filters{width:260px;flex-shrink:0;background:#fff;border:1px solid var(--hairline);border-radius:20px;padding:24px;box-shadow:0 2px 8px rgba(26,29,38,.04);}
.filters h3{font-size:15px;margin:0 0 14px;}
.filters .group{margin-bottom:22px;}
.filters label{display:flex;align-items:center;gap:8px;font-size:13px;color:var(--gray);margin-bottom:10px;cursor:pointer;}
.filters input[type=checkbox]{accent-color:var(--pink);width:16px;height:16px;}
.range{height:6px;border-radius:999px;background:var(--lav);position:relative;margin:14px 0 6px;}
.range .fill{position:absolute;top:0;bottom:0;border-radius:999px;background:var(--grad);}
.price-inputs{display:flex;align-items:center;gap:8px;}
.price-inputs input[type=number]{width:0;flex:1;border:1px solid var(--hairline);border-radius:10px;padding:8px 10px;font-family:inherit;font-size:12.5px;color:var(--ink);}
.price-inputs span{color:var(--gray);font-size:12px;}
.filters .price-apply{width:100%;margin-top:12px;background:var(--grad);color:#fff;border:none;border-radius:10px;padding:10px;font-family:inherit;font-size:13px;font-weight:600;cursor:pointer;}
.grid-top{display:flex;justify-content:space-between;align-items:center;margin-bottom:18px;}
select.sort{border:1px solid var(--hairline);border-radius:12px;padding:9px 14px;font-family:inherit;font-size:13px;background:#fff;}
.product-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:22px;}
/* کارت‌ها — hairline border به‌جای سایه سنگین، حس بوتیک و خلوت */
.card{display:block;position:relative;background:#fff;border:1px solid var(--hairline);border-radius:20px;overflow:hidden;box-shadow:var(--elevation-1);transition:transform var(--dur-4) var(--ease-standard), box-shadow var(--dur-4) var(--ease-standard), border-color var(--dur-4) var(--ease-standard);cursor:pointer;}
.card:hover{transform:translateY(-3px);box-shadow:var(--elevation-brand);border-color:rgba(var(--purple-rgb),.2);}
.card .img{height:190px;display:flex;align-items:center;justify-content:center;font-size:78px;color:var(--purple);position:relative;overflow:hidden;}
.card .img .icon-xl{width:64px;height:64px;stroke-width:1.5px;}
/* product image zoom-on-hover — scale only (GPU), clipped by .img overflow:hidden */
.card .img .product-photo{width:100%;height:100%;object-fit:cover;transition:transform var(--dur-6) var(--ease-standard);}
.card:hover .img .product-photo{transform:scale(1.06);}
.card .heart{position:absolute;top:12px;right:12px;width:34px;height:34px;border-radius:999px;background:#fff;display:flex;align-items:center;justify-content:center;font-size:13px;color:var(--gray);}
.card .heart .icon{width:16px;height:16px;}
.card .heart.active{color:var(--pink);background:var(--pink-tint);}
.card .heart.active .icon{fill:var(--pink);}
.card .off{position:absolute;top:14px;left:14px;background:var(--pink);color:#fff;font-size:11px;font-weight:700;padding:4px 10px;border-radius:999px;}
.card .body{padding:16px 18px 20px;text-align:right;}
.card .cat{color:var(--purple);font-size:12px;font-weight:600;}
.card .name{font-size:16px;font-weight:700;margin:5px 0;letter-spacing:-.01em;}
.card .rating{font-size:12px;color:var(--gray);}
.card .pricerow{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;margin-top:12px;gap:8px 10px;}
.card .price{font-weight:700;font-size:15px;white-space:nowrap;flex-shrink:0;}
.card .add{width:32px;height:32px;border-radius:10px;background:var(--pink);color:#fff;border:none;font-size:17px;cursor:pointer;flex-shrink:0;}
.qty-stepper{display:flex;align-items:center;gap:6px;flex-shrink:0;}
.qty-stepper .qty-btn{width:32px;height:32px;border-radius:10px;background:var(--pink);color:#fff;border:none;font-size:15px;cursor:pointer;flex-shrink:0;}
.qty-stepper .qty-btn.qty-dec{background:var(--lav);color:var(--purple);}
.qty-stepper .qty-value{min-width:24px;text-align:center;font-weight:700;font-size:var(--text-base);color:var(--ink);flex-shrink:0;}
.pagination{display:flex;justify-content:center;align-items:center;gap:8px;margin-top:36px;}
.pagination a, .pagination span.ellipsis{width:38px;height:38px;border-radius:12px;display:flex;align-items:center;justify-content:center;font-size:13px;font-weight:600;cursor:pointer;color:var(--gray);background:#fff;box-shadow:0 4px 10px rgba(26,29,38,.05);}
.pagination span.ellipsis{cursor:default;box-shadow:none;background:none;}
.pagination a.active{background:var(--grad);color:#fff;}
.pagination a.disabled{opacity:.35;pointer-events:none;}
.badgeNew{background:var(--yellow);color:var(--ink);font-size:11px;font-weight:700;padding:3px 9px;border-radius:999px;} /* not wired up anywhere yet */

.pd-layout{display:flex;gap:48px;}
.pd-gallery{width:520px;flex-shrink:0;}
.pd-main-img{height:440px;border-radius:28px;background:linear-gradient(135deg,#F1EAFA,#FBE9F0);display:flex;align-items:center;justify-content:center;font-size:170px;color:var(--purple);margin-bottom:14px;overflow:hidden;}
.pd-main-img .icon-xl{width:140px;height:140px;stroke-width:1.2px;}
.pd-main-img img{width:100%;height:100%;object-fit:cover;}
.pd-thumbs img{width:100%;height:100%;object-fit:cover;border-radius:12px;}
.pd-thumbs{display:flex;gap:10px;}
.pd-thumbs button{width:78px;height:78px;border-radius:14px;background:var(--lav);display:flex;align-items:center;justify-content:center;font-size:30px;color:var(--purple);border:2px solid transparent;cursor:pointer;padding:0;font-family:inherit;}
.pd-thumbs button.active{border-color:var(--pink);}
.pd-info{flex:1;text-align:right;}
.pd-cat{color:var(--purple);font-weight:600;font-size:13px;}
.pd-title{font-size:var(--text-4xl);font-weight:700;margin:8px 0;letter-spacing:-.02em;}
.pd-rating{color:var(--gray);font-size:13px;margin-bottom:18px;}
.pd-price{font-size:30px;font-weight:700;color:var(--pink);margin-bottom:6px;letter-spacing:-.01em;}
.pd-oldprice{color:var(--gray);text-decoration:line-through;font-size:14px;margin-inline-start:10px;}
.pd-desc{color:var(--gray);font-size:14px;line-height:2;margin:18px 0 24px;}
.pd-options{display:flex;gap:10px;margin-bottom:24px;}
.pd-opt{padding:10px 20px;border-radius:12px;border:1.5px solid var(--hairline);font-size:13px;font-weight:600;cursor:pointer;background:#fff;color:inherit;font-family:inherit;}
.pd-opt.active{background:var(--grad);color:#fff;border-color:transparent;}
.pd-buyrow{display:flex;gap:14px;align-items:center;}
.qty{display:flex;align-items:center;border:1.5px solid var(--hairline);border-radius:14px;overflow:hidden;}
.qty button{width:40px;height:48px;border:none;background:#fff;font-size:16px;cursor:pointer;}
.qty span{width:36px;text-align:center;font-weight:700;}
.pd-buy{flex:1;background:var(--grad);color:#fff;border:none;border-radius:14px;padding:14px;font-weight:700;font-size:15px;cursor:pointer;}
.pd-fav{width:52px;height:52px;border-radius:14px;background:var(--lav);border:none;font-size:19px;color:var(--gray);display:flex;align-items:center;justify-content:center;cursor:pointer;}
.pd-fav.active{color:var(--pink);background:var(--pink-tint);}
.pd-fav.active .icon{fill:var(--pink);}
.pd-trust{display:flex;gap:22px;margin-top:28px;border-top:1px solid var(--hairline);padding-top:22px;}
.pd-trust div{font-size:12px;color:var(--gray);text-align:center;flex:1;}
.pd-trust div b{display:block;color:var(--ink);font-size:13px;margin-bottom:4px;}
.pd-tabs{display:flex;gap:30px;margin-top:56px;border-bottom:1.5px solid var(--hairline);padding-bottom:14px;font-weight:600;font-size:14px;color:var(--gray);}
.pd-tabs .active{color:var(--pink);}
.pd-reviews{margin-top:24px;color:var(--gray);font-size:14px;line-height:2;max-width:800px;}
.review-item{border-bottom:1px solid var(--hairline);padding:14px 0;max-width:800px;}
.review-item:last-child{border-bottom:none;}
.review-head{display:flex;justify-content:space-between;font-size:13.5px;margin-bottom:6px;}
.review-head span{color:var(--gray);}
.review-item p{margin:0;color:var(--gray);font-size:13.5px;line-height:1.9;}

.contact-hero{background:var(--grad);border-radius:28px;padding:54px 64px;color:#fff;text-align:center;margin-bottom:36px;}
.contact-hero h1{font-size:var(--text-4xl);margin:0 0 8px;letter-spacing:-.02em;}
.contact-hero p{opacity:.9;font-size:14px;margin:0;}
.contact-layout{display:flex;gap:32px;}
.contact-cards{width:320px;flex-shrink:0;display:flex;flex-direction:column;gap:16px;}
.ccard{background:#fff;border:1px solid var(--hairline);border-radius:20px;padding:22px;box-shadow:0 2px 8px rgba(26,29,38,.04);display:flex;gap:14px;align-items:center;text-align:right;}
.ccard .ic{width:50px;height:50px;border-radius:14px;background:var(--lav);display:flex;align-items:center;justify-content:center;font-size:20px;flex-shrink:0;}
.ccard b{display:block;font-size:14px;margin-bottom:3px;}
.ccard span{color:var(--gray);font-size:12.5px;}
.contact-form{flex:1;background:#fff;border:1px solid var(--hairline);border-radius:24px;padding:36px;box-shadow:0 2px 8px rgba(26,29,38,.04);}
.contact-form h3{margin:0 0 22px;font-size:18px;}
.form-row{display:flex;gap:16px;margin-bottom:16px;}
.field{flex:1;text-align:right;}
.field label{display:block;font-size:12.5px;color:var(--gray);margin-bottom:6px;}
.field input, .field textarea{width:100%;border:1.5px solid var(--hairline);border-radius:12px;padding:12px 14px;font-family:inherit;font-size:13.5px;resize:none;}
.field input:focus, .field textarea:focus{outline:2px solid var(--purple);border-color:transparent;}
.send-btn{background:var(--grad);color:#fff;border:none;border-radius:14px;padding:14px 34px;font-weight:700;font-size:14px;cursor:pointer;}
.map-box{height:230px;border-radius:20px;background:repeating-linear-gradient(45deg,#F1EAFA,#F1EAFA 10px,#FBE9F0 10px,#FBE9F0 20px);margin-top:16px;display:flex;align-items:center;justify-content:center;color:var(--purple);font-weight:700;}

/* login + register share this */
.auth-box{background:#fff;border:1px solid var(--hairline);border-radius:24px;padding:40px;max-width:440px;margin:40px auto;box-shadow:0 4px 12px rgba(26,29,38,.06);}
.auth-box h1{text-align:center;font-size:22px;margin-bottom:6px;}
.auth-box .sub{text-align:center;color:var(--gray);font-size:13px;margin-bottom:26px;}
.auth-box .field{margin-bottom:16px;text-align:right;}
.auth-box .field label{display:block;font-size:12.5px;color:var(--gray);margin-bottom:6px;}
.auth-box .field input{width:100%;border:1.5px solid var(--hairline);border-radius:10px;padding:11px 14px;font-family:inherit;font-size:14px;}
.auth-box .error{background:var(--danger-bg);color:var(--danger);border-radius:10px;padding:10px 14px;font-size:13px;margin-bottom:16px;}
.auth-box .switch{text-align:center;font-size:13px;color:var(--gray);margin-top:16px;}

.admin-shell{display:flex;background:#1A1326;min-height:640px;border-radius:24px;overflow:hidden;}
.admin-side{width:250px;background:#14101F;padding:28px 20px;flex-shrink:0;}
.admin-side .logo{color:#fff;margin-bottom:var(--space-7);font-size:var(--text-xl);}
.admin-side .mark{width:36px;height:36px;font-size:15px;}
.admin-nav a{display:flex;align-items:center;gap:12px;color:#AAB1C0;padding:12px 14px;border-radius:12px;font-size:14px;font-weight:600;margin-bottom:6px;}
.admin-nav a.active{background:var(--grad);color:#fff;}
.admin-nav-disabled{display:flex;align-items:center;gap:12px;color:#5B6272;padding:12px 14px;border-radius:12px;font-size:14px;font-weight:600;margin-bottom:6px;cursor:not-allowed;user-select:none;}

/* pform = product-style crud form, reused on products/categories/sections/etc */
.pform{background:#fff;border:1px solid var(--hairline);border-radius:18px;padding:22px;box-shadow:0 2px 8px rgba(26,29,38,.04);margin-bottom:22px;}
.pform .grid{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;}
.pform label{display:block;font-size:12.5px;color:var(--gray);margin-bottom:6px;}
.pform input,.pform textarea,.pform select{width:100%;border:1.5px solid var(--hairline);border-radius:10px;padding:10px 12px;font-family:inherit;font-size:13px;}
.pform .full{grid-column:1/-1;}
.actions-cell{display:flex;align-items:center;gap:6px;flex-wrap:nowrap;white-space:nowrap;width:1%;}
.actions-cell form{display:inline-flex;margin:0;}
.btn-sm{padding:7px 12px;border-radius:8px;font-size:12px;font-weight:600;border:none;cursor:pointer;white-space:nowrap;line-height:1.3;flex-shrink:0;}
.btn-edit{background:rgba(26,29,38,.07);color:var(--ink);}
.btn-del{background:var(--danger-bg);color:var(--danger);}
.btn-star{background:var(--lav);color:var(--purple);}
.btnSecondary{background:#eee;color:#333;} /* leftover from before btn-edit existed, remove if unused */
.bulk-bar{display:none;align-items:center;gap:var(--space-4);background:var(--lav);border-radius:14px;padding:var(--space-3) var(--space-4);margin-bottom:var(--space-4);}
.bulk-bar.visible{display:flex;}
.bulk-bar span{font-size:var(--text-sm);color:var(--purple);font-weight:600;}
.admin-main{flex:1;padding:28px 32px;background:var(--cream);border-top-right-radius:24px;border-bottom-right-radius:24px;}
.admin-topbar{display:flex;justify-content:space-between;align-items:center;margin-bottom:24px;}
.admin-topbar h2{font-size:var(--text-xl);margin:0;}
.admin-search{border:1.5px solid var(--hairline);border-radius:12px;padding:10px 16px;font-family:inherit;font-size:13px;width:260px;}
.kpi-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:18px;margin-bottom:26px;}
.kpi{background:#fff;border:1px solid var(--hairline);border-radius:18px;padding:20px;box-shadow:0 2px 8px rgba(26,29,38,.04);text-align:right;}
.kpi .ic{width:40px;height:40px;border-radius:12px;display:flex;align-items:center;justify-content:center;font-size:17px;margin-bottom:14px;}
.kpi .val{font-size:22px;font-weight:700;}
.kpi .lbl{color:var(--gray);font-size:12.5px;margin-top:2px;}
.kpi .delta{font-size:11.5px;font-weight:700;margin-top:8px;}
.delta.up{color:var(--success);} .delta.down{color:var(--danger);}
.admin-table{background:#fff;border:1px solid var(--hairline);border-radius:18px;padding:22px;box-shadow:0 2px 8px rgba(26,29,38,.04);overflow-x:auto;}
.admin-table h3{margin:0 0 16px;font-size:15px;}
table{width:100%;border-collapse:collapse;font-size:13px;text-align:right;}
th{color:var(--gray);font-weight:600;padding:10px 8px;border-bottom:1.5px solid var(--hairline);font-size:12.5px;white-space:nowrap;}
td{padding:12px 8px;border-bottom:1px solid var(--hairline);vertical-align:middle;}
/* price was wrapping mid-number on narrow admin tables */
.price-cell{
  white-space:nowrap;
}
.status{padding:5px 12px;border-radius:999px;font-size:11.5px;font-weight:700;}
.status.done{background:var(--success-bg);color:var(--success);}
.status.pending{background:var(--warning-bg);color:var(--warning);}
.status.cancel{background:var(--danger-bg);color:var(--danger);}
.row-prod{display:flex;align-items:center;gap:10px;}
.row-prod .thumb{width:34px;height:34px;border-radius:9px;background:var(--lav);display:flex;align-items:center;justify-content:center;font-size:15px;color:var(--purple);}

/* ===================== هیرو — قلب تغییرات مینیمال‌شیک ===================== */
/* فضای بازتر، تایپوگرافی قوی‌تر، تک‌رنگ عمیق به‌جای گرادیان شلوغ */
.hero{display:flex;gap:64px;align-items:center;padding:56px 64px 72px;max-width:1440px;margin:0 auto;}
.hero-visual{width:520px;height:440px;border-radius:40px;background:linear-gradient(135deg,var(--purple),#3D2A78);flex-shrink:0;display:flex;align-items:center;justify-content:center;font-size:170px;position:relative;overflow:hidden;z-index:1;box-shadow:0 24px 60px rgba(91,63,168,.25);}
.hero-text{position:relative;z-index:1;}
/* object-position nudged off centre so the framing favours the top-left: on both slides the
   instruments sit high and left and the floor/table fills the bottom-right, so this shows
   more subject and less surface, and keeps the very corner off screen. There is not much
   slack to spend — the tile is 520x440 against 1200x896 art, so cover already trims only
   ~12% of the width — but the corners were checked and carry no watermark, so a real crop
   would have thrown away artwork to solve a problem the images do not have. */
.hero-visual.has-image img{width:100%;height:100%;object-fit:cover;object-position:42% 38%;}
/* display:contents so the <picture> adds no box: .hero-visual is a flex container and the
   img's width/height:100% must keep resolving against it, not against a wrapper. */
.hero-visual picture{display:contents;}
.hero-visual .dot{position:absolute;bottom:34px;right:34px;width:26px;height:26px;border-radius:999px;background:var(--yellow);}
.hero-visual .ring{position:absolute;top:-14px;left:24px;width:46px;height:46px;border-radius:999px;border:6px solid #fff;}
.float-card{position:absolute;background:#fff;border:1px solid var(--hairline);border-radius:18px;padding:12px 16px;font-size:13px;font-weight:600;box-shadow:0 14px 30px rgba(26,29,38,.12);}
.fc1{top:60px;right:-30px;} .fc2{bottom:-20px;left:-10px;}
.hero-text{flex:1;text-align:right;}
.chip{display:inline-block;background:var(--lav);color:var(--purple);font-size:13px;font-weight:600;padding:9px 18px;border-radius:999px;margin-bottom:24px;letter-spacing:.01em;}
.hero-text h1, .hero-text h2{font-size:var(--text-5xl);font-weight:700;line-height:1.25;margin:0 0 22px;letter-spacing:-.03em;color:var(--ink);}
.hero-text h1 span, .hero-text h2 span{color:var(--pink);}
.hero-text p{color:var(--ink-soft);font-size:17px;line-height:1.9;max-width:520px;margin:0 0 32px;}
.hero-btns{display:flex;gap:16px;margin-bottom:40px;}
.btn-ghost{background:#fff;border:1.5px solid var(--hairline);border-radius:14px;padding:15px 28px;font-weight:600;font-size:15px;cursor:pointer;font-family:inherit;}
.btn-primary{background:linear-gradient(135deg,rgba(var(--purple-rgb),.92),rgba(var(--pink-rgb),.92));background-size:160% 160%;backdrop-filter:blur(10px) saturate(160%);-webkit-backdrop-filter:blur(10px) saturate(160%);color:#fff;border:var(--glass-border);border-radius:14px;padding:15px 30px;font-weight:600;font-size:15px;cursor:pointer;font-family:inherit;box-shadow:0 14px 26px rgba(var(--purple-rgb),.25), inset 0 1px 0 rgba(255,255,255,.25);animation:gradientDrift 6s ease infinite;}

@keyframes gradientDrift{
  0%,100%{background-position:0% 50%;}
  50%{background-position:100% 50%;}
}
@media (prefers-reduced-motion: reduce){
  .btn-primary{ animation:none; }
}

/* آمار هیرو — شمارنده انیمیشنی. tabular-nums جلوی تکان خوردن layout حین شمارش رو می‌گیره */
.hero-stats{display:flex;gap:48px;}
.hero-stats div{text-align:right;}
.hero-stats b{display:block;font-size:24px;font-weight:700;color:var(--ink);letter-spacing:-.02em;font-variant-numeric:tabular-nums;}
.hero-stats span{font-size:12.5px;color:var(--gray);}

/* hero slider — a translateX-based row (transform only, GPU-friendly) rather than a
   pure opacity crossfade: with per-slide content of slightly different lengths, an
   absolutely-positioned crossfade needs a fixed container height (fragile, clips or
   leaves gaps); a flex row where each slide is 100% width and the row translates
   sizes naturally to whichever slide is active with no such fragility. */
/* overflow:clip (not hidden) so the off-screen inactive slide in this RTL row can
   never leak into the document's scrollWidth — with hidden, a composited/animating
   descendant momentarily un-clips the left-side RTL overflow and adds a transient
   horizontal scrollbar at wide viewports during the intro animation. clip contains it. */
.hero-slider{position:relative;max-width:1440px;margin:0 auto;overflow:hidden;overflow:clip;}
/* NO overflow on the track. An earlier pass put `overflow:clip` here to stop a
   transient horizontal-scrollbar glitch, on the reasoning that "the row's box already
   encloses every slide, so it hides nothing". That reasoning is wrong in RTL: the flex
   row's own box is only one slide wide (100%), and slides 2..n overflow it to the LEFT.
   Clipping to that box therefore painted away every slide except the first — which is
   exactly why slide 2 rendered blank, and why dragging/arrowing "did nothing visible".
   `.hero-slider` above is the correct clipping boundary and already handles the
   scrollWidth leak on its own (regression-checked at 7 widths in hero-test.js). */
.hero-slides{display:flex;transition:transform .5s ease;}
.hero-slide{flex:0 0 100%;min-width:0;}
.hero-slide .hero{margin:0;max-width:none;}
.hero-arrow{position:absolute;top:50%;transform:translateY(-50%);width:44px;height:44px;border-radius:999px;background:rgba(255,255,255,.9);border:1px solid var(--hairline);box-shadow:0 8px 20px rgba(26,29,38,.14);display:flex;align-items:center;justify-content:center;color:var(--purple);cursor:pointer;opacity:0;transition:opacity .18s ease, background-color .18s ease, transform .18s ease;z-index:2;}
.hero-slider:hover .hero-arrow, .hero-arrow:focus-visible{opacity:1;}
.hero-arrow:hover{background:#fff;transform:translateY(-50%) scale(1.06);}
.hero-arrow-prev{right:20px;}
.hero-arrow-next{left:20px;}
.hero-dots{position:absolute;bottom:16px;left:50%;transform:translateX(-50%);display:flex;gap:8px;z-index:2;}
.hero-dot{width:8px;height:8px;border-radius:999px;background:rgba(26,29,38,.18);border:none;cursor:pointer;padding:0;transition:background-color .18s ease, width .18s ease;}
.hero-dot.active{background:var(--purple);width:22px;}
@media (max-width:860px){
  .hero-arrow{display:none;}
}
@media (prefers-reduced-motion: reduce){
  .hero-slides{transition:none;}
  .hero-arrow, .hero-dot{transition:none;}
}
.section{padding:10px 64px 48px;max-width:1440px;margin:0 auto;}
.section-title{display:flex;justify-content:space-between;align-items:center;margin-bottom:22px;}
.section-title h2{font-size:var(--text-2xl);margin:0;letter-spacing:-.02em;}
.section-title a{color:var(--purple);font-size:14px;font-weight:600;}
.chips-row{display:flex;gap:12px;flex-wrap:wrap;}
.pill{background:#fff;border:1.2px solid var(--hairline);border-radius:999px;padding:10px 20px;font-size:14px;font-weight:500;cursor:pointer;}
.pill.active{background:var(--grad);color:#fff;border-color:transparent;box-shadow:0 10px 20px rgba(var(--purple-rgb),.28);}

/* feature-strip — خلوت‌تر و ظریف‌تر، hairline به‌جای سایه سنگین */
.feature-strip{display:flex;justify-content:space-between;background:#fff;border:1px solid var(--hairline);border-radius:28px;padding:38px 56px;box-shadow:0 2px 12px rgba(26,29,38,.04);max-width:1440px;margin:0 auto 56px;}
.feature{display:flex;align-items:center;gap:14px;text-align:right;}
.feature .fic{width:56px;height:56px;border-radius:16px;display:flex;align-items:center;justify-content:center;font-size:22px;color:var(--purple);}
.feature .fic .icon{width:26px;height:26px;}
.feature b{display:block;font-size:15px;} .feature span{font-size:12.5px;color:var(--gray);}
footer.site{background:#14101F;color:#AAB1C0;padding:52px 64px 26px;}
.footer-top{display:flex;justify-content:space-between;max-width:1440px;margin:0 auto 30px;}
.footer-brand{text-align:right;max-width:290px;}
.footer-brand .logo{color:#fff;margin-bottom:14px;}
.footer-brand p{font-size:13px;line-height:1.9;margin:0 0 16px;}
.socials{display:flex;gap:10px;}
.socials a{width:38px;height:38px;border-radius:999px;background:#2A1F3D;display:flex;align-items:center;justify-content:center;color:#fff;font-size:14px;text-decoration:none;}
.footer-links{text-align:right;}
.footer-links b{color:#fff;display:block;margin-bottom:12px;font-size:15px;}
.footer-links div{font-size:13px;margin-bottom:10px;}
.newsletter{text-align:right;}
.newsletter b{color:#fff;font-size:16px;display:block;margin-bottom:6px;}
.newsletter p{font-size:13px;margin:0 0 14px;}
.sub-row{display:flex;gap:10px;}
.sub-row input{background:#2A1F3D;border:none;border-radius:13px;padding:13px 16px;color:#fff;font-family:inherit;font-size:13px;width:220px;}
.sub-row button{background:var(--yellow);border:none;border-radius:13px;padding:13px 24px;font-weight:700;font-family:inherit;font-size:14px;cursor:pointer;}
.footer-bottom{border-top:1px solid #2A1F3D;max-width:1440px;margin:0 auto;padding-top:20px;text-align:center;font-size:12px;color:#8B93A5;}

/* ===== homepage sections: deals, promo carousels/banners, three-column lists,
   magazine, brands, seo text — all DB-driven via homepage_blocks ===== */

/* پیشنهاد شگفت‌انگیز — products + countdown | stacked banners.
   The tab strip used to be a third column here, 160px wide, which inset the products and
   left the countdown and the grid starting on a different edge from every other section.
   It now lives in the section header as a horizontal chip bar — see `.section-title-filters`. */
.deals-layout{display:flex;gap:24px;align-items:flex-start;}
.deals-tabs{
  display:flex; align-items:center; gap:var(--space-2);
  flex:1 1 auto; min-width:0;                 /* shrinkable inside the header row */
  overflow-x:auto; scrollbar-width:none;
  /* a scroll container forces overflow-y to auto as well, which would shave the active
     chip's shadow off. A few px of block padding gives it room to land. */
  padding-block:4px;
}
.deals-tabs::-webkit-scrollbar{ display:none; }
.deals-tab{background:#fff;border:1.2px solid var(--hairline);border-radius:12px;padding:12px 16px;font-family:inherit;font-size:13.5px;font-weight:600;color:var(--ink);text-align:center;white-space:nowrap;flex:0 0 auto;cursor:pointer;}
.deals-tab:hover{border-color:var(--purple);color:var(--purple);}
.deals-tab.active{background:var(--grad);color:#fff;border-color:transparent;box-shadow:0 10px 20px rgba(var(--purple-rgb),.28);}
.deals-products{flex:1;min-width:0;}
.deals-countdown{display:flex;align-items:center;gap:10px;background:#14101F;border-radius:16px;padding:14px 20px;margin-bottom:18px;width:fit-content;}
.cd-unit{display:flex;flex-direction:column;align-items:center;min-width:38px;}
.cd-val{font-size:20px;font-weight:700;color:#fff;font-variant-numeric:tabular-nums;transition:transform .2s ease, opacity .2s ease;}
.cd-val.tick{animation:cdTick .3s ease;}
@keyframes cdTick{0%{opacity:.3;transform:translateY(-4px);}100%{opacity:1;transform:translateY(0);}}
.cd-label{font-size:10.5px;color:#AAB1C0;margin-top:2px;}
.cd-sep{color:#AAB1C0;font-weight:700;}
.deals-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;}
.deal-card{display:block;background:#fff;border:1px solid var(--hairline);border-radius:18px;overflow:hidden;box-shadow:0 2px 8px rgba(26,29,38,.04);transition:.2s;}
.deal-card:hover{transform:translateY(-4px);box-shadow:0 16px 30px rgba(var(--purple-rgb),.16);}
.deal-card-img{height:120px;display:flex;align-items:center;justify-content:center;font-size:42px;color:var(--purple);position:relative;overflow:hidden;}
.deal-card-img .product-photo{width:100%;height:100%;object-fit:cover;}
.deal-card-img .off{position:absolute;top:10px;left:10px;background:var(--pink);color:#fff;font-size:10.5px;font-weight:700;padding:3px 8px;border-radius:999px;}
.deal-card-body{padding:12px 14px 16px;text-align:right;}
.deal-card-name{font-size:13px;font-weight:700;margin-bottom:6px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.deal-card-prices{display:flex;flex-direction:column;gap:2px;}
.deal-old-price{color:var(--gray);text-decoration:line-through;font-size:11.5px;}
.deal-price{color:var(--pink);font-weight:700;font-size:13.5px;}
.deals-banners{display:flex;flex-direction:column;gap:16px;width:200px;flex-shrink:0;}
.deals-banner{display:block;border-radius:16px;overflow:hidden;box-shadow:0 10px 22px rgba(26,29,38,.08);}
.deals-banner img{width:100%;height:150px;object-fit:cover;display:block;}

/* فروش ویژه / بزودی — colored panel carousel with a vertical label */
.colored-carousel{position:relative;background:linear-gradient(135deg,var(--purple),var(--pink));border-radius:28px;padding:28px 72px;display:flex;align-items:center;gap:24px;overflow:hidden;}
.colored-carousel.coming-soon{background:linear-gradient(135deg,#14101F,#4A3670);}
.colored-carousel-label{flex-shrink:0;writing-mode:vertical-rl;transform:rotate(180deg);color:#fff;font-size:22px;font-weight:700;letter-spacing:.02em;}
.colored-carousel-track{flex:1;display:flex;gap:16px;overflow-x:auto;scroll-behavior:smooth;scrollbar-width:none;}
.colored-carousel-track::-webkit-scrollbar{display:none;}
.colored-carousel-card{flex:0 0 160px;background:rgba(255,255,255,.96);border-radius:16px;padding:14px;text-align:center;transition:transform .18s ease;}
.colored-carousel-card:not(.is-soon):hover{transform:translateY(-4px);}
.ccc-img{height:90px;border-radius:12px;background:var(--lav);display:flex;align-items:center;justify-content:center;font-size:34px;color:var(--purple);margin-bottom:10px;overflow:hidden;position:relative;}
.ccc-img .product-photo{width:100%;height:100%;object-fit:cover;}
.soon-badge{position:absolute;bottom:6px;left:50%;transform:translateX(-50%);background:#14101F;color:#fff;font-size:9.5px;font-weight:700;padding:2px 8px;border-radius:999px;}
.ccc-name{font-size:12px;font-weight:600;color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.ccc-price{font-size:12.5px;font-weight:700;color:var(--pink);margin-top:4px;}
.colored-carousel-arrow{position:absolute;top:50%;transform:translateY(-50%);width:36px;height:36px;border-radius:999px;background:rgba(255,255,255,.9);border:none;display:flex;align-items:center;justify-content:center;color:var(--purple);cursor:pointer;opacity:0;transition:opacity .18s ease;z-index:2;}
.colored-carousel:hover .colored-carousel-arrow{opacity:1;}
.ccc-prev{right:20px;} .ccc-next{left:20px;}

/* promo banners */
.promo-trio{display:flex;gap:20px;}
.promo-trio-item{flex:1;border-radius:20px;overflow:hidden;display:block;box-shadow:0 10px 22px rgba(26,29,38,.07);transition:transform .18s ease;}
.promo-trio-item:hover{transform:translateY(-4px);}
.promo-trio-item img{width:100%;height:160px;object-fit:cover;display:block;}
.promo-wide{display:block;border-radius:24px;overflow:hidden;box-shadow:0 12px 26px rgba(26,29,38,.08);}
.promo-wide img{width:100%;height:auto;display:block;}

/* three-column compact product lists */
.three-col-row{display:grid;grid-template-columns:repeat(3,1fr);gap:24px;}
.three-col-item{background:#fff;border:1px solid var(--hairline);border-radius:20px;padding:20px;box-shadow:0 2px 8px rgba(26,29,38,.04);}
.three-col-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:14px;}
.three-col-head h3{font-size:15px;margin:0;}
.three-col-head a{color:var(--purple);font-size:12px;font-weight:600;}
.compact-row{display:flex;align-items:center;gap:10px;padding:9px 0;border-bottom:1px solid var(--hairline);}
.compact-row:last-child{border-bottom:none;}
.compact-row-thumb{width:40px;height:40px;border-radius:10px;flex-shrink:0;display:flex;align-items:center;justify-content:center;font-size:16px;color:var(--purple);overflow:hidden;}
.compact-row-thumb .product-photo{width:100%;height:100%;object-fit:cover;}
.compact-row-info{flex:1;min-width:0;}
.compact-row-name{font-size:12.5px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.compact-row-rating{font-size:11px;color:var(--gray);margin-top:2px;}
.compact-row-price{font-size:12px;font-weight:700;color:var(--pink);flex-shrink:0;white-space:nowrap;}

/* مجله — reuses .blog-card (defined in blog.php's page-local <style>), paged with dots */
.magazine-pager{overflow:hidden;}
.magazine-track{display:flex;transition:transform .4s ease;}
.magazine-page{flex:0 0 100%;min-width:0;display:grid;grid-template-columns:repeat(4,1fr);gap:22px;}
.magazine-dots{display:flex;justify-content:center;gap:8px;margin-top:20px;}
.magazine-dot{width:8px;height:8px;border-radius:999px;background:rgba(26,29,38,.18);border:none;cursor:pointer;padding:0;transition:background-color .18s ease, width .18s ease;}
.magazine-dot.active{background:var(--purple);width:22px;}

/* brand logos — grayscale, color on hover */
.brands-strip{display:flex;flex-wrap:wrap;justify-content:center;align-items:center;gap:36px;background:#fff;border:1px solid var(--hairline);border-radius:24px;padding:32px;box-shadow:0 2px 8px rgba(26,29,38,.04);}
.brand-logo{display:flex;align-items:center;justify-content:center;height:44px;}
.brand-logo img{max-height:100%;max-width:120px;object-fit:contain;filter:grayscale(1) opacity(.55);transition:filter .18s ease;}
.brand-logo:hover img{filter:grayscale(0) opacity(1);}

/* seo text block with "ادامه مطلب" expander */
.seo-text-block{background:#fff;border:1px solid var(--hairline);border-radius:22px;padding:32px 36px;box-shadow:0 2px 8px rgba(26,29,38,.04);text-align:right;}
.seo-text-block h2{font-size:var(--text-xl);margin:0 0 14px;letter-spacing:-.02em;}
.seo-text-content{position:relative;max-height:120px;overflow:hidden;color:var(--gray);font-size:13.5px;line-height:2;transition:max-height .3s ease;}
.seo-text-content p{margin:0 0 14px;}
.seo-text-content::after{content:'';position:absolute;bottom:0;right:0;left:0;height:48px;background:linear-gradient(rgba(255,255,255,0),#fff);transition:opacity .2s ease;}
.seo-text-block.expanded .seo-text-content{max-height:1000px;}
.seo-text-block.expanded .seo-text-content::after{opacity:0;pointer-events:none;}
.seo-text-toggle{display:flex;align-items:center;gap:6px;margin-top:14px;background:none;border:none;color:var(--purple);font-family:inherit;font-weight:700;font-size:13px;cursor:pointer;padding:0;}
.seo-text-toggle:hover{color:var(--pink);}
.seo-text-toggle .chevron{transition:transform .2s ease;}
.seo-text-block.expanded .seo-text-toggle .chevron{transform:rotate(180deg);}

@media (max-width:1100px){
  .deals-grid{grid-template-columns:repeat(2,1fr);}
  /* `1fr` is `minmax(auto,1fr)`, so the column can never shrink below the item's
     min-content. At 375px that min-content was 340px against a 303px container —
     the whole row overflowed 37px to the left and the thumbnails were cut off.
     `minmax(0,1fr)` lets the column take the container width and the name inside
     ellipsise (it already carries min-width:0). */
  .three-col-row{grid-template-columns:minmax(0,1fr);}
  .magazine-page{grid-template-columns:repeat(2,1fr);}
}
@media (max-width:860px){
  .deals-layout{flex-direction:column;}
  /* the chips are a row at every width now, so the old flip to `flex-direction:row` here
     is gone; what is still needed is giving them a line of their own once the header
     stops fitting on one. */
  .deals-banners{flex-direction:row;width:100%;}
  .deals-banner img{height:110px;}
  .promo-trio{flex-direction:column;}
  .colored-carousel{padding:22px 56px;}
  .colored-carousel-label{font-size:16px;}
  .brands-strip{gap:24px;padding:22px;}
}
@media (max-width:520px){
  .deals-grid{grid-template-columns:1fr;}
  .magazine-page{grid-template-columns:1fr;}
  .colored-carousel-card{flex:0 0 130px;}
}
@media (prefers-reduced-motion: reduce){
  .colored-carousel-arrow, .deal-card, .promo-trio-item, .brand-logo img{transition:none;}
  .magazine-track{transition:none;}
  .cd-val.tick{animation:none;}
  .seo-text-content{transition:none;}
}

/* contact + cart fab, env() keeps it clear of the iphone home indicator */
.fab{
  position:fixed; bottom:calc(var(--space-6) + env(safe-area-inset-bottom, 0px)); z-index:80;
  width:56px; height:56px; border-radius:999px;
  display:flex; align-items:center; justify-content:center; font-size:22px;
  color:#fff; text-decoration:none;
  background:linear-gradient(135deg,rgba(var(--purple-rgb),.92),rgba(var(--pink-rgb),.92));
  backdrop-filter:blur(10px) saturate(160%); -webkit-backdrop-filter:blur(10px) saturate(160%);
  border:var(--glass-border);
  box-shadow:0 8px 24px rgba(var(--purple-rgb),.3), inset 0 1px 0 rgba(255,255,255,.25);
  transition:transform .18s ease, box-shadow .18s ease, opacity .18s ease, visibility .18s ease;
}
.fab:hover{ transform:translateY(-3px) scale(1.05); box-shadow:0 14px 30px rgba(var(--purple-rgb),.38), inset 0 1px 0 rgba(255,255,255,.3); }
.fab:active{ transform:scale(.96); }
.fab-contact{ right:calc(var(--space-6) + env(safe-area-inset-right, 0px)); }
.fab-cart{ left:calc(var(--space-6) + env(safe-area-inset-left, 0px)); }
.fab .badge{ border:2px solid #fff; }
/* Hidden while the mobile nav overlay is open, so nothing competes with it for taps. */
body.mobile-nav-open .fab{ opacity:0; visibility:hidden; pointer-events:none; }
@media (max-width:520px){
  .fab{ width:48px; height:48px; font-size:19px; bottom:calc(var(--space-4) + env(safe-area-inset-bottom, 0px)); }
  .fab-contact{ right:calc(var(--space-4) + env(safe-area-inset-right, 0px)); }
  .fab-cart{ left:calc(var(--space-4) + env(safe-area-inset-left, 0px)); }
}

.mobile-nav-overlay{ display:none; }
@media (max-width:860px){
  .mobile-nav-overlay{
    display:block;
    position:fixed; inset:0; z-index:300;
    background:var(--cream);
    transform:translateX(100%);
    transition:transform .25s ease;
    overflow-y:auto;
    pointer-events:none;
    padding-top:env(safe-area-inset-top, 0px);
    padding-bottom:env(safe-area-inset-bottom, 0px);
  }
  .mobile-nav-overlay.open{ transform:translateX(0); pointer-events:auto; }
  .mobile-nav-header{display:flex; align-items:center; justify-content:space-between; padding:var(--space-5) var(--space-5) var(--space-4); border-bottom:1px solid var(--hairline);}
  .mobile-nav-header .logo{ font-size:var(--text-xl); }
  .mobile-nav-close{width:40px; height:40px; border-radius:999px; background:var(--lav); border:none; display:flex; align-items:center; justify-content:center; color:var(--purple); cursor:pointer;}
  .mobile-nav-list{padding:var(--space-2) var(--space-4) var(--space-8);}
  .mobile-nav-list > a{display:block; padding:var(--space-4) var(--space-3); font-size:var(--text-lg); font-weight:600; color:var(--ink); border-bottom:1px solid var(--hairline);}
  .mobile-nav-list > a.active{ color:var(--pink); }
  .mobile-nav-group{ border-bottom:1px solid var(--hairline); }
  .mobile-nav-group-toggle{width:100%; display:flex; align-items:center; justify-content:space-between; padding:var(--space-4) var(--space-3); background:none; border:none; font-family:inherit; font-size:var(--text-lg); font-weight:600; color:var(--ink); cursor:pointer;}
  .mobile-nav-group-toggle .chevron{ color:var(--gray); transition:transform .2s ease; flex-shrink:0; }
  .mobile-nav-group.expanded .mobile-nav-group-toggle .chevron{ transform:rotate(180deg); }
  .mobile-nav-submenu{ max-height:0; overflow:hidden; transition:max-height .25s ease; padding:0 var(--space-3); }
  .mobile-nav-group.expanded .mobile-nav-submenu{ max-height:800px; }
  .mobile-nav-family-title{ font-size:var(--text-xs); font-weight:700; color:var(--purple); letter-spacing:.02em; margin:var(--space-4) 0 var(--space-2); }
  .mobile-nav-submenu a{ display:block; padding:var(--space-2) var(--space-2); font-size:var(--text-base); color:var(--gray); }
  .mobile-nav-viewall{ display:block; margin:var(--space-2) 0 var(--space-4); font-weight:700; color:var(--purple) !important; }
  /* account / cart / wishlist / phone — the header actions that don't fit a phone
     header row, given a proper home at the foot of the menu */
  .mobile-nav-actions{
    display:grid; gap:var(--space-1);
    margin-top:var(--space-5); padding-top:var(--space-4);
    border-top:1.5px solid var(--lav);
  }
  .mobile-nav-actions a, .mobile-nav-actions button{
    display:flex; align-items:center; gap:var(--space-3);
    padding:var(--space-3) var(--space-2); border-radius:12px;
    font-size:var(--text-base); font-weight:600; color:var(--ink);
    background:none; border:none; font-family:inherit; text-align:start;
    width:100%; cursor:pointer;
    transition:background-color var(--dur-2) var(--ease-standard), color var(--dur-2) var(--ease-standard);
  }
  .mobile-nav-actions .icon{ color:var(--purple); }
  .mobile-nav-actions a:hover, .mobile-nav-actions button:hover,
  .mobile-nav-actions a:focus-visible, .mobile-nav-actions button:focus-visible{
    background:var(--lav); color:var(--purple);
  }
  .mobile-nav-actions .js-wishlist-toggle.active{ color:var(--pink); }
  .mobile-nav-actions .js-wishlist-toggle.active .icon{ color:var(--pink); fill:var(--pink); }
}
@media (prefers-reduced-motion: reduce){
  .mobile-nav-overlay, .mobile-nav-submenu, .mobile-nav-group-toggle .chevron{ transition:none; }
}

@media (max-width:1100px){
  header.site{padding:0 28px;}
  .header-topline{padding:8px 28px;}
  .search-box-wrap{max-width:380px;}
  main{padding:28px 28px 60px;}
  .hero{padding:36px 28px 48px;gap:36px;}
  .section, .feature-strip, .footer-top{padding-left:28px;padding-right:28px;}
  .section{padding-top:10px;padding-bottom:40px;}
  .footer-top{gap:24px;flex-wrap:wrap;}
  .product-grid{grid-template-columns:repeat(3,1fr);}
  .pd-layout{gap:30px;}
  .pd-gallery{width:400px;}
}

@media (max-width:860px){
  .menu-toggle{display:flex;order:-1;}
  header.site{position:relative;}

  /* ---- mobile header, rebuilt as an explicit 3-column grid ----
     It used to be `flex-wrap:wrap`, which let the browser decide the rows: the
     logo+hamburger pair wrapped to row 1 and hugged the inline-start edge, the
     actions cluster dropped to row 2 and hugged the inline-END edge (it carries
     `margin-right:auto` for the desktop layout), and the search took row 3. The
     result zig-zagged — ~170px of dead space on one side of row 1 and ~118px on
     the other side of row 2, with nothing sharing an edge.
     A grid pins each item to a named column instead of leaving it to wrapping:
       col 1 (inline-start / right in RTL): hamburger
       col 2 (1fr):                          logo, pushed against the hamburger
       col 3 (inline-end / left in RTL):     actions
       row 2:                                search, spanning all three
     Everything now shares the same two outer edges. */
  .header-main{
    display:grid;
    grid-template-columns:auto 1fr auto;
    align-items:center;
    column-gap:var(--space-3);
    row-gap:var(--space-3);
    padding:var(--space-3) 0;
  }
  .menu-toggle{grid-column:1;grid-row:1;order:0;margin:0;}
  .logo{grid-column:2;grid-row:1;justify-self:start;margin:0;}
  .actions{
    grid-column:3;grid-row:1;justify-self:end;
    margin:0;               /* kill the desktop `margin-right:auto` */
    gap:var(--space-2);
  }
  .search-box-wrap{grid-column:1 / -1;grid-row:2;order:0;max-width:none;width:100%;}

  .header-topline{padding:6px 20px;}
  .phone-btn span{display:none;}
  /* touch has no :hover, so the dropdown panels are unreachable here — tapping an
     icon just goes straight to that group's shop page instead */
  .icon-nav-panel{display:none;}
  /* ---- category strip: a real horizontal scroller ----
     `justify-content:space-between` + `flex:1 1 0` is a desktop distribution and
     fights an overflowing scroller, and centring the column meant a 2-line label
     ("گیتار و سازهای زهی") pushed its icon up out of line with its 1-line
     neighbours. Fixed items, top-aligned icons, snap points, and end padding so
     the last chip can scroll clear of the edge. */
  .icon-nav{
    padding:0 0 10px;
    overflow-x:auto;
    justify-content:flex-start;
    gap:var(--space-1);
    scroll-snap-type:x proximity;
    scrollbar-width:none;                  /* the strip is short; a bar just adds noise */
    scroll-padding-inline:var(--space-2);
  }
  .icon-nav::-webkit-scrollbar{display:none;}
  .icon-nav-item{flex:0 0 auto;min-width:0;width:88px;scroll-snap-align:start;}
  .icon-nav-link{
    height:auto;min-height:74px;
    justify-content:flex-start;            /* icons align across items regardless of label lines */
    padding:var(--space-2) var(--space-1);
    gap:var(--space-1);
  }
  .icon-nav-link span{display:block;}
  .hero{flex-direction:column;padding:32px 24px 44px;}
  .hero-visual{width:100%;max-width:440px;height:320px;font-size:120px;}
  .hero-text{text-align:center;}
  .hero-text p{margin-inline:auto;}
  .hero-text h1, .hero-text h2{font-size:42px;}
  .hero-btns{justify-content:center;flex-wrap:wrap;}
  .hero-stats{justify-content:center;}
  .hero-stats div{text-align:center;}
  .chip{display:block;width:fit-content;margin-inline:auto;}
  .float-card{display:none;}
  /* wrapping flipped the row's arrangement: a title that fit sat on one side, a
     title that wrapped sat on the other, so no two sections lined up. The row is
     only ever two items — keep them on one line and let the heading wrap inside
     its own box instead. */
  .section-title{flex-wrap:nowrap;gap:var(--space-3);align-items:baseline;}
  .product-grid{grid-template-columns:repeat(2,1fr);gap:16px;}
  .feature-strip{flex-direction:column;gap:20px;padding:26px 24px;}
  .listing-layout{flex-direction:column;}
  .filters{width:100%;}
  .pd-layout{flex-direction:column;}
  .pd-gallery{width:100%;}
  .pd-main-img{height:300px;font-size:120px;}
  .footer-top{flex-direction:column;align-items:flex-start;gap:28px;}
  .sub-row input{width:160px;}
  .admin-shell{flex-direction:column;}
  .admin-side{width:100%;padding:20px;}
  .admin-nav{display:flex;overflow-x:auto;gap:8px;}
  .admin-nav a{white-space:nowrap;margin-bottom:0;}
  .admin-main{border-radius:0;padding:22px;}
  .kpi-row{grid-template-columns:repeat(2,1fr);}
  .contact-layout{flex-direction:column;}
  .contact-cards{width:100%;}
  .form-row{flex-direction:column;gap:14px;}
  .admin-table{overflow-x:auto;}
  table{min-width:560px;}
}

@media (max-width:560px){
  /* At 375px the row 1 budget is 339px. Hamburger 40 + logo 113 + gaps leaves ~150
     for the actions, and the full cluster (login 122 + cart 40 + wishlist 40 + gaps)
     needs 238 — so it overflowed and shoved the logo off its column. The wishlist
     toggle moves into the mobile menu (see .mobile-nav-actions) and the login button
     goes compact; cart stays, because a cart badge has to be visible at a glance. */
  .actions .js-wishlist-toggle{display:none;}
  /* Same budget, same answer, measured the same way: at 375px row 1 has 39px of
     headroom and a visible .icon-btn plus its gap costs 52px, so the home button
     overflows the row exactly as the wishlist did. It moves into
     .mobile-nav-actions alongside it — and the logo is still a link to index.php
     at every width, so the way home is never actually absent. */
  .actions .home-btn{display:none;}
  .actions .btn-grad{padding:10px 14px;font-size:var(--text-sm);}
  .logo{font-size:var(--text-xl);}
  /* the topline duplicates links that are already at the top of the mobile menu, and
     on a phone it costs a whole 47px row that scrolls sideways to read. Nothing is
     lost by dropping it here — includes/header.php renders $topLinks in the overlay. */
  .header-topline{display:none;}
}

@media (max-width:520px){
  header.site{padding:0 18px;}
  .header-main{padding:var(--space-3) 0;column-gap:var(--space-2);}
  .header-topline{padding:6px 14px;}
  .phone-btn{display:none;}
  .icon-nav-item{width:82px;}
  .icon-nav-link{padding:var(--space-2) 2px;}
  .icon-nav-link span{font-size:11.5px;}
  main{padding:20px 18px 50px;}
  h1.pagetitle{font-size:22px;}
  .hero{padding:26px 18px 36px;}
  .hero-text h1, .hero-text h2{font-size:32px;}
  .hero-visual{height:240px;font-size:90px;border-radius:32px;}
  .section{padding:8px 18px 32px;}
  .section-title h2{font-size:20px;}
  .product-grid{grid-template-columns:1fr 1fr;gap:12px;}
  .card .img{height:130px;font-size:52px;}
  .card .name{font-size:14px;}
  .kpi-row{grid-template-columns:1fr;}
  .admin-search{width:150px;}
  .pd-title{font-size:24px;}
  .pd-price{font-size:24px;}
  /* the buy button drops to its own full-width line; without this the quantity
     stepper and the favourite button left on the line above packed against the
     inline start and stranded the left third of the row empty. */
  .pd-buyrow{flex-wrap:wrap;}
  .pd-buy{flex:1 1 100%;order:1;}
  .pd-buyrow .qty{flex:1 1 auto;justify-content:space-between;}
  .pd-buyrow .qty span{flex:1 1 auto;width:auto;}
  .pd-fav{flex:0 0 auto;}
  .contact-hero{padding:36px 24px;}
  .contact-form{padding:22px;}
  .logo{font-size:19px;}
}
/* =====================================================================
   PHASE F — motion pass + design-system state/feedback layer
   (scroll-reveal, intro animations, toasts, inline validation,
    complete disabled/loading states, cart AJAX loaders)
   All motion is transform/opacity only, 150-400ms, Material-style easing,
   and every block below has a prefers-reduced-motion escape hatch.
   ===================================================================== */

/* ---- scroll reveal --------------------------------------------------
   The hidden initial state is gated on html.reveal-ready, which an inline
   <head> script sets synchronously before first paint. If JS is disabled
   the class is never added and content stays fully visible (no content is
   ever trapped behind opacity:0). The IntersectionObserver adds .is-visible
   once, then unobserves (one-shot). */
html.reveal-ready .reveal{
  opacity:0;
  transform:translateY(24px);
  transition:opacity var(--dur-6) var(--ease-decelerate), transform var(--dur-6) var(--ease-decelerate);
}
html.reveal-ready .reveal.is-visible{
  opacity:1;
  transform:none;
}

/* ---- header + hero load-in on first paint ---------------------------
   header drops in, hero content rises in with a small stagger. Gated on
   reveal-ready so it only runs when JS/motion are in play. */
@keyframes introDown{from{opacity:0;transform:translateY(-14px);}to{opacity:1;transform:none;}}
@keyframes introUp{from{opacity:0;transform:translateY(20px);}to{opacity:1;transform:none;}}
@keyframes introPop{from{opacity:0;transform:scale(.96);}to{opacity:1;transform:none;}}
html.reveal-ready header.site{animation:introDown var(--dur-6) var(--ease-decelerate) both;animation-delay:40ms;}
/* `:not(.intro-done)` is what makes this a LOAD-IN rather than a slide transition, and it
   is not cosmetic. These rules key off `.hero-slide.active`, and paging the slider is
   exactly what moves that class — so every arrow click, dot, swipe and autoplay tick used
   to re-arm them on the incoming slide. Both keyframes start at `opacity:0`, and with
   `both` + delays up to 340ms the new slide was held INVISIBLE for a third of a second and
   then popped and re-staggered back in, on top of a track that was already translating.
   That is what read as the hero reloading instead of sliding. JS stamps `.intro-done` on
   the slider before the first paging of any kind, so the intro plays once on first paint
   and never again. See the matching guard in the reduced-motion block far below — it has
   to carry the same selector or it stops out-specifying these. */
html.reveal-ready .hero-slider:not(.intro-done) .hero-slide.active .hero-visual{animation:introPop var(--dur-6) var(--ease-emphasized) both;animation-delay:120ms;}
html.reveal-ready .hero-slider:not(.intro-done) .hero-slide.active .hero-text > *{animation:introUp var(--dur-6) var(--ease-decelerate) both;}
html.reveal-ready .hero-slider:not(.intro-done) .hero-slide.active .hero-text > .chip{animation-delay:140ms;}
html.reveal-ready .hero-slider:not(.intro-done) .hero-slide.active .hero-text > h1,
html.reveal-ready .hero-slider:not(.intro-done) .hero-slide.active .hero-text > h2{animation-delay:190ms;}
html.reveal-ready .hero-slider:not(.intro-done) .hero-slide.active .hero-text > p{animation-delay:240ms;}
html.reveal-ready .hero-slider:not(.intro-done) .hero-slide.active .hero-text > .hero-btns{animation-delay:290ms;}
html.reveal-ready .hero-slider:not(.intro-done) .hero-slide.active .hero-text > .hero-stats{animation-delay:340ms;}

/* while dragging a swipeable track, kill the snap transition so it tracks
   the finger 1:1; JS toggles .is-dragging */
.hero-slides.is-dragging, .magazine-track.is-dragging{transition:none;}
.hero-slider, .magazine-pager{touch-action:pan-y;}

/* ---- draggable affordance: the hero says "grab me" before you try ----
   grab on hover, grabbing for as long as a drag is live (JS toggles .is-grabbing).
   the controls keep their own pointer cursor — .hero-arrow/.hero-dot set it, and
   they're more specific than the bare .hero-slider rule so they win by default.
   .is-draggable is stamped by JS only once the drag is actually wired up (script ran,
   more than one slide), so the cursor never promises a drag that can't happen. */
.hero-slider.is-draggable{cursor:grab;}
.hero-slider.is-grabbing{cursor:grabbing;}
/* mid-drag the grabbing cursor has to beat every descendant, including the links
   and buttons inside the slide — otherwise the cursor flickers back to a pointer
   as the drag passes over the CTA. only ever active while a drag is in flight. */
.hero-slider.is-grabbing *{cursor:grabbing;}
.hero-slider.is-grabbing{user-select:none;-webkit-user-select:none;}

/* ---- CTA gradient: deliberate shift on hover (on top of the ambient
   drift) so the primary action reads as responding to the pointer ---- */
.btn-primary:hover{background-position:100% 50%;}

/* ---- complete, consistent interactive states -----------------------
   disabled + loading for every button-like control, applied site-wide so
   the states are identical everywhere (Material/Polaris discipline). */
button:disabled, a.is-disabled,
.btn-primary:disabled, .btn-grad:disabled, .btn-ghost:disabled,
.send-btn:disabled, .price-apply:disabled, .sub-row button:disabled,
.pd-buy:disabled, .btn-sm:disabled{
  opacity:.55;
  cursor:not-allowed;
  filter:none !important;
  transform:none !important;
  box-shadow:none !important;
  pointer-events:none;
}
/* loading: dim the label, drop a spinner in the middle. .is-loading works on
   any button; keeps its box size so layout does not jump. */
.is-loading{position:relative;color:transparent !important;pointer-events:none;cursor:progress;}
.is-loading > *{opacity:0;}
.is-loading::after{
  content:"";position:absolute;top:50%;left:50%;width:18px;height:18px;margin:-9px 0 0 -9px;
  border-radius:999px;border:2px solid rgba(255,255,255,.45);border-top-color:#fff;
  animation:spin .6s linear infinite;
}
/* ghost/light buttons need a dark spinner to stay visible */
.btn-ghost.is-loading::after, .btn-edit.is-loading::after, .deals-tab.is-loading::after{
  border-color:rgba(var(--purple-rgb),.25);border-top-color:var(--purple);
}
@keyframes spin{to{transform:rotate(360deg);}}

/* ---- cart AJAX loading on the product-card stepper -----------------
   while a cart_api call is in flight the stepper shows a shimmer skeleton
   pill so the tap has immediate feedback even on a slow network. */
.qty-stepper.is-loading{position:relative;min-width:44px;min-height:32px;pointer-events:none;}
.qty-stepper.is-loading > *{visibility:hidden;}
.qty-stepper.is-loading::after{
  content:"";position:absolute;inset:0;border-radius:10px;
  background:linear-gradient(90deg,var(--lav) 25%,#fbf6ff 37%,var(--lav) 63%);
  background-size:400% 100%;animation:skelShimmer 1.4s ease infinite;
}

/* ---- toasts / snackbars --------------------------------------------
   bottom-center region, aria-live announced. Brand purple-pink accent bar,
   elevation-4, slide-up in / fade-out. Meaningful feedback for cart + forms. */
.toast-region{
  position:fixed;left:50%;bottom:calc(var(--space-6) + env(safe-area-inset-bottom,0px));
  transform:translateX(-50%);
  z-index:2000;display:flex;flex-direction:column;gap:var(--space-2);align-items:center;
  width:max-content;max-width:min(92vw,420px);pointer-events:none;
}
.toast{
  display:flex;align-items:center;gap:var(--space-3);
  background:#fff;color:var(--ink);border:1px solid var(--hairline);
  border-radius:14px;padding:var(--space-3) var(--space-4);
  box-shadow:var(--elevation-4);font-size:var(--text-base);font-weight:600;
  pointer-events:auto;position:relative;overflow:hidden;
  animation:toastIn var(--dur-5) var(--ease-decelerate) both;
}
.toast.toast-out{animation:toastOut var(--dur-4) var(--ease-accelerate) both;}
.toast::before{content:"";position:absolute;top:0;bottom:0;right:0;width:4px;background:var(--grad);}
.toast .toast-ic{display:flex;align-items:center;justify-content:center;width:26px;height:26px;border-radius:999px;flex-shrink:0;color:#fff;}
.toast.toast-success .toast-ic{background:var(--success);}
.toast.toast-error{border-color:var(--danger-bg);}
.toast.toast-error::before{background:var(--danger);}
.toast.toast-error .toast-ic{background:var(--danger);}
.toast .toast-ic .icon{width:15px;height:15px;stroke-width:2.5px;}
@keyframes toastIn{from{opacity:0;transform:translateY(16px);}to{opacity:1;transform:none;}}
@keyframes toastOut{from{opacity:1;transform:none;}to{opacity:0;transform:translateY(8px);}}

/* ---- inline form validation ----------------------------------------
   field-level error text + red ring, mirrors the semantic danger tokens. */
.field.has-error input, .field.has-error textarea,
.auth-box .field.has-error input,
.sub-row.has-error input{
  border-color:var(--danger) !important;
  box-shadow:0 0 0 3px rgba(220,38,38,.14);
}
.field-error{
  display:block;color:var(--danger);font-size:var(--text-xs);font-weight:600;
  margin-top:6px;line-height:1.5;
}
.sub-row + .field-error, .sub-row .field-error{color:var(--danger-on-dark);margin-top:8px;}

@media (prefers-reduced-motion: reduce){
  html.reveal-ready .reveal{opacity:1;transform:none;transition:none;}
  html.reveal-ready header.site,
  /* these two must carry the same `:not(.intro-done)` prefix as the rules they are
     cancelling. Without it they are two classes less specific and lose, which would leave
     the intro animation running under reduced motion — the one place it must not. */
  html.reveal-ready .hero-slider:not(.intro-done) .hero-slide.active .hero-visual,
  html.reveal-ready .hero-slider:not(.intro-done) .hero-slide.active .hero-text > *{animation:none;}
  .btn-primary:hover{background-position:0% 50%;}
  .card:hover .img .product-photo{transform:none;}
  .is-loading::after, .qty-stepper.is-loading::after{animation:none;}
  .toast{animation:none;}
  .toast.toast-out{animation:none;opacity:0;}
}

/* =====================================================================
   USABILITY PASS — affordance, target size, focus, state feedback.
   Patterns follow the same systems the rest of this file was audited
   against (Material, Polaris, Carbon, Adobe Spectrum) plus WCAG 2.2's
   24x24 minimum target size. Brand palette and RTL layout unchanged.
   ===================================================================== */

/* ---- skip link: first tab stop, visually hidden until focused ---- */
.skip-link{
  position:fixed; top:-100px; right:var(--space-4); z-index:9999;
  background:var(--purple); color:#fff;
  padding:var(--space-3) var(--space-5); border-radius:0 0 12px 12px;
  font-weight:700; font-size:var(--text-base);
  box-shadow:var(--elevation-4);
  transition:top var(--dur-3) var(--ease-decelerate);
}
.skip-link:focus{ top:0; }
.skip-link:focus-visible{ outline:none; box-shadow:var(--elevation-4), 0 0 0 3px rgba(255,255,255,.7); }
/* the skip target itself must not grab a focus ring when it receives programmatic focus */
#main-content:focus{ outline:none; }

/* ---- product card: stretched link ----------------------------------
   The card is a <div> now; `.card-link` is the one real link and its
   ::after covers the whole card, so the entire card stays clickable
   while the heart and the stepper remain independent buttons.
   Anything the user must be able to click on its own sits above the
   overlay (z-index 2) — the overlay itself is z-index 1. */
.card-link{ color:inherit; }
.card-link::after{
  content:''; position:absolute; inset:0; z-index:1;
  border-radius:inherit;
}
.card .heart, .card .qty-stepper{ position:relative; z-index:2; }
.card .heart{
  position:absolute; z-index:2;
  border:none; padding:0; cursor:pointer; font-family:inherit;
  transition:transform var(--dur-3) var(--ease-standard), background-color var(--dur-3) var(--ease-standard), color var(--dur-3) var(--ease-standard), box-shadow var(--dur-3) var(--ease-standard);
}
/* keyboard focus anywhere inside the card lifts the whole card, so tabbing
   through a grid shows the same "this one is active" cue the mouse gets */
.card:focus-within{ transform:translateY(-3px); box-shadow:var(--elevation-brand); border-color:rgba(var(--purple-rgb),.2); }
.card .name a{ color:inherit; }
.card:hover .name, .card:focus-within .name{ color:var(--purple); }
.card .name{ transition:color var(--dur-3) var(--ease-standard); }
/* the focus ring belongs to the card, not to the invisible stretched overlay */
.card-link:focus-visible{ outline:none; box-shadow:none; }
.card:focus-within{ outline:2px solid var(--purple); outline-offset:2px; }
.card:hover:not(:focus-within){ outline:none; }

/* ---- WCAG 2.2 target size: 24x24 minimum ---------------------------
   These were all real links/buttons already, just physically too small
   to hit reliably — inline text links ~20px tall, and an 8px slider dot. */
.footer-links div{ margin-bottom:2px; }
.footer-links div a{
  display:block; padding:var(--space-2) 0; line-height:1.5;
  transition:color var(--dur-3) var(--ease-standard), padding-inline-start var(--dur-3) var(--ease-standard);
}
.footer-links div a:hover{ color:#fff; }
.topline-links a{ display:inline-block; padding:var(--space-2) 0; line-height:1.4; }
/* the phone number is a real tel: link but read as static text — 22px tall, no
   hover, no hit padding. now it's a proper target with visible feedback. */
.phone-btn:hover{ background:var(--pink-tint); }
/* the remaining sub-24px targets, all of them inline text links doing real work:
   the section "مشاهده همه" links, breadcrumbs, and the login/register switch. */
.section-title a, .three-col-head a, .pagesub a, .switch a, .seo-text-toggle{
  display:inline-block; padding:var(--space-2) var(--space-2);
  margin-inline:calc(var(--space-2) * -1); border-radius:8px; line-height:1.4;
}
.section-title a:hover, .three-col-head a:hover, .pagesub a:hover, .switch a:hover{
  background:var(--lav); color:var(--purple);
}
.seo-text-toggle:hover{ background:var(--lav); }
/* the dot stays visually 8px; the hit area is grown to 24x24 with a
   transparent ::before, the standard way to hit the target-size minimum
   without redesigning the control */
.hero-dot{ position:relative; }
.hero-dot::before{
  content:''; position:absolute; top:50%; left:50%;
  transform:translate(-50%,-50%);
  width:24px; height:24px; border-radius:999px;
}
.hero-dot:hover{ background:rgba(var(--purple-rgb),.45); }
/* filter checkboxes: bigger box + a full-width padded label row to click.
   (selectors deliberately carry `.filters .group` — the existing
   `.filters input[type=checkbox]` rule above is more specific than a bare
   class and would otherwise keep the box at 16px.) */
.filters .group .category-filter-checkbox{ width:18px; height:18px; cursor:pointer; }
.filters .group label{
  display:flex; align-items:center; gap:var(--space-2);
  margin-bottom:0;
  padding:var(--space-2) var(--space-1); border-radius:8px; cursor:pointer;
  transition:background-color var(--dur-2) var(--ease-standard), color var(--dur-2) var(--ease-standard);
}
.filters .group label:hover{ background:var(--lav); color:var(--purple); }

/* ---- focus visibility on dark surfaces -----------------------------
   The global focus ring is a translucent purple, which all but vanishes
   against the near-black footer. Dark surfaces get a light ring instead. */
footer.site a:focus-visible,
footer.site button:focus-visible,
footer.site input:focus-visible,
.mobile-nav-overlay a:focus-visible,
.mobile-nav-overlay button:focus-visible{
  outline:none;
  box-shadow:0 0 0 3px rgba(255,255,255,.75);
  border-color:transparent;
}

/* ---- interaction states that were missing entirely ------------------ */
/* tabs: real buttons now — reset the chrome, then give them hover +
   a selected underline instead of only a colour change */
.pd-tabs{ position:relative; }
.pd-tabs [role="tab"]{
  background:none; border:none; font-family:inherit; font-size:inherit;
  font-weight:inherit; color:inherit; cursor:pointer;
  padding:var(--space-2) var(--space-1) var(--space-3);
  margin-bottom:calc(var(--space-3) * -1 - 1.5px);
  border-bottom:2.5px solid transparent;
  transition:color var(--dur-3) var(--ease-standard), border-color var(--dur-3) var(--ease-standard);
}
.pd-tabs [role="tab"]:hover{ color:var(--purple); }
.pd-tabs [role="tab"].active{ color:var(--pink); border-bottom-color:var(--pink); }
[role="tabpanel"]:focus{ outline:none; }
[role="tabpanel"]:focus-visible{ outline:2px solid var(--purple); outline-offset:4px; border-radius:8px; }

/* gallery thumbnails had no hover feedback at all */
.pd-thumbs button{ transition:border-color var(--dur-3) var(--ease-standard), transform var(--dur-3) var(--ease-standard); }
.pd-thumbs button:hover{ border-color:rgba(var(--purple-rgb),.45); transform:translateY(-2px); }

/* variant picker: hover + a clear pressed state */
.pd-opt{ transition:border-color var(--dur-3) var(--ease-standard), background-color var(--dur-3) var(--ease-standard), color var(--dur-3) var(--ease-standard), transform var(--dur-2) var(--ease-standard); }
.pd-opt:hover:not(.active){ border-color:var(--purple); color:var(--purple); background:var(--lav); }
.pd-opt:active{ transform:scale(.97); }
.pd-fav{ transition:background-color var(--dur-3) var(--ease-standard), color var(--dur-3) var(--ease-standard), transform var(--dur-2) var(--ease-standard); }
.pd-fav:hover{ background:var(--pink-tint); color:var(--pink); }
.pd-fav:active, .pd-buy:active, .card .add:active, .qty-stepper .qty-btn:active{ transform:scale(.96); }

/* mega-menu links: an arrow that slides in on hover, so the row reads as
   "goes somewhere" rather than just changing colour */
.mega-col a{ position:relative; border-radius:6px; padding-inline:var(--space-2); margin-inline:calc(var(--space-2) * -1); }
.mega-col a:hover, .mega-col a:focus-visible{ background:var(--lav); }
.icon-nav-link:focus-visible{ outline:none; box-shadow:0 0 0 3px rgba(var(--purple-rgb),.35); }

/* ---- readability: line length and rhythm --------------------------- */
.pd-desc{ max-width:62ch; }
.pd-reviews{ max-width:70ch; }
.footer-brand p{ max-width:44ch; }

@media (prefers-reduced-motion: reduce){
  .skip-link{ transition:none; }
  .card:focus-within{ transform:none; }
  .pd-thumbs button:hover{ transform:none; }
  .pd-opt:active, .pd-fav:active, .pd-buy:active,
  .card .add:active, .qty-stepper .qty-btn:active{ transform:none; }
}

/* =====================================================================
   SHOP FILTERING — two-handle price slider, AJAX result states,
   and a softer selected-chip gradient.
   ===================================================================== */

/* ---- two-handle price range ----------------------------------------
   Two native <input type=range> stacked on one drawn track. The inputs are
   transparent and pointer-transparent EXCEPT for their thumbs, so whichever
   handle you aim at is the one you grab even though the boxes fully overlap.
   The component is forced LTR so "min = left handle" is stable inside the
   RTL page; .range-ends labels which end is which. */
.range-slider{
  position:relative; direction:ltr;
  height:24px; margin:18px 0 2px;
}
.range-track, .range-fill{
  position:absolute; top:50%; transform:translateY(-50%);
  height:6px; border-radius:999px;
}
.range-track{ left:0; right:0; background:var(--lav); }
.range-fill{ background:linear-gradient(90deg, var(--purple), var(--pink)); }

.range-input{
  position:absolute; left:0; right:0; top:0;
  width:100%; height:24px; margin:0;
  background:none; border:none; padding:0;
  -webkit-appearance:none; appearance:none;
  pointer-events:none;            /* the track must not swallow clicks meant for the other handle */
}
.range-input:focus{ outline:none; }
.range-min{ z-index:3; }
.range-max{ z-index:4; }

/* thumbs — the only part of the input that takes pointer events */
.range-input::-webkit-slider-thumb{
  -webkit-appearance:none; appearance:none;
  pointer-events:auto;
  width:20px; height:20px; border-radius:50%;
  background:#fff; border:2.5px solid var(--purple);
  box-shadow:var(--elevation-2);
  cursor:grab;
  transition:transform var(--dur-2) var(--ease-standard), box-shadow var(--dur-2) var(--ease-standard), border-color var(--dur-2) var(--ease-standard);
}
.range-input::-moz-range-thumb{
  pointer-events:auto;
  width:20px; height:20px; border-radius:50%;
  background:#fff; border:2.5px solid var(--purple);
  box-shadow:var(--elevation-2);
  cursor:grab;
  transition:transform var(--dur-2) var(--ease-standard), box-shadow var(--dur-2) var(--ease-standard), border-color var(--dur-2) var(--ease-standard);
}
.range-input::-moz-range-track{ background:none; border:none; }
.range-input:hover::-webkit-slider-thumb{ transform:scale(1.12); border-color:var(--pink); }
.range-input:hover::-moz-range-thumb{ transform:scale(1.12); border-color:var(--pink); }
.range-input:active::-webkit-slider-thumb{ cursor:grabbing; transform:scale(1.18); }
.range-input:active::-moz-range-thumb{ cursor:grabbing; transform:scale(1.18); }
.range-input:focus-visible::-webkit-slider-thumb{ box-shadow:0 0 0 4px rgba(var(--purple-rgb),.30); }
.range-input:focus-visible::-moz-range-thumb{ box-shadow:0 0 0 4px rgba(var(--purple-rgb),.30); }

.range-ends{
  display:flex; justify-content:space-between; direction:ltr;
  font-size:var(--text-xs); color:var(--gray); margin-bottom:var(--space-3);
}
.range-ends span:first-child{ order:1; }   /* "کمترین" belongs at the min (left) end */

/* ---- AJAX result states --------------------------------------------- */
.product-grid.is-filtering{
  opacity:.45;
  transition:opacity var(--dur-3) var(--ease-standard);
  pointer-events:none;             /* don't let a click land on a card about to be replaced */
}
.shop-empty{ color:var(--gray); padding:40px 0; text-align:center; }
.shop-empty[hidden]{ display:none; }

/* ---- softer selected state -------------------------------------------
   The selected chip used a hard 90deg purple→pink sweep plus a heavy 28%
   brand shadow, which read as a harsh slab next to the quiet outline chips.
   A diagonal ramp with a mid stop and a lighter shadow keeps the same brand
   colours but lets them blend instead of banding. Shared by every "this one
   is selected" surface so they stay consistent. */
.pill.active,
.pd-opt.active,
.deals-tab.active{
  background:linear-gradient(135deg,
    color-mix(in oklab, var(--purple) 92%, white),
    color-mix(in oklab, var(--purple) 55%, var(--pink)) 55%,
    color-mix(in oklab, var(--pink) 90%, white));
  box-shadow:0 6px 16px rgba(var(--purple-rgb),.18);
}
/* fallback for engines without color-mix(): plain 3-stop ramp, same intent */
@supports not (background: color-mix(in oklab, red, blue)){
  .pill.active, .pd-opt.active, .deals-tab.active{
    background:linear-gradient(135deg, #6B4FBE, #8E44A0 55%, #C2517E);
  }
}
.pill.active:hover{
  box-shadow:0 8px 20px rgba(var(--purple-rgb),.24);
  transform:translateY(-1px);
}

/* the primary CTA gradient gets the same softening, minus the hover shift
   that .btn-primary already animates */
.btn-grad, .price-apply, .pd-buy{
  background:linear-gradient(135deg,
    color-mix(in oklab, var(--purple) 94%, white),
    color-mix(in oklab, var(--purple) 50%, var(--pink)) 55%,
    color-mix(in oklab, var(--pink) 92%, white));
}
@supports not (background: color-mix(in oklab, red, blue)){
  .btn-grad, .price-apply, .pd-buy{
    background:linear-gradient(135deg, #6D51C0, #9046A2 55%, #C4547F);
  }
}

@media (prefers-reduced-motion: reduce){
  .product-grid.is-filtering{ transition:none; }
  .range-input::-webkit-slider-thumb{ transition:none; }
  .range-input::-moz-range-thumb{ transition:none; }
  .range-input:hover::-webkit-slider-thumb,
  .range-input:active::-webkit-slider-thumb{ transform:none; }
  .range-input:hover::-moz-range-thumb,
  .range-input:active::-moz-range-thumb{ transform:none; }
  .pill.active:hover{ transform:none; }
}

/* =====================================================================
   SECTION BALANCE — content centred inside its (already centred) container.
   Every section BOX measured dead-centre (skew 0) at 1440/1366/1280; what
   leaned was the content floating to one edge inside those boxes.
   ===================================================================== */

/* ---- coloured carousels (فروش ویژه / به‌زودی) ------------------------
   `.colored-carousel-track` is a flex scroller. With only 2-3 cards there's
   nothing to scroll, and RTL flex packs the cards against the inline start
   (the right), stranding 300-640px of empty panel on the left — measured at
   every desktop width. `safe center` centres them when they fit and falls
   back to start-packing the moment they overflow, so a long list is still
   fully scrollable (plain `center` would make the first cards unreachable).
   Snap-align centre lets a card come to rest centred while scrolling, which
   is what the arrows now scroll between. */
.colored-carousel-track{
  justify-content:safe center;
  scroll-snap-type:x proximity;
  scroll-padding-inline:var(--space-4);
}
.colored-carousel-card{ scroll-snap-align:center; }

/* ---- پیشنهاد شگفت‌انگیز -----------------------------------------------
   The countdown is `width:fit-content`, so in RTL it hugged the right edge of
   the deals column with the whole left half of the row empty — that's the
   "leans right". Centre it over the cards it belongs to. */
.deals-countdown{ margin-inline:auto; }

/* ---- chips rows ------------------------------------------------------
   The homepage row carried an inline `justify-content:flex-end`. Once the
   chips wrapped, the last one ("همه سازها") was left stranded alone against
   the opposite edge with a gap on the other side. Centring keeps both the
   full row and the short wrapped row balanced. */
.chips-row{ justify-content:center; }

/* a partially-filled magazine page shouldn't pack its cards to one edge either */
.magazine-page{ justify-content:center; }

/* =====================================================================
   SESSION 5 — mobile centring + the coloured carousels rebuilt for phones.
   Measured at 375px and 414px before touching anything; every rule below
   answers a number, not a hunch. See PROGRESS.md for the readings.
   ===================================================================== */

/* ---- section header rows ---------------------------------------------
   The markup is `<a>مشاهده همه</a><h2>Title</h2>`. In an RTL flex row the
   FIRST item takes the inline start (the right), so the link led the row and
   the heading was pushed hard against the left edge — reading backwards, and
   out of line with the right-anchored content underneath it. Reordering in
   CSS (rather than swapping the markup) keeps the link early in the tab and
   reading order, where it already was. */
.section-title h2, .three-col-head h3{ order:-1; }
.section-title h2{ min-width:0; }
.section-title a, .three-col-head a{ flex:0 0 auto; white-space:nowrap; }

/* ---- coloured carousels on phones (فروش ویژه / به‌زودی) ---------------
   The desktop layout is a horizontal row: vertical label + track, with 56-72px
   of side padding reserved for the overlaid arrows. On a 375px phone that
   arithmetic collapses — measured: 303px panel − 112px padding − 25px label −
   24px gap left a 142px track for 130px cards (164px in the به‌زودی block).
   So one card sat off-centre with ~180px of empty panel beside it, every other
   card was clipped away by the track's own overflow, and the arrows — which
   only appear on :hover — were unreachable on touch entirely.

   On phones it becomes a vertical stack instead: horizontal label on top, a
   full-width one-card-per-view snap track, and the arrows as a real centred
   control row underneath, where they can't cover the card. One card per view
   is deliberate: a peeking neighbour is a nice affordance at desktop widths
   but at 375px it reads as the half-cut card that was reported. */
@media (max-width:860px){
  .colored-carousel{
    flex-direction:column; align-items:stretch;
    gap:var(--space-3);
    /* the bottom padding is the arrow row's reserved space */
    padding:20px 16px 68px;
  }
  .colored-carousel-label{
    writing-mode:horizontal-tb; transform:none;
    width:100%; text-align:center; font-size:18px;
  }
  .colored-carousel-track{
    flex:none; width:100%;
    justify-content:flex-start;      /* one card fills the track; nothing to centre */
    gap:var(--space-3);
    scroll-snap-type:x mandatory;    /* mandatory so a card can never rest half-way */
    scroll-padding-inline:0;
  }
  .colored-carousel-card{
    flex:0 0 100%; min-width:0;
    scroll-snap-align:center;
    padding:16px;
  }
  /* a full-width card needs a taller image or it reads as a letterbox */
  .colored-carousel .ccc-img{ height:150px; }
  .colored-carousel .ccc-name{ font-size:14px; }
  .colored-carousel .ccc-price{ font-size:14px; }

  /* arrows: out of the card's way, always visible (there is no hover on touch),
     and grown to a comfortable tap target. */
  .colored-carousel-arrow{
    top:auto; bottom:14px; transform:none;
    opacity:1; width:44px; height:44px;
  }
  .ccc-prev{ right:calc(50% - 50px); left:auto; }
  .ccc-next{ left:calc(50% - 50px); right:auto; }
}

/* Even at desktop widths, hover-only arrows are invisible to a touch device
   (tablets, touch laptops). Show them whenever hover isn't available. */
@media (hover:none){
  .colored-carousel-arrow{ opacity:1; }
}
.colored-carousel-arrow:focus-visible{ opacity:1; }

/* Once the three-col row stopped overflowing, the names had to fit a real 303px
   column and the single-line ellipsis started cutting mid-word through the Latin
   part of mixed Persian/Latin names ("...استراتوکستر ries"), which bidi reorders
   into nonsense. Two wrapped lines instead. */
@media (max-width:860px){
  .compact-row-name{
    white-space:normal; text-overflow:clip;
    display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical;
  }
}

/* ---- product cards: bottom-align the price row -----------------------
   Grid stretch already makes every card box the same height, but `.body` was a
   plain block, so the price + add button sat directly under the name — and at
   two-up phone widths a name that wraps to two lines pushed its card's price a
   whole line below its neighbour's. Pinning the price row to the bottom of the
   card makes the row read as a row. */
.card{ display:flex; flex-direction:column; }
.card .img{ flex:0 0 auto; }
.card .body{ display:flex; flex-direction:column; flex:1 1 auto; }
.card .pricerow{ margin-top:auto; }

/* ---- the deals tab strip --------------------------------------------
   It's a horizontal scroller on phones; it was showing a native scrollbar and
   had no scroll padding, so the last tab sat flush against the cut edge. */
@media (max-width:860px){
  .deals-tabs{
    gap:var(--space-2);
    scrollbar-width:none;
    scroll-snap-type:x proximity;
    padding-bottom:2px;
  }
  .deals-tabs::-webkit-scrollbar{ display:none; }
  .deals-tab{ flex:0 0 auto; scroll-snap-align:start; }
}

/* =====================================================================
   SESSION 6 — on phones, "centred" means the content READS centred, not
   merely that its gutters are equal.

   Session 5 asserted symmetry: every section's content box measured skew 0,
   flush to both edges, and that is still true. But symmetric is not the same
   as centred. The page was edge-anchored everywhere — headings hard against
   the right, "مشاهده همه" hard against the left, card text right-aligned with
   prices pushed to the opposite edge. Equal gutters, nothing centred. On a
   375px screen that reads as content flung at the two edges, which is what
   was being reported. These rules centre the composition itself.

   Phone-only (<=640px): tablets keep the two-ended row layout, which works
   fine at those widths and matches the desktop design.
   ===================================================================== */
@media (max-width:640px){

  /* ---- section headers: a centred stacked block ----
     Title on its own line, "مشاهده همه" centred beneath it, instead of the
     two being flung to opposite edges. (The h2 already carries order:-1 from
     session 5, so in a column it lands on top.) */
  .section-title, .three-col-head{
    flex-direction:column;
    align-items:center;
    justify-content:center;
    text-align:center;
    gap:var(--space-2);
  }
  .section-title h2, .three-col-head h3{ text-align:center; }
  /* the -8px hit-area bleed is only there to keep the text flush with an edge;
     centred, it just makes the box asymmetric */
  .section-title a, .three-col-head a{ margin-inline:0; }

  /* ---- product cards (پرفروش‌ترین‌ها) read as centred tiles ---- */
  .card .body{ text-align:center; }
  .card .pricerow{ justify-content:center; }

  /* ---- deal cards (پیشنهاد شگفت‌انگیز) ---- */
  .deal-card-body{ text-align:center; }
  .deal-card-prices{ align-items:center; }

  /* ---- deal category tabs ----
     Session 5 made these a one-line scroller. Four tabs need 416px and the
     viewport is 303px, so a scroller can never centre them — it can only pack
     them against an edge. Wrapping to two centred rows fits, centres, and
     removes the overflow entirely. */
  .deals-tabs{
    flex-wrap:wrap;
    justify-content:center;
    overflow-x:visible;
  }
  .deals-tab{ text-align:center; }
  .deals-banners{ justify-content:center; }

  /* ---- جدیدترین‌ها / پرفروش‌ترین‌ها compact rows ----
     The row keeps its thumb / name / price structure — it's a list row, and
     stacking it would cost a lot of height — but the name block between the
     two ends is centred rather than jammed against the thumb. */
  .compact-row-info{ text-align:center; }

  /* ---- feature strip: icon above centred text ---- */
  .feature{ flex-direction:column; text-align:center; }

  /* ---- SEO block ----
     Heading and the expand control centre. The body copy deliberately does NOT:
     centred multi-line prose has a ragged start edge on every line and is
     measurably harder to read. Centring it would look tidier and read worse. */
  .seo-text-block h2{ text-align:center; }
  .seo-text-toggle{ justify-content:center; width:100%; }
}

/* =====================================================================
   SESSION 7 — visual polish on the purple → pink brand.

   The palette itself lives in the admin theme settings (site_settings:
   theme_color_purple / theme_color_pink), now #7C3AED → #E8339A. Everything
   below is built on --purple / --pink so a future palette change from the
   admin panel carries through instead of being fought by hardcoded hexes.
   ===================================================================== */
:root{
  --brand-grad: linear-gradient(135deg, var(--purple), var(--pink));
  --brand-grad-soft: linear-gradient(140deg, rgba(var(--purple-rgb),.09), rgba(var(--pink-rgb),.07));
  --frame-border: linear-gradient(140deg, rgba(var(--purple-rgb),.34), rgba(var(--pink-rgb),.28));
  --frame-border-hover: linear-gradient(140deg, rgba(var(--purple-rgb),.62), rgba(var(--pink-rgb),.52));
  --brand-glow: 0 16px 36px rgba(var(--purple-rgb),.18), 0 4px 14px rgba(var(--pink-rgb),.10);
  /* #E8339A measures ~3.9:1 on white — fine for fills and for large display
     type, below AA for the 12–14px bold prices it was being used on. This is
     the same hue stepped down until it clears 4.5:1; small text uses it. */
  --pink-ink:#C42B82;
}
@supports (color: color-mix(in oklab, red, blue)){
  /* derive it so an admin palette change carries through; the static value
     above stays as the fallback. Contrast is asserted in polish-test.js. */
  :root{ --pink-ink: color-mix(in oklab, var(--pink) 78%, #180410); }
}

/* ---- 1. framed containers ("کادر") ----------------------------------
   A hairline gradient border via the two-background trick: a solid fill
   clipped to the padding box, the brand gradient clipped to the border box.
   One extra background per card, no extra element, and it follows the
   border-radius exactly (which a ::before overlay would not). */
.card, .deal-card, .three-col-item, .brands-strip{
  border:1px solid transparent;
  background:
    linear-gradient(#fff,#fff) padding-box,
    var(--frame-border) border-box;
}
/* these two carry body content rather than product imagery, so they take the
   tinted fill — enough to read as branded, light enough to stay clean */
/* .seo-text-block is deliberately NOT tinted: its collapsed state is faded out by
   a `::after` that ramps to solid #fff, so a tinted fill shows as a white band
   across the fade. It keeps the gradient frame, just on a white fill. */
.seo-text-block{
  border:1px solid transparent;
  background:
    linear-gradient(#fff,#fff) padding-box,
    var(--frame-border) border-box;
}
.feature-strip{
  border:1px solid transparent;
  /* THREE layers, and the order matters. The padding-box tint is translucent by
     design, so it cannot be the bottom layer: the border-box gradient would show
     through the entire panel rather than just the 1px ring, stacking .34 under
     .09 and rendering a heavy purple wash instead of a hairline. An opaque white
     layer sits between them to stop that. */
  background:
    var(--brand-grad-soft) padding-box,
    linear-gradient(#fff,#fff) padding-box,
    var(--frame-border) border-box;
}
.card, .deal-card, .three-col-item{
  transition: transform var(--dur-4) var(--ease-standard),
              box-shadow var(--dur-4) var(--ease-standard),
              background var(--dur-4) var(--ease-standard);
}
.card:hover, .card:focus-within, .deal-card:hover, .deal-card:focus-visible, .three-col-item:hover{
  box-shadow: var(--brand-glow);
  background:
    linear-gradient(#fff,#fff) padding-box,
    var(--frame-border-hover) border-box;
}
.three-col-item:hover{ transform:translateY(-2px); }

/* ---- 2. colour accents ----------------------------------------------- */
/* a short gradient rule under every section heading */
.section-title h2{ position:relative; padding-bottom:var(--space-2); }
.section-title h2::after{
  content:''; position:absolute; bottom:0; left:0;
  width:44px; height:3px; border-radius:999px; background:var(--brand-grad);
}
[dir="rtl"] .section-title h2::after{ left:auto; right:0; }
/* the phone header is centred (session 6), so the rule centres with it */
@media (max-width:640px){
  .section-title h2::after{ left:50%; right:auto; transform:translateX(-50%); }
}

/* discount badges pick up the gradient instead of a flat pink */
.card .off, .deal-card-img .off{
  background:var(--brand-grad);
  box-shadow:0 4px 12px rgba(var(--pink-rgb),.30);
}

/* small bold pink text moves to the AA-safe step of the same hue */
.deal-price, .ccc-price, .compact-row-price,
.pd-tabs .active, .pd-tabs [role="tab"].active,
.seo-text-toggle:hover, .mega-col a:hover, .search-suggest-viewall:hover{
  color:var(--pink-ink);
}

/* category pills get a soft brand wash on hover rather than only a border */
.pill:hover{ background:var(--brand-grad-soft); border-color:rgba(var(--purple-rgb),.38); }
/* the icon tiles in the feature strip pick up the gradient edge */
.feature .fic{ box-shadow:inset 0 0 0 1px rgba(var(--purple-rgb),.12); }

/* ---- 3. micro-interactions ------------------------------------------- */
/* a single light sweep across primary CTAs on hover/focus. The overlay is
   pointer-events:none so it can never intercept the click. */
.btn-grad, .pd-buy, .price-apply, .btn-primary{ position:relative; overflow:hidden; }
.btn-grad::after, .pd-buy::after, .price-apply::after, .btn-primary::after{
  content:''; position:absolute; inset:0; pointer-events:none;
  background:linear-gradient(115deg, transparent 32%, rgba(255,255,255,.40) 50%, transparent 68%);
  transform:translateX(-140%);
}
.btn-grad:hover::after, .btn-grad:focus-visible::after,
.pd-buy:hover::after, .pd-buy:focus-visible::after,
.price-apply:hover::after, .price-apply:focus-visible::after,
.btn-primary:hover::after, .btn-primary:focus-visible::after{
  animation:brandShimmer 850ms var(--ease-standard);
}
@keyframes brandShimmer{ from{ transform:translateX(-140%); } to{ transform:translateX(140%); } }

/* soft brand glow on the primary surfaces while hovered */
.btn-grad:hover, .pd-buy:hover, .price-apply:hover{
  box-shadow:0 10px 26px rgba(var(--purple-rgb),.30), inset 0 1px 0 rgba(255,255,255,.28);
}

/* the coloured carousel arrows lift slightly and warm up on hover */
.colored-carousel-arrow{ transition:opacity var(--dur-3) var(--ease-standard), transform var(--dur-3) var(--ease-standard), background-color var(--dur-3) var(--ease-standard); }
.colored-carousel-arrow:hover{ transform:scale(1.08); background:#fff; }
@media (max-width:860px){
  /* the mobile arrows are statically positioned, so the desktop rule's
     translateY(-50%) is not in play and a plain scale is correct */
  .colored-carousel-arrow:hover{ transform:scale(1.08); }
}

/* ---- reduced motion: everything above degrades to a static state ------ */
@media (prefers-reduced-motion: reduce){
  .btn-grad::after, .pd-buy::after, .price-apply::after, .btn-primary::after{ display:none; }
  .card, .deal-card, .three-col-item{ transition:none; }
  .three-col-item:hover{ transform:none; }
  .colored-carousel-arrow{ transition:none; }
  .colored-carousel-arrow:hover{ transform:none; }
}

/* ---- contrast correction for the brighter palette ---------------------
   #E8339A is a great identity colour and a poor text background: white on it
   measures 3.91:1, and the buttons carrying white text are 13-15px at weight
   600-700, i.e. NOT "large text", so they need 4.5:1. The previous pink
   (#C43E7D) happened to clear it at 4.85:1, so adopting the brighter brand
   pink silently regressed every filled button and badge.

   Resolution: the vivid gradient stays for everything decorative (frames,
   accent rules, glows, hover washes) and FILLED SURFACES THAT CARRY TEXT end
   at --pink-ink instead — same hue, stepped down until white clears AA. The
   difference is barely perceptible across a large fill; the contrast
   difference is 3.91:1 vs 5.2:1. Asserted per gradient stop in polish-test.js. */
:root{
  --grad: linear-gradient(90deg, var(--purple), var(--pink-ink));
}
.btn-grad{
  background:linear-gradient(90deg, rgba(var(--purple-rgb),.96), var(--pink-ink));
}
.btn-primary{
  background:linear-gradient(135deg, rgba(var(--purple-rgb),.96), var(--pink-ink));
}
.card .off, .deal-card-img .off{
  background:linear-gradient(135deg, var(--purple), var(--pink-ink));
}
/* session 4 softened these by mixing each end toward white. Against the old
   pink that still cleared AA; against the new one it does not. Same soft
   three-stop ramp, but built between the two ink-safe ends instead. */
.pill.active, .pd-opt.active, .deals-tab.active{
  background:linear-gradient(135deg,
    var(--purple),
    color-mix(in oklab, var(--purple) 45%, var(--pink-ink)) 55%,
    var(--pink-ink));
}
@supports not (background: color-mix(in oklab, red, blue)){
  .pill.active, .pd-opt.active, .deals-tab.active{
    background:linear-gradient(135deg, #6B4FBE, #9B3A8E 55%, #C42B82);
  }
}
/* the coloured carousel panel carries an 18px bold white label — just under the
   18.66px "large text" cutoff, so it needs the full 4.5:1 too */
.colored-carousel{ background:linear-gradient(135deg, var(--purple), var(--pink-ink)); }

/* =====================================================================
   SESSION 8 — design-system pass.

   The site already had good spacing, type, motion and elevation tokens. What
   it lacked was the layer above them: a radius scale, semantic surface/text
   tokens, a focus system that actually survives contact with the layout, and
   a consistent state matrix. Those are what make a system feel deliberate
   rather than assembled. Reference points: Primer's focus + state model,
   Material's elevation ramp, Polaris's empty-state anatomy, Carbon's skeleton
   loading. Adapted, not copied — this is an RTL Persian storefront, not a SaaS
   console.
   ===================================================================== */
:root{
  /* ---- radius scale ----
     The file had 18 distinct radii including 6/9/13/22/40px one-offs. A scale
     removes the guesswork: pick the tier, not the number. */
  /* PORTED. The file's radius steps are 2 / 4 / 6 / 8 / 12 / 20 / 24 / 100(pill)
     and nothing else. The previous 8/10/14/20/28/36 ramp was noticeably rounder
     than the system it was meant to follow, so every tier moves down a notch.
     The tiers are assigned so the components that consume them land on the value
     the Figma component actually uses — see the per-component note below. */
  --radius-xs:4px;    /* tiny tags */
  --radius-sm:6px;    /* underline tabs, small chips        (Figma Tab = 6)     */
  --radius-md:8px;    /* inputs, badges, menu items          (Figma Input = 8)   */
  --radius-lg:12px;   /* buttons, cards, alerts, pop-ups     (Figma Card = 12)   */
  --radius-xl:20px;   /* pill tabs, side panels              (Figma Tab pill=20) */
  --radius-2xl:24px;  /* pagination, menu containers         (Figma Pagination=24)*/
  --radius-pill:999px;

  /* ---- semantic surfaces ----
     Components were hardcoding #fff and rgba(26,29,38,...) everywhere, which
     is why the palette change in session 7 had to touch so many rules. */
  --surface:#fff;                        /* Figma White/100% */
  --surface-sunken:var(--cream);         /* Grey/50 */
  --surface-muted:var(--grey-100);       /* Grey/100 — inert fills, track bgs */
  --surface-brand-subtle:var(--lav);
  --surface-brand-wash:linear-gradient(140deg, rgba(var(--purple-rgb),.09), rgba(var(--pink-rgb),.07));
  /* borders now come off the ported ramp: the file draws component strokes as a
     solid Grey step, and reserves translucent ink for shadows only */
  --border-subtle:var(--grey-200);       /* #E5E7EA */
  --border-strong:var(--grey-300);       /* #D2D5DB */
  --border-brand:rgba(var(--purple-rgb),.38);
  --border-width:1.5px;                  /* Figma strokeWeight on every control */
  --text-primary:var(--ink);
  --text-secondary:var(--ink-soft);
  --text-tertiary:var(--gray);
  --text-placeholder:var(--grey-400);
  --text-on-brand:#fff;

  /* ---- line-height / tracking ----
     PORTED, with one deliberate exception. The file pairs each size with a line
     height: headings land on 1.19-1.21, captions on 1.33, Latin body on 1.5.
     tight/snug are taken verbatim. --leading-normal keeps a Persian uplift to
     1.6 rather than dropping to the file's 1.5: Vazirmatn's ascenders and the
     Arabic-script diacritics need the extra room, which is the same reason
     session 8 set 1.7. 1.6 splits the difference — closer to the system,
     still legible in Persian. --leading-relaxed (1.75) is for long prose. */
  --leading-tight:1.2;      /* Figma H1-H5 */
  --leading-snug:1.33;      /* Figma C1-C3 captions */
  --leading-normal:1.6;     /* Figma body is 1.5; +.1 for Persian */
  --leading-relaxed:1.75;
  --tracking-tight:-.02em;
  --tracking-normal:0;

  /* ---- control sizing — PORTED from the Button / Input / Badge frames.
     The file ships five control heights and three badge heights; these make
     them nameable instead of each button inventing its own padding. ---- */
  --control-h-xs:24px; --control-h-sm:32px; --control-h-md:40px;
  --control-h-lg:48px; --control-h-xl:56px;
  --control-px-sm:var(--space-2);   /* 8  */
  --control-px-md:var(--space-3);   /* 12 */
  --control-px-lg:var(--space-4);   /* 16 */
  --badge-h-sm:16px; --badge-h-md:24px; --badge-h-lg:32px;

  /* ---- focus ----
     A single ring definition, re-themed per surface by overriding the custom
     property (see the dark-surface block below). */
  --focus-ring-width:3px;
  --focus-ring-offset:2px;
  --focus-ring-color:rgba(var(--purple-rgb),.65);
}

/* ---- the focus system -------------------------------------------------
   The previous ring was `box-shadow: 0 0 0 3px`. box-shadow is painted inside
   the clipping chain, so ANY ancestor with overflow hidden/clip/auto/scroll
   cuts it off — measured on this site: the product-page quantity buttons sit
   1px inside an overflow:hidden `.qty`, the carousel cards sit flush against
   an overflow:auto track, the topline links flush against their clipper. Those
   rings were being erased. `outline` is not clipped by ancestors, and unlike
   box-shadow it is still painted in Windows High Contrast / forced-colors
   mode, where box-shadow is dropped entirely.

   Same selector list and specificity as the original rule, later in the
   cascade, so it replaces it cleanly. */
a:focus-visible, button:focus-visible, input:focus-visible,
textarea:focus-visible, select:focus-visible,
[tabindex]:focus-visible, summary:focus-visible{
  outline:var(--focus-ring-width) solid var(--focus-ring-color);
  outline-offset:var(--focus-ring-offset);
  box-shadow:none;
}
/* Dark surfaces re-theme the ring by overriding the token — the rule above is
   untouched. Custom-property inheritance is doing the work. */
.footer-top, .footer-bottom, .deals-countdown, .colored-carousel{
  --focus-ring-color:rgba(255,255,255,.92);
}
/* ...and the white cards sitting ON the coloured panel flip it back, otherwise
   a white ring lands on a white card. */
.colored-carousel-card, .colored-carousel-arrow{
  --focus-ring-color:rgba(var(--purple-rgb),.75);
}
/* inputs already carry a border; the ring reads better tight against it */
input:focus-visible, textarea:focus-visible, select:focus-visible{
  outline-offset:1px;
  border-color:var(--purple);
}

/* ---- interaction state matrix ----------------------------------------
   Every interactive surface gets the same four steps, so the whole UI
   responds with one vocabulary: rest → hover (lift + brand tint) → active
   (press down, never just "no change") → disabled (flat, no pointer). */
.pill:active, .deals-tab:active, .icon-btn:active, .colored-carousel-arrow:active{
  transform:translateY(0) scale(.97);
}
.pill:disabled, .pill[aria-disabled="true"], .deals-tab:disabled{
  background:var(--disabled-bg); color:var(--disabled-text);
  border-color:transparent; box-shadow:none; cursor:not-allowed; transform:none;
}
/* hover is a pointer affordance; on touch it sticks after a tap, so gate the
   lift on devices that actually hover. This is why cards felt "stuck" on
   phones after tapping. */
@media (hover:hover){
  .pill:hover{ transform:translateY(-2px); box-shadow:var(--elevation-2); }
  .deals-tab:hover:not(.active){ border-color:var(--border-brand); background:var(--surface-brand-subtle); }
}
@media (hover:none){
  .card:hover, .deal-card:hover, .three-col-item:hover, .pill:hover{ transform:none; }
}

/* ---- elevation discipline --------------------------------------------
   Raised surfaces now step through the existing ramp instead of each one
   inventing its own rgba. Cards rest at 1, lift to a brand-tinted 3. */
.card, .deal-card, .three-col-item{ box-shadow:var(--elevation-1); }
.card:hover, .card:focus-within, .deal-card:hover, .three-col-item:hover{
  box-shadow:var(--elevation-3), var(--elevation-brand);
}
.brands-strip, .feature-strip, .seo-text-block{ box-shadow:var(--elevation-2); }

/* ---- badges -----------------------------------------------------------
   One anatomy, four intents. `.badgeNew` was defined but wired to nothing;
   these replace it with something the templates can actually use. */
.tag{
  display:inline-flex; align-items:center; gap:var(--space-1);
  padding:3px 10px; border-radius:var(--radius-pill);
  font-size:var(--text-xs); font-weight:700; line-height:var(--leading-snug);
  white-space:nowrap;
}
.tag-brand{ background:linear-gradient(135deg,var(--purple),var(--pink-ink)); color:var(--text-on-brand); }
/* the intent badges read from the ported triples now, instead of each carrying
   its own hand-picked foreground hex */
.tag-new{ background:var(--intent-warning-bg); color:var(--intent-warning-fg); }
.tag-soon{ background:var(--grey-800); color:#fff; }
.tag-muted{ background:var(--surface-brand-subtle); color:var(--purple); }
.tag-success{ background:var(--intent-success-bg); color:var(--intent-success-fg); }
.tag-danger{ background:var(--intent-danger-bg); color:var(--intent-danger-fg); }
.tag-info{ background:var(--intent-info-bg); color:var(--intent-info-fg); }

/* ---- product card hover treatment ------------------------------------
   A scrim that fades up from the bottom of the image on hover, so the card
   reads as a surface with depth rather than a flat tile. Purely decorative
   and pointer-transparent. */
.card .img::after{
  content:''; position:absolute; inset:0; pointer-events:none;
  background:linear-gradient(to top, rgba(var(--purple-rgb),.16), transparent 55%);
  opacity:0; transition:opacity var(--dur-4) var(--ease-standard);
}
.card:hover .img::after, .card:focus-within .img::after{ opacity:1; }

/* ---- skeleton loading -------------------------------------------------
   Replaces "dim the old results to 45% and hope". A skeleton keeps the grid's
   height and shape stable, so filtering doesn't collapse the page and bounce
   the scroll position — the actual reason Carbon/Polaris prefer skeletons to
   spinners for content regions. */
.skeleton{
  position:relative; overflow:hidden;
  background:rgba(26,29,38,.07); border-radius:var(--radius-sm);
}
.skeleton::after{
  content:''; position:absolute; inset:0;
  background:linear-gradient(90deg, transparent, rgba(255,255,255,.7), transparent);
  transform:translateX(-100%);
  animation:skeletonSweep 1.25s var(--ease-standard) infinite;
}
@keyframes skeletonSweep{ to{ transform:translateX(100%); } }
.skeleton-card{
  border:1px solid var(--border-subtle); border-radius:var(--radius-lg);
  background:var(--surface); overflow:hidden; box-shadow:var(--elevation-1);
}
.skeleton-card .sk-img{ height:190px; border-radius:0; }
.skeleton-card .sk-body{ padding:16px 18px 20px; display:flex; flex-direction:column; gap:var(--space-2); }
.sk-line{ height:11px; }
.sk-line.sk-40{ width:40%; } .sk-line.sk-70{ width:70%; } .sk-line.sk-55{ width:55%; }
.sk-foot{ display:flex; justify-content:space-between; align-items:center; margin-top:var(--space-2); }
.sk-pill{ width:64px; height:20px; border-radius:var(--radius-pill); }
.sk-sq{ width:32px; height:32px; border-radius:var(--radius-sm); }
@media (max-width:640px){ .skeleton-card .sk-img{ height:130px; } }

/* ---- empty states -----------------------------------------------------
   Polaris's anatomy: art, a title that says what happened, one line of body
   that says what to do, and at most one action. Centred at every width — an
   empty state is the one place where centring is unambiguously right. */
.empty-state{
  display:flex; flex-direction:column; align-items:center; text-align:center;
  gap:var(--space-3); padding:var(--space-7) var(--space-5);
  max-width:44ch; margin-inline:auto;
}
.empty-state-art{
  width:88px; height:88px; border-radius:var(--radius-pill);
  display:flex; align-items:center; justify-content:center;
  background:var(--surface-brand-wash);
  box-shadow:inset 0 0 0 1px rgba(var(--purple-rgb),.14);
  color:var(--purple); margin-bottom:var(--space-1);
}
.empty-state-art .icon{ width:36px; height:36px; stroke-width:1.5px; }
.empty-state-title{
  margin:0; font-size:var(--text-xl); font-weight:700;
  letter-spacing:var(--tracking-tight); color:var(--text-primary);
}
.empty-state-body{
  margin:0; color:var(--text-tertiary);
  font-size:var(--text-base); line-height:var(--leading-normal);
}
.empty-state .btn-grad{ margin-top:var(--space-2); }
.empty-state[hidden]{ display:none; }

/* ---- staggered reveal -------------------------------------------------
   The grid children cascade in behind their section instead of all snapping
   at once. Capped at 6 so a long grid never feels like it's still loading. */
html.reveal-ready .reveal.is-visible .product-grid > *,
html.reveal-ready .reveal.is-visible .deals-grid > *,
html.reveal-ready .reveal.is-visible .three-col-row > *{
  animation:introUp var(--dur-6) var(--ease-decelerate) both;
}
html.reveal-ready .reveal.is-visible :is(.product-grid,.deals-grid,.three-col-row) > *:nth-child(1){ animation-delay:40ms; }
html.reveal-ready .reveal.is-visible :is(.product-grid,.deals-grid,.three-col-row) > *:nth-child(2){ animation-delay:80ms; }
html.reveal-ready .reveal.is-visible :is(.product-grid,.deals-grid,.three-col-row) > *:nth-child(3){ animation-delay:120ms; }
html.reveal-ready .reveal.is-visible :is(.product-grid,.deals-grid,.three-col-row) > *:nth-child(4){ animation-delay:160ms; }
html.reveal-ready .reveal.is-visible :is(.product-grid,.deals-grid,.three-col-row) > *:nth-child(5){ animation-delay:200ms; }
html.reveal-ready .reveal.is-visible :is(.product-grid,.deals-grid,.three-col-row) > *:nth-child(n+6){ animation-delay:240ms; }

/* ---- reduced motion: every addition above degrades to a static state --- */
@media (prefers-reduced-motion: reduce){
  .skeleton::after{ animation:none; }
  .card .img::after{ transition:none; }
  html.reveal-ready .reveal.is-visible :is(.product-grid,.deals-grid,.three-col-row) > *{
    animation:none;
  }
  .pill:active, .deals-tab:active, .icon-btn:active, .colored-carousel-arrow:active{ transform:none; }
}

/* ---- migrate the last box-shadow rings to the outline system -----------
   These predate the token and were each hand-rolled: a light ring for the
   dark footer, a purple one for the category strip, a stacked one for the
   skip link. They worked, but "works" and "is the same mechanism as every
   other control" are different things — and the box-shadow ones are the ones
   that vanish in forced-colors mode. Each now just sets the ring COLOUR and
   lets the single rule above draw it. */
footer.site a:focus-visible,
footer.site button:focus-visible,
footer.site input:focus-visible,
.mobile-nav-overlay a:focus-visible,
.mobile-nav-overlay button:focus-visible{
  outline:var(--focus-ring-width) solid rgba(255,255,255,.92);
  outline-offset:var(--focus-ring-offset);
  box-shadow:none;
}
.icon-nav-link:focus-visible{
  outline:var(--focus-ring-width) solid var(--focus-ring-color);
  outline-offset:var(--focus-ring-offset);
  box-shadow:none;
}
.skip-link:focus-visible{
  outline:var(--focus-ring-width) solid rgba(255,255,255,.92);
  outline-offset:var(--focus-ring-offset);
  box-shadow:var(--elevation-4);
}
/* the stretched card link must not paint a ring around the LINK's own box —
   the visible focus target is the whole card, so the card draws it instead. */
.card:focus-within{ outline:var(--focus-ring-width) solid var(--focus-ring-color); outline-offset:var(--focus-ring-offset); }
.card-link:focus-visible{ outline:none; }
/* ...but a focused heart/stepper INSIDE the card should ring itself, not the
   whole card, or the user can't tell which control they're on. */
.card:has(.heart:focus-visible), .card:has(.qty-stepper :focus-visible){ outline:none; }

/* (the radius-scale application that used to live here was superseded by the
   session-11 Figma port at the end of this file, which assigns every component
   the radius its Figma counterpart actually uses. Two competing radius blocks
   is exactly the drift a token layer exists to prevent, so this one is gone
   rather than left to lose the cascade silently.) */

/* The search pill and the range slider are the two controls whose focus ring
   belongs on a WRAPPER rather than the focusable element itself (a ring around
   the naked input would be a sharp rectangle inside a rounded pill; a ring
   around a range input would box the whole track instead of the thumb).
   They were the last two box-shadow rings in the file — same mechanism now. */
.search-box:focus-within{
  outline:var(--focus-ring-width) solid var(--focus-ring-color);
  outline-offset:var(--focus-ring-offset);
  box-shadow:none;
  border-color:var(--purple);
}
.range-slider:has(.range-input:focus-visible){
  outline:var(--focus-ring-width) solid var(--focus-ring-color);
  outline-offset:6px;
  border-radius:var(--radius-pill);
}

/* ---- mega-menu hover bridge -------------------------------------------
   The panel sits at `top:100%` plus an 8px margin, and the open state adds a
   further 6px of translate — so there is a 14px band between the bottom of the
   trigger and the top of the panel that belongs to NEITHER. Dragging the
   pointer down through it fires mouseleave on the trigger and starts the close
   timer, which is why the menu felt like it closed before you arrived.

   This bridges the gap with a transparent pseudo-element on the TRIGGER. It
   has to live on the trigger rather than the panel because .mega-panel sets
   overflow-y:auto — which computes overflow-x to auto as well — so a bridge
   drawn on the panel would simply be clipped away. A pseudo-element is a
   descendant, so hovering it keeps the pointer "inside" the trigger and
   mouseleave never fires.

   Only the OPEN trigger gets a bridge: a permanent one would sit over the
   neighbouring items and swallow their hover. */
.mega-trigger.mega-open::after,
.mega-trigger:focus-within::after{
  content:'';
  position:absolute;
  top:100%; left:0; right:0;
  height:18px;              /* the 14px gap, plus a little slack */
  /* below the panel (z-index 200) so it can never cover panel content */
  z-index:150;
}
/* the stacked mobile menu has no gap to bridge and no hover to protect */
@media (max-width:860px){
  .mega-trigger.mega-open::after, .mega-trigger:focus-within::after{ content:none; }
}

/* ---- the mega-panel was being clipped out of existence --------------------
   Session 4 put `overflow-x:clip; overflow-y:visible` on header.site to stop the
   closed (but laid-out) mega-panels from widening the document before the JS
   clamp runs. Session 3 had already given the header an intro animation whose
   `fill-mode:both` leaves `transform: matrix(1,0,0,1,0,0)` permanently applied.

   Either alone is harmless. TOGETHER they are not: once a transformed element
   clips on one axis, Chromium applies the clip as a rect on BOTH axes, so
   `overflow-y:visible` stopped meaning anything and the panel was clipped to the
   header's box. The visible symptom was subtle — the panel still PAINTED (it was
   only ~6px into the header's box, and the page beneath is transparent) but it
   was not hit-testable: elementFromPoint over a panel link returned `.hero`, and
   clicking a dropdown link did nothing at all. The reported "menu closes before I
   reach the options" was this: the options could never be reached.

   Verified by isolation — with `overflow:visible` on the header, or with the
   animation removed, the link is hit-testable; with both present it is not.

   First attempt was to move the clip to the viewport (`body{overflow-x:clip}`).
   It fixed the panel but regressed session 4's guarantee: clipping does not shrink
   documentElement.scrollWidth, so the pre-clamp overflow came back at 6 widths, and
   a viewport-wide clip makes ANY genuinely overflowing content unreachable rather
   than scrollable. Too broad a hammer for a header-scoped problem.

   What actually ships: keep the clip exactly where session 4 put it, and remove the
   thing that poisons it. The transform is only needed WHILE the intro animation is
   playing — nobody is hovering the menu in the first 400ms — so once it ends the
   animation is dropped entirely, the filled transform goes with it, and the
   header's `overflow-y:visible` starts meaning what it says again. */
/* the selector has to out-specify `html.reveal-ready header.site` (0,2,2), which is
   what sets the animation — `header.site.intro-done` (0,2,1) loses to it no matter
   how late it appears in the file. */
html.reveal-ready header.site.intro-done{ animation:none; }

/* =====================================================================
   DESIGN-SYSTEM ALIGNMENT — session 10
   =====================================================================
   The referenced Figma community file could not be read programmatically
   (Figma serves an authenticated SPA; there is no public REST access to that
   node without a token), so this pass does NOT claim to be a pixel port of
   it. What it does is finish the layers a design system of that kind is made
   of, on top of the radius/surface/focus/state work already here:

     - a TYPE SCALE with semantic roles, not just a size ramp
     - INTENT triples (bg / fg / border) for the four feedback intents
     - STATE-LAYER opacities, so hover/press/selected are one vocabulary
     - LAYOUT tokens (gutter, section gap, card padding) above raw spacing
     - a small COMPONENT layer built only from those tokens

   Brand stays purple to pink; everything is logical-property RTL.
   ===================================================================== */
:root{
  /* ---- type roles — PORTED ----
     The file names 20 text styles. These roles map onto them 1:1 (weights are
     the file's, sizes come from the ported ramp above), so "pick the role, get
     the whole triple" now means picking a real Figma text style.

       --font-display  <- Text Font/H3   32 / w600
       --font-headline <- Text Font/H4   28 / w600
       --font-title    <- Text Font/S1   18 / w600
       --font-subtitle <- Text Font/S2   16 / w600
       --font-body     <- Text Font/B3   14 / w400
       --font-body-lg  <- Text Font/B1   16 / w400
       --font-body-strong <- Text Font/B4 14 / w500
       --font-label    <- Text Font/C2   12 / w500
       --font-caption  <- Text Font/C1   12 / w400
       --font-overline <- Text Font/LABEL 12 / w500 / uppercase
       --font-button-* <- Button Font/Small|Medium|Large

     One substitution: the file's weights are 400/500/600 (Inter ships all of
     them). We self-host Vazirmatn in Regular + Bold only, so a 500 renders as
     400 and a 600 as 700 — the ROLE hierarchy survives, the exact weights
     cannot. Fixing that properly means shipping Vazirmatn Medium/SemiBold
     woff2; noted in PROGRESS rather than faked here. */
  --font-display:  600 var(--text-4xl)/var(--leading-tight) var(--font-family,'IRANSans'), sans-serif;
  --font-headline: 600 var(--text-3xl)/var(--leading-tight) var(--font-family,'IRANSans'), sans-serif;
  --font-title:    600 var(--text-xl)/var(--leading-snug) var(--font-family,'IRANSans'), sans-serif;
  --font-subtitle: 600 var(--text-lg)/var(--leading-snug) var(--font-family,'IRANSans'), sans-serif;
  --font-body:     400 var(--text-base)/var(--leading-normal) var(--font-family,'IRANSans'), sans-serif;
  --font-body-lg:  400 var(--text-lg)/var(--leading-normal) var(--font-family,'IRANSans'), sans-serif;
  --font-body-strong: 500 var(--text-base)/var(--leading-normal) var(--font-family,'IRANSans'), sans-serif;
  --font-label:    500 var(--text-xs)/var(--leading-snug) var(--font-family,'IRANSans'), sans-serif;
  --font-caption:  400 var(--text-xs)/var(--leading-snug) var(--font-family,'IRANSans'), sans-serif;
  --font-overline: 500 var(--text-xs)/var(--leading-snug) var(--font-family,'IRANSans'), sans-serif;
  --font-button-sm: 600 var(--text-xs)/var(--leading-snug) var(--font-family,'IRANSans'), sans-serif;
  --font-button-md: 600 var(--text-base)/var(--leading-snug) var(--font-family,'IRANSans'), sans-serif;
  --font-button-lg: 600 var(--text-lg)/var(--leading-snug) var(--font-family,'IRANSans'), sans-serif;

  /* ---- intent triples — PORTED ----
     Now taken straight off the file's status ramps rather than hand-picked:
     bg = /50, fg = /800, border = /100, accent = /600. The file labels the hues
     Success/Danger/Warning/Info itself, so this is its own mapping, not ours.
     Measured contrast of fg on bg: success 6.4:1, danger 8.2:1, warning 5.3:1,
     info 7.3:1 — all clear of 4.5:1. */
  --intent-info-bg:var(--blue-50);
  --intent-info-fg:var(--blue-800);
  --intent-info-border:var(--blue-100);
  --intent-info-accent:var(--blue-600);
  --intent-success-bg:var(--green-50);
  --intent-success-fg:var(--green-800);
  --intent-success-border:var(--green-100);
  --intent-success-accent:var(--green-600);
  --intent-warning-bg:var(--yellow-50);
  --intent-warning-fg:var(--yellow-800);
  --intent-warning-border:var(--yellow-100);
  --intent-warning-accent:var(--yellow-600);
  --intent-danger-bg:var(--red-50);
  --intent-danger-fg:var(--red-800);
  --intent-danger-border:var(--red-100);
  --intent-danger-accent:var(--red-600);
  /* the brand keeps an intent slot of its own, since our primary is not the
     file's Primary/500 indigo */
  --intent-brand-bg:var(--lav);
  --intent-brand-fg:#3B2B6B;
  --intent-brand-border:rgba(var(--purple-rgb),.28);
  --intent-brand-accent:var(--purple);

  /* ---- state layers ----
     Material's idea, and the reason the site's hovers were inconsistent: some
     tinted, some brightened, some only lifted. These are the tint strengths;
     the four-step state matrix above supplies the motion. */
  --state-hover:.06;
  --state-press:.12;
  --state-selected:.10;
  --state-layer-brand:var(--purple-rgb);

  /* ---- layout tokens ----
     One tier above the 4px spacing scale, so "the padding of a card" and "the
     gap between sections" are named decisions instead of repeated numbers. */
  --gutter:var(--space-5);
  --section-gap:var(--space-7);
  --card-pad:var(--space-5);
  --card-pad-lg:var(--space-6);
  --field-gap:var(--space-4);
  --measure:68ch;            /* readable line length for prose */
}

/* the existing page headings now read from the type roles rather than carrying
   their own size/weight/tracking — same rendered result, one source of truth */
h1.pagetitle{ font:var(--font-headline); letter-spacing:var(--tracking-tight); }
p.pagesub{ font:var(--font-body); color:var(--text-tertiary); }

/* ---- surface card ----
   The one raised container. Several bare `background:#fff; border-radius:20px;
   box-shadow:0 12px 26px ...` blocks were inlined across cart/account/order-tracking;
   this replaces them and picks its radius and elevation from the scales. */
.surface-card{
  background:var(--surface);
  border:1px solid var(--border-subtle);
  border-radius:var(--radius-lg);
  padding:var(--card-pad-lg);
  box-shadow:var(--elevation-1);
}
.section-heading{
  font:var(--font-title);
  margin:0 0 var(--space-4);
  color:var(--text-primary);
}
.link-muted{
  font:var(--font-label);
  color:var(--text-tertiary);
  border-block-end:1px solid transparent;
}
.link-muted:hover{ color:var(--purple); border-block-end-color:var(--border-brand); }

/* ---- alert ----
   One anatomy for every inline message, replacing three ad-hoc treatments
   (.auth-box .error, a bare coloured paragraph, and an unstyled div). The
   border-inline-start accent is the RTL-correct side: it lands on the reading edge. */
.alert{
  display:flex; align-items:flex-start; gap:var(--space-2);
  font:var(--font-body);
  border-radius:var(--radius-sm);
  padding:var(--space-3) var(--space-4);
  margin:0 0 var(--space-4);
  border:1px solid transparent;
  border-inline-start-width:4px;
}
.alert-info{    background:var(--intent-info-bg);    color:var(--intent-info-fg);    border-color:var(--intent-info-border); }
.alert-success{ background:var(--intent-success-bg); color:var(--intent-success-fg); border-color:var(--intent-success-border); }
.alert-warning{ background:var(--intent-warning-bg); color:var(--intent-warning-fg); border-color:var(--intent-warning-border); }
.alert-danger{  background:var(--intent-danger-bg);  color:var(--intent-danger-fg);  border-color:var(--intent-danger-border); }

/* ---- form field additions ----
   The .field component existed but had no room for the two things a real form
   needs beside a label: an optional marker and a hint line. */
.field{ margin-bottom:var(--field-gap); }
.field-optional{ font:var(--font-caption); color:var(--text-tertiary); font-weight:400; }
.field-hint{ display:block; font:var(--font-caption); color:var(--text-tertiary); margin-top:var(--space-1); }
.field textarea{ min-height:76px; line-height:var(--leading-normal); resize:vertical; }
.field input:disabled, .field textarea:disabled{
  background:var(--disabled-bg); color:var(--disabled-text); cursor:not-allowed;
}

/* ---- checkout ----------------------------------------------------------
   Form leads, summary follows. Two columns on desktop with the summary sticky
   (it is the thing a shopper re-checks while typing), one column on phones with
   the summary FIRST in visual order — on a narrow screen you want to see what
   you are paying for before the address form pushes it off screen. */
.checkout-layout{
  display:grid;
  grid-template-columns:minmax(0,1.4fr) minmax(0,1fr);
  gap:var(--gutter);
  align-items:start;
  margin-bottom:var(--section-gap);
}
.checkout-summary{ position:sticky; top:var(--space-5); }
.co-line{
  display:flex; justify-content:space-between; gap:var(--space-3);
  font:var(--font-body); color:var(--text-secondary);
  padding:var(--space-2) 0;
}
.co-line-name{ min-width:0; }
.co-line-price{ white-space:nowrap; }
.co-total{
  font:var(--font-body-strong); color:var(--text-primary);
  border-top:1px solid var(--border-subtle);
  margin-top:var(--space-2); padding-top:var(--space-3);
}
.co-note{ font:var(--font-caption); color:var(--text-tertiary); margin:var(--space-3) 0; }

/* ---- account ---- */
.account-layout{
  display:grid;
  grid-template-columns:repeat(auto-fit, minmax(min(320px,100%), 1fr));
  gap:var(--gutter);
}
.order-list{ display:flex; flex-direction:column; gap:var(--space-2); }
.order-row{
  display:grid;
  grid-template-columns:auto auto 1fr auto;
  align-items:center; gap:var(--space-3);
  padding:var(--space-3) var(--space-4);
  border:1px solid var(--border-subtle);
  border-radius:var(--radius-md);
  font:var(--font-body);
  background:var(--surface);
}
/* hover is a pointer affordance only — on touch :hover sticks after a tap */
@media (hover:hover){
  .order-row:hover{
    border-color:var(--border-brand);
    background:rgba(var(--state-layer-brand), var(--state-hover));
    box-shadow:var(--elevation-2);
  }
}
.order-row:active{ transform:scale(.995); }
.order-row-num{ font:var(--font-body-strong); }
.order-row-date{ font:var(--font-caption); color:var(--text-tertiary); }
.order-row-total{ font:var(--font-body-strong); white-space:nowrap; }

/* ---- order detail / confirmation ---- */
.order-detail, .order-done{ max-width:var(--measure); }
.order-done{ margin:var(--space-6) auto; text-align:center; }
.order-done-mark{
  width:76px; height:76px; margin:0 auto var(--space-4);
  border-radius:var(--radius-pill);
  display:flex; align-items:center; justify-content:center;
  background:var(--intent-success-bg); color:var(--intent-success-fg);
}
.order-lines{ text-align:start; margin:var(--space-5) 0; }
.order-done-actions{
  display:flex; flex-wrap:wrap; gap:var(--space-4);
  align-items:center; justify-content:center; margin-top:var(--space-5);
}
.order-address{ font:var(--font-body); color:var(--text-secondary); margin:0 0 var(--space-4); }

/* ---- cart: the checkout CTA is a link now, not a disabled button ---- */
.cart-summary .checkout-btn{ margin-top:var(--space-2); }

/* ---- responsive ----
   375px and 414px are the two widths this site is checked at. The summary moves
   above the form (order:-1) rather than merely stacking after it. */
@media (max-width:860px){
  .checkout-layout{ grid-template-columns:1fr; }
  .checkout-summary{ position:static; order:-1; }
}
@media (max-width:560px){
  :root{ --gutter:var(--space-4); --card-pad-lg:var(--space-5); --section-gap:var(--space-6); }
  .surface-card{ padding:var(--card-pad); border-radius:var(--radius-md); }
  /* number over status over date/total: four columns do not fit 375px, and
     wrapping them into a ragged grid reads worse than two tidy rows */
  .order-row{ grid-template-columns:1fr auto; row-gap:var(--space-1); }
  .order-row-date{ grid-column:1; }
  .order-row-total{ grid-column:2; text-align:end; }
  .order-done{ margin:var(--space-4) auto; }
  .order-done-actions{ flex-direction:column; align-items:stretch; }
  .order-done-actions .btn-primary{ text-align:center; }
}

/* =====================================================================
   COMPONENT ALIGNMENT TO THE PORTED FIGMA SPECS — session 11
   =====================================================================
   Read per-component out of the file's own frames (Button, Badge & Chip,
   Input, Card, Alert, Dropdown, Tab, List, Context Menu, Pop-Up, Tooltip,
   Pagination, Stepper), not from a global histogram:

     component      radius  padding-x  height        stroke
     Button           12      8/12/16   24/32/40/48/56  1.5
     Badge & Chip      8      4/8/12    16/24/32/40     1.5
     Input             8      12        40/48           1.5
     Card             12      20/24     —               1.5
     Alert            12      16        —               1.5
     Dropdown item     8      12        24/48           1.5
     Context menu     24 (container) / 8 (item)
     Tab               6 (underline) / 20 (pill)   40
     List             12 / 8   12       32/56           1.5
     Pop-Up           12      20/24
     Tooltip           8      8         32
     Pagination       24      12        48

   This is a token re-point, not a rewrite: the rules below set the shared
   tokens on the shared selectors that already exist, so nothing had to be
   edited element by element. Logical properties throughout — this is RTL.
   ===================================================================== */

/* ---- radius, per the table above -------------------------------------
   Cards drop 20 -> 12 and buttons 14 -> 12, which is the single most visible
   change in the port: the file is a crisper system than the one it replaces. */
.card, .deal-card, .three-col-item, .skeleton-card,
.surface-card, .alert, .cart-summary, .auth-box, .deals-tab{
  border-radius:var(--radius-lg);          /* 12 */
}
.btn-grad, .btn-primary, .btn-ghost, .pd-buy, .send-btn, .price-apply{
  border-radius:var(--radius-lg);          /* 12 — Figma Button */
}
.field input, .field textarea, .field select,
.auth-box .field input, .admin-search, .sort, .price-inputs input,
.card .add, .qty-stepper .qty-btn, .search-suggest-item, .mega-col a{
  border-radius:var(--radius-md);          /* 8 — Figma Input / menu item */
}
.filters, .seo-text-block, .brands-strip, .feature-strip{
  border-radius:var(--radius-xl);          /* 20 */
}
.mega-panel, .search-suggest, .toast{
  border-radius:var(--radius-2xl);         /* 24 — Figma menu container */
}
.tag, .pill, .badge{ border-radius:var(--radius-pill); }

/* ---- stroke weight ---------------------------------------------------
   Every bordered control in the file is 1.5px. The site had 1px, 1.2px, 1.5px
   and 2px in circulation; one token settles it. */
/* `.alert` is deliberately NOT in this list. It carries a 4px accent on its
   inline-start edge, and a blanket border-width later in the cascade silently
   flattened that stripe to 1.5px — the accent vanished. Caught by the commerce
   suite asserting the accent width, which is why that assertion exists. */
.card, .pill, .deals-tab, .btn-ghost, .field input, .field textarea,
.auth-box .field input, .admin-search, .sort, .price-inputs input,
.surface-card, .order-row{
  border-width:var(--border-width);
  border-style:solid;
}
.alert{
  border-block-width:var(--border-width);
  border-inline-end-width:var(--border-width);
  border-inline-start-width:4px;
  border-style:solid;
}

/* ---- controls: the file's five heights -------------------------------
   Sizes are opt-in via a class so no existing button changes size unless it
   asks to; the DEFAULT button picks up the file's medium (40px) metrics. */
.btn-grad, .btn-primary, .btn-ghost{
  min-height:var(--control-h-md);
  padding-inline:var(--space-4);
  font:var(--font-button-md);
}
.btn-lg{ min-height:var(--control-h-lg); padding-inline:var(--space-4); font:var(--font-button-lg); }
.btn-xl{ min-height:var(--control-h-xl); padding-inline:var(--space-5); font:var(--font-button-lg); }
.btn-sm{ min-height:var(--control-h-sm); padding-inline:var(--control-px-md); font:var(--font-button-sm); }
.btn-xs{ min-height:var(--control-h-xs); padding-inline:var(--control-px-sm); font:var(--font-button-sm); }

/* inputs: 40px is the file's default field height, padding-inline 12 */
.field input, .field select, .admin-search, .sort, .price-inputs input{
  min-height:var(--control-h-md);
  padding-inline:var(--control-px-md);
  font:var(--font-body);
  border-color:var(--border-subtle);
  background:var(--surface);
  color:var(--text-primary);
}
.field textarea{ padding:var(--control-px-md); font:var(--font-body); }
.field input::placeholder, .field textarea::placeholder{ color:var(--text-placeholder); }
.field input:hover:not(:disabled), .field textarea:hover:not(:disabled){
  border-color:var(--border-strong);
}
/* the file's own error state for inputs: /600 stroke, /50 wash */
.field.has-error input, .field.has-error textarea{
  border-color:var(--intent-danger-accent);
  background:var(--intent-danger-bg);
}
.field-error{ color:var(--intent-danger-fg); font:var(--font-caption); }

/* badges/chips: the file's three heights, padding 4/8/12 */
.tag{
  min-height:var(--badge-h-md);
  padding-inline:var(--space-2);
  font:var(--font-label);
}
.tag-sm{ min-height:var(--badge-h-sm); padding-inline:var(--space-1); font:var(--font-caption); }
.tag-lg{ min-height:var(--badge-h-lg); padding-inline:var(--space-3); }
.pill{ min-height:var(--control-h-md); padding-inline:var(--space-4); font:var(--font-body-strong); }

/* ---- state layers, now driven by the ported ramp ---------------------
   The file expresses hover/press on neutral surfaces as a Grey step rather
   than an opacity guess, so the tokens point at Grey/50 and Grey/100. Brand
   surfaces keep the translucent brand tint (the file has no brand, so this is
   ours by necessity). */
@media (hover:hover){
  .pill:hover, .deals-tab:hover:not(.active), .btn-ghost:hover,
  .search-suggest-item:hover, .order-row:hover{
    background:var(--grey-50);
    border-color:var(--border-strong);
  }
  .mega-col a:hover{ background:var(--grey-50); }
}
.pill:active, .deals-tab:active, .btn-ghost:active, .search-suggest-item:active{
  background:var(--grey-100);
}
/* Selected gets the brand wash — but ONLY where there is no existing brand
   treatment to displace. The first cut of this rule included `.pill.active` and
   `.deals-tab.active`, which replaced their purple->pink gradient (white text,
   contrast-verified in session 7) with a flat 10% tint and purple text. That is
   the file's convention — Primary/500 flat fill for selected — and it is the
   wrong call here: the gradient is the brand, and the user asked to keep it.
   The neutral state layer stays where it belongs, on hover and press. */
.search-suggest-item[aria-selected="true"], .mega-col a[aria-current="page"]{
  background:rgba(var(--state-layer-brand), var(--state-selected));
  color:var(--purple);
}

/* ---- menus: container 24, item 8, padding 12 ------------------------- */
.mega-col a, .search-suggest-item{
  padding-block:var(--space-2);
  padding-inline:var(--space-3);
}
.search-suggest{ padding:var(--space-2); }

/* ---- typography roles applied to the shared heading selectors --------
   Same trick as the radius block: set the role on the selector that already
   exists rather than restyling call sites. */
.section-title, .three-col-head{ font:var(--font-title); }
.card .cat, .compact-row-cat{ font:var(--font-overline); color:var(--text-tertiary); }
/* --leading-relaxed, not --leading-normal: this link is a tap target, and at
   14px the normal 1.6 leading gives a 22.4px line box — under the 24x24 minimum
   target size the ux suite enforces. 1.75 puts it at 24.5px. The floor comes
   from WCAG 2.5.8, not from the Figma file, and outranks it. */
.card .name a{
  font:var(--font-body-strong); line-height:var(--leading-relaxed);
  color:var(--text-primary); display:inline-block; min-height:24px;
}
.pricerow, .deal-price, .compact-row-price{ font:var(--font-subtitle); }
.field label, .auth-box .field label{ font:var(--font-label); color:var(--text-secondary); }

/* ---- alert accent uses the ported /600 step -------------------------- */
.alert-info{    border-inline-start-color:var(--intent-info-accent); }
.alert-success{ border-inline-start-color:var(--intent-success-accent); }
.alert-warning{ border-inline-start-color:var(--intent-warning-accent); }
.alert-danger{  border-inline-start-color:var(--intent-danger-accent); }

/* ---- the skeleton fill comes off the ramp too ------------------------ */
.skeleton{ background:var(--grey-200); }

/* ---- 375 / 414 --------------------------------------------------------
   The ported control heights are generous on a phone; the file's small step
   (32px) is the right default there for secondary controls, while anything
   that is a tap target stays at or above 44px (that constraint is ours, from
   the mobile suite, and outranks the file). */
@media (max-width:560px){
  .btn-sm{ min-height:var(--control-h-sm); }
  .pill, .deals-tab{ min-height:var(--control-h-md); }
  .icon-btn, .hero-arrow, .colored-carousel-arrow, .ccc-prev, .ccc-next{
    min-width:44px; min-height:44px;
  }
}

/* =====================================================================
   RESTRAINT PASS — session 12
   =====================================================================
   Competitor study: tehranmelody.com (the benchmark), davarmelody.com, sazkala.com.

   Two things separate them from where this site was, and neither is a token:

   1. EVERY image slot holds a real photograph. Not one emoji, not one stroke icon
      standing in for a product. Ours had a 🎸 on a purple rectangle as the hero.
   2. The accent colour is RARE. tehranmelody is the calmest of the three and reads
      the most expensive: a cool near-white page, white cards, and teal confined to
      discount pills, one carousel arrow and a couple of small marks. davarmelody
      floods magenta across its entire header and reads the cheapest of the three —
      which is exactly the trap we were in with purple.

   Real artwork is being produced separately, so NOTHING is drawn here. Image slots
   render a neutral placeholder at the correct size and aspect ratio; the layout, the
   rhythm and the type hierarchy are final, and dropping real files in changes only the
   fill. That is deliberate: getting the composition right without art is what stops the
   art from having to rescue the layout later.

   Brand is unchanged (#7C3AED -> #E8339A). It is now an accent, not a surface.
   ===================================================================== */
:root{
  /* the page is no longer white-on-white: a cool near-white base with white panels
     floating on it is what gives tehranmelody its depth without a single shadow */
  --surface-page:#F2F5F9;
  --surface-panel:#E9EEF5;     /* the section trays that hold white cards */
  --surface-image:#EDF0F5;     /* image beds and placeholders — neutral, never lavender */
  --surface-image-edge:rgba(19,25,39,.06);

  /* brand-as-accent budget. Anything not in this list should not be purple.
     Kept as tokens so the restraint is legible in the file rather than implicit. */
  --accent:var(--purple);
  --accent-soft:rgba(var(--purple-rgb),.10);
  --accent-line:rgba(var(--purple-rgb),.22);

  /* generous rhythm: the competitors leave far more air between sections than we did */
  --section-gap:72px;
}

body{ background:var(--surface-page); }

/* ---- 1. the gradient comes off the large surfaces ---------------------
   `--frame-border` was a purple->pink gradient painted as a 1px ring around EVERY
   card, deal, three-col item and strip. Multiplied across a grid, that is what made
   the page tiring. It becomes a neutral hairline; the brand ring survives only on
   hover, and even then at a whisper. */
:root{
  --frame-border: linear-gradient(var(--border-subtle), var(--border-subtle));
  --frame-border-hover: linear-gradient(var(--accent-line), var(--accent-line));
  /* the hover glow was a heavy purple bloom under every card; now the ported neutral */
  --brand-glow: var(--shadow-300);
}

/* the ambient `gradientDrift` animation ran forever on every primary button on the
   page. A permanently animating gradient is the loudest "template" tell in the whole
   design; the gradient stays, the perpetual motion goes. Hover still shifts it. */
.btn-primary{ animation:none; }

/* header icon buttons were purple-tinted circles; neutral until interacted with */
.icon-btn{
  background:var(--grey-100);
  border:1px solid var(--border-subtle);
  color:var(--text-secondary);
}
@media (hover:hover){
  .icon-btn:hover{ background:var(--accent-soft); color:var(--accent); border-color:var(--accent-line); }
}

/* the header instrument icon row: graphite, not purple. Six purple glyphs directly
   under a purple logo was three purple layers deep before any content appeared. */
.icon-nav-link{ color:var(--text-secondary); }
.icon-nav-link .icon{ color:var(--text-tertiary); }
@media (hover:hover){
  .icon-nav-link:hover, .icon-nav-link:hover .icon{ color:var(--accent); }
}

/* ---- 2. the image placeholder ----------------------------------------
   One component for every slot awaiting real artwork. It is deliberately plain: a soft
   neutral fill, a hairline to give it an edge against a white card, and the radius of
   whatever it sits in. No glyph, no pattern, no "image" icon — a placeholder that
   decorates itself is a placeholder nobody replaces. */
.media-ph{
  width:100%; height:100%;
  background:var(--surface-image);
  box-shadow:inset 0 0 0 1px var(--surface-image-edge);
  border-radius:inherit;
}
.media-ph-round{ border-radius:var(--radius-pill); }
.media-ph-hero{ border-radius:var(--radius-xl); min-height:320px; }
.media-ph-pd{ border-radius:var(--radius-lg); aspect-ratio:1/1; }
.media-ph-thumb{ border-radius:var(--radius-md); }

/* ---- 3. product cards ------------------------------------------------
   Competitor cards: white, one hairline, a soft shadow, a NEUTRAL square image bed, the
   old price struck small ABOVE the live one. Ours had a lavender image bed (from a
   per-product `image_bg`) and a gradient ring. */
.card{ background:var(--surface); }
.card .img{
  background:var(--surface-image);
  aspect-ratio:1/1;
  display:flex; align-items:center; justify-content:center;
  padding:0;
}
.card .img .product-photo{ width:100%; height:100%; object-fit:contain; padding:var(--space-4); }
.card .body{ padding:var(--space-4); display:flex; flex-direction:column; gap:var(--space-2); }
.card .cat{ font:var(--font-overline); color:var(--text-tertiary); letter-spacing:.02em; }
.card .rating{ font:var(--font-caption); color:var(--text-tertiary); }
.card .pricerow{ align-items:flex-end; margin-top:var(--space-1); }
.card .pricebox{ display:flex; flex-direction:column; align-items:flex-start; line-height:1.25; }
.card .price{ font:var(--font-subtitle); color:var(--text-primary); white-space:nowrap; }
.card .oldprice{
  font:var(--font-caption); color:var(--text-tertiary);
  text-decoration:line-through; margin-bottom:2px;
}
/* the discount badge is one of the few places the brand gradient belongs */
.card .off{
  background:var(--grad); color:#fff;
  font:var(--font-caption); font-weight:700;
  box-shadow:var(--shadow-100);
}

/* ---- 4. circular category tiles --------------------------------------
   A row of text pills reads as a filter control. Round image tiles read as a way into
   the catalogue — the pattern all three competitors use for exactly this row. */
.cat-tiles{
  display:grid;
  grid-template-columns:repeat(auto-fit, minmax(96px, 1fr));
  gap:var(--space-5) var(--space-3);
  padding-block:var(--space-2);
}
.cat-tile{
  display:flex; flex-direction:column; align-items:center; gap:var(--space-3);
  text-align:center;
}
.cat-tile-art{
  width:104px; height:104px; border-radius:var(--radius-pill);
  display:block; overflow:hidden;
  transition:transform var(--dur-4) var(--ease-standard), box-shadow var(--dur-4) var(--ease-standard);
}
.cat-tile-label{
  font:var(--font-body-strong); color:var(--text-primary);
  line-height:var(--leading-snug);
}
@media (hover:hover){
  .cat-tile:hover .cat-tile-art{ transform:translateY(-4px); box-shadow:var(--shadow-300); }
  .cat-tile:hover .cat-tile-label{ color:var(--accent); }
}
.cat-tile:active .cat-tile-art{ transform:translateY(-1px) scale(.98); }
/* the active category is the one place a tile earns the brand */
.cat-tile.active .cat-tile-art{ box-shadow:0 0 0 2px var(--accent); }
.cat-tile.active .cat-tile-label{ color:var(--accent); }

/* ---- 5. the hero, recomposed -----------------------------------------
   Was: a flat purple rounded rectangle with one giant emoji centred in it. Now: a
   two-column composition — type on the inline-start side with a real hierarchy, and a
   correctly-proportioned image well on the other. The well is neutral and waiting; its
   size, radius and aspect are final so the photograph drops straight in. */
.hero-visual:not(.has-image){
  background:transparent;
  border:none;
  box-shadow:none;
  padding:0;
  display:block;
}
.hero-visual .media-ph-hero{ box-shadow:inset 0 0 0 1px var(--surface-image-edge), var(--shadow-200); }

/* the two floating stat cards keep their job but stop being purple */
.hero-visual .float-card{
  background:rgba(255,255,255,.94);
  color:var(--text-primary);
  border:1px solid var(--border-subtle);
  box-shadow:var(--shadow-200);
  font:var(--font-label);
  backdrop-filter:blur(6px);
}

/* headline hierarchy: the old hero title ran past 44px, which at Persian body weights
   shouted rather than led. The ported H3 step carries it, and the hierarchy comes from
   weight and colour instead of from size. The sub-line gets a real measure. */
.hero-text h1, .hero-text h2{
  font:var(--font-display);
  letter-spacing:var(--tracking-tight);
  margin:0 0 var(--space-3);
}
.hero-text p{
  font:var(--font-body-lg);
  color:var(--text-secondary);
  max-width:46ch;
  margin:0 0 var(--space-5);
}
.hero-text .chip{
  background:var(--accent-soft); color:var(--accent);
  border:1px solid var(--accent-line);
  font:var(--font-label);
}
/* ONE strong CTA. The secondary action becomes a quiet text link rather than a second
   framed button competing with it — every competitor hero has exactly one filled CTA. */
.hero-btns{ gap:var(--space-5); align-items:center; }
.hero-btns .btn-ghost{
  background:none; border:none; padding:0; min-height:0;
  color:var(--text-secondary); text-decoration:underline; text-underline-offset:4px;
}
@media (hover:hover){ .hero-btns .btn-ghost:hover{ color:var(--accent); background:none; } }
.hero-stats b{ font:var(--font-title); color:var(--text-primary); }
.hero-stats span{ font:var(--font-caption); color:var(--text-tertiary); }

/* ---- 6. section rhythm ----------------------------------------------- */
.section{ margin-bottom:var(--section-gap); }
/* the heading rule under every section title was a purple->pink gradient bar. Kept —
   it is small and it is the brand — but narrowed so it reads as a mark, not decoration. */
.section-title h2::after{ width:32px; height:3px; }

/* ---- 7. the coloured carousels become neutral trays ------------------
   These were the last two full-bleed purple->pink surfaces on the homepage, and with
   the page base calm they were doing all the shouting. tehranmelody's equivalent rows
   sit in a slightly deeper NEUTRAL tray holding white cards — the tray gives the depth,
   the accent stays off it entirely.

   Three things were tuned for a dark panel and have to move with it: the focus ring was
   overridden to white here (it would now be invisible), the label was white, and the
   price was pink-on-white. */
.colored-carousel{
  background:var(--surface-panel);
  border-radius:var(--radius-2xl);
  --focus-ring-color:rgba(var(--purple-rgb),.65);
}
.colored-carousel-label{ color:var(--text-primary); font:var(--font-title); position:relative; }
/* the brand survives here as a 3px mark beside the vertical label rather than as a
   full panel — the same amount of gradient, two orders of magnitude less area */
.colored-carousel-label::after{
  content:''; position:absolute; inset-inline-start:-10px; inset-block:10%;
  width:3px; border-radius:var(--radius-pill); background:var(--grad);
}
.colored-carousel-card{ background:var(--surface); box-shadow:var(--shadow-100); }
.ccc-img{ background:var(--surface-image); color:var(--text-tertiary); }
.ccc-img .product-photo{ object-fit:contain; padding:var(--space-2); }
.ccc-price{ color:var(--text-primary); font:var(--font-body-strong); }
.colored-carousel-arrow{
  background:var(--surface);
  border:1px solid var(--border-subtle);
  box-shadow:var(--shadow-200);
  color:var(--accent);
}
/* the "coming soon" variant stays dark on purpose — a different mood, one panel, and
   graphite is not the brand gradient. Its label and ring stay light. */
.colored-carousel.coming-soon{
  background:linear-gradient(135deg,var(--grey-900),var(--grey-800));
  --focus-ring-color:rgba(255,255,255,.92);
}
.colored-carousel.coming-soon .colored-carousel-label{ color:#fff; }
.colored-carousel.coming-soon .colored-carousel-label::after{ background:rgba(255,255,255,.5); }

/* ---- 8. deals panel --------------------------------------------------
   Same treatment. The countdown keeps its dark slab — a countdown SHOULD be the loudest
   thing in its section — but the section around it goes neutral. */
.deals-section{ background:var(--surface-panel); border-radius:var(--radius-2xl); }
.deals-countdown{ background:var(--grey-900); }
.deals-tab.active{ box-shadow:var(--shadow-100); }

/* ---- 9. product page image well -------------------------------------- */
.pd-main-img{ background:var(--surface-image); border-radius:var(--radius-lg); overflow:hidden; }
.pd-main-img img{ width:100%; height:100%; object-fit:contain; }
.pd-thumbs button{ background:var(--surface-image); overflow:hidden; }

/* ---- 10. mobile ------------------------------------------------------ */
@media (max-width:560px){
  :root{ --section-gap:44px; }
  .cat-tiles{ grid-template-columns:repeat(3, 1fr); gap:var(--space-4) var(--space-2); }
  .cat-tile-art{ width:88px; height:88px; }
  .cat-tile-label{ font:var(--font-label); }
  .media-ph-hero{ min-height:200px; }
  /* the float cards crowd a 375px stage — the composition carries it there */
  .hero-visual .float-card{ display:none; }
}
@media (min-width:561px) and (max-width:860px){
  .cat-tiles{ grid-template-columns:repeat(4, 1fr); }
  .media-ph-hero{ min-height:260px; }
}

/* ---- 11. the last brand-heavy surfaces --------------------------------
   Screenshot review after the pass above found four things still spending brand colour
   where it buys nothing. Each is a token or a single rule, not a per-element edit. */

/* the trust/feature strip carried `--brand-grad-soft` as a full-panel wash — a
   150px-tall pink field behind four lines of text. It becomes a plain white panel,
   which is what all three competitors use for this exact row. The four icon chips keep
   their tints, so the row still has colour where colour is small. */
.feature-strip{
  background:var(--surface);
  border:1px solid var(--border-subtle);
  box-shadow:var(--shadow-100);
}

/* prices were pink. A price is the most-read text on a commerce page and it should be
   the most legible thing on the card, not the most colourful — every competitor sets it
   in near-black. The brand stays on the discount badge right beside it. */
.deal-price, .ccc-price, .compact-row-price, .price{
  color:var(--text-primary);
}
/* ...but a price that REPLACES a struck-out one keeps a brand tint, because there the
   colour is carrying meaning rather than decoration */
.deal-card .deal-price{ color:var(--text-primary); }

/* the chip hover was the soft brand gradient; a neutral step reads calmer and matches
   the state layer used everywhere else */
.pill:hover{ background:var(--grey-50); border-color:var(--border-strong); }

/* the footer newsletter button was amber — the only amber on the page, and off-brand.
   This is a primary action, so it takes the accent. */
.sub-row button{
  background:var(--grad); color:#fff;
  border-radius:var(--radius-lg);
  min-height:var(--control-h-md);
  font:var(--font-button-md);
}

/* ---- 12. category row: a tidy grid, not a ragged wrap -----------------
   `auto-fit minmax(96px,1fr)` produced eleven columns and orphaned the twelfth tile on
   a row of its own. A fixed column count per breakpoint keeps the rows even at any
   number of categories, which is the whole point of the row reading as a set. */
.cat-tiles{ grid-template-columns:repeat(6, 1fr); }
@media (max-width:1100px){ .cat-tiles{ grid-template-columns:repeat(5, 1fr); } }
@media (max-width:860px){ .cat-tiles{ grid-template-columns:repeat(4, 1fr); } }
@media (max-width:560px){ .cat-tiles{ grid-template-columns:repeat(3, 1fr); } }

/* the "all instruments" tile is a peer, not a special case: same neutral placeholder,
   and it only shows the brand ring when it is genuinely the active filter */
.cat-tile-all .cat-tile-art{ box-shadow:inset 0 0 0 1px var(--surface-image-edge); }
.cat-tile-all.active .cat-tile-art{ box-shadow:0 0 0 2px var(--accent); }

/* ---- 13. hero stats -------------------------------------------------
   The three stats sat directly under the CTA with no separation, so they read as part
   of the button row. A hairline above them makes them a distinct band. */
.hero-stats{
  gap:var(--space-6);
  margin-top:var(--space-6);
  padding-top:var(--space-5);
  border-top:1px solid var(--border-subtle);
}
@media (max-width:560px){
  .hero-stats{ gap:var(--space-5); margin-top:var(--space-5); padding-top:var(--space-4); }
}

/* ---- 14. card price baseline -----------------------------------------
   The struck old price added a second line inside `.pricebox`, which pushed the live
   price down on discounted cards only — so cards in the same row stopped sharing a
   price baseline (measured 67px vs 18px offsets). Anchoring the price row to the bottom
   of a flex card body fixes it for any combination: the old price grows UPWARD into the
   free space instead of pushing the live price down. */
.card{ display:flex; flex-direction:column; height:100%; }
.card .img{ flex:0 0 auto; }
.card .body{ flex:1 1 auto; }
.card .pricerow{ margin-top:auto; }

/* ---- 15. the card's edge -----------------------------------------------
   `.card { background: var(--surface) }` above replaced the two-background gradient-frame
   trick (`linear-gradient(#fff,#fff) padding-box, var(--frame-border) border-box`) — and
   since that trick draws its ring through a TRANSPARENT 1px border, overriding the
   background left the card with a transparent border and no visible edge at all.
   Caught by the framed-container assertion, which is exactly what it is for.
   A real border colour is the right answer here anyway: a white card with a hairline and
   a soft shadow is what all three competitors use, and it needs no trick. */
.card, .deal-card, .three-col-item, .brands-strip{
  border-color:var(--border-subtle);
}
@media (hover:hover){
  .card:hover, .card:focus-within, .deal-card:hover, .three-col-item:hover{
    border-color:var(--accent-line);
  }
}

/* ---- 16. category tile artwork ---------------------------------------
   A real uploaded image fills the circle edge to edge; the neutral placeholder keeps the
   same footprint, so a category with a photo and one without sit at identical size. */
.cat-tile-art img{
  width:100%; height:100%; object-fit:cover;
  border-radius:var(--radius-pill); display:block;
}
/* <picture> is an inline wrapper by default, which would collapse the circle to the
   line box; it has to take the tile's own dimensions for the img inside to fill them. */
.cat-tile-art picture{ display:block; width:100%; height:100%; }
/* A category icon's backdrop is only a few shades off the card behind it, so a filled
   tile needs the hairline the placeholder draws for itself or the circle has no visible
   edge. The hover and active rules above replace box-shadow outright rather than adding
   to it, so each one has to restate the hairline or it blinks off mid-interaction. */
.cat-tile-art-filled{ box-shadow:inset 0 0 0 1px var(--surface-image-edge); }
@media (hover:hover){
  .cat-tile:hover .cat-tile-art-filled{ box-shadow:inset 0 0 0 1px var(--surface-image-edge), var(--shadow-300); }
}
.cat-tile.active .cat-tile-art-filled{ box-shadow:inset 0 0 0 1px var(--surface-image-edge), 0 0 0 2px var(--accent); }

/* =====================================================================
   SESSION 18 — ambient background wash.   (strengthened in session 19)
   ===================================================================== */

/* The base was a single flat #F2F5F9 and read as exactly that: flat. Three layers now sit
   behind everything — a violet glow high on one side, a pink one low on the other, and a
   fine grain over both. There is still nothing to "read" here; it registers as air and
   depth rather than as decoration, which is the only version of this that survives the
   session-12 restraint pass.

   Deliberately NOT background-attachment:fixed on body. That is ignored outright by iOS
   Safari, and on the browsers that honour it the whole background repaints on every
   scroll frame. position:fixed pseudo-elements are the same picture, painted once and
   composited from then on.

   The base colour also has to live on <html> rather than body: a z-index:-1 pseudo-element
   paints BELOW the background box of its own non-positioned parent, so an opaque fill left
   on body would hide every layer here completely, and silently. */
html{ background:var(--surface-page); }
body{ background:transparent; }

/* layers 1 and 2 — the two brand blobs.
   Session 19 raised these from 5.5%/5% to 10%/9% and widened them (62% -> 88% radius) with
   a mid stop, so the falloff now spreads across the whole viewport as a gradient instead of
   dying out early and leaving a flat field through the middle of the page. */
body::before{
  content:'';
  position:fixed;
  inset:0;                 /* exactly the viewport — it can never add a scrollbar */
  z-index:-2;              /* above the canvas, below the grain, below every section */
  pointer-events:none;     /* never swallows a click, a hover or a drag */
  background:
    radial-gradient(88% 72% at 88% -6%,
      rgba(var(--purple-rgb), .10), rgba(var(--purple-rgb), .042) 46%, transparent 74%),
    radial-gradient(84% 68% at 6% 106%,
      rgba(var(--pink-rgb), .09), rgba(var(--pink-rgb), .038) 46%, transparent 74%);
}

/* layer 3 — grain.
   A 120x120 fractal-noise tile, inline as base64 (372 bytes, so still no extra
   request), repeated and held at 2.5%. This is the part that stops the result reading as two
   blobs on a flat fill: it breaks up the large smooth areas the gradients leave behind. It
   gets its own element rather than a third background layer on ::before so its opacity is
   tunable on its own, and so reduced-motion can drop it without touching the glow. */
body::after{
  content:'';
  position:fixed;
  inset:0;
  z-index:-1;
  pointer-events:none;
  opacity:.025;
  background-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHdpZHRoPScxMjAnIGhlaWdodD0nMTIwJz48ZmlsdGVyIGlkPSduJz48ZmVUdXJidWxlbmNlIHR5cGU9J2ZyYWN0YWxOb2lzZScgYmFzZUZyZXF1ZW5jeT0nLjgnIG51bU9jdGF2ZXM9JzQnIHN0aXRjaFRpbGVzPSdzdGl0Y2gnLz48ZmVDb2xvck1hdHJpeCB0eXBlPSdzYXR1cmF0ZScgdmFsdWVzPScwJy8+PC9maWx0ZXI+PHJlY3Qgd2lkdGg9JzEyMCcgaGVpZ2h0PScxMjAnIGZpbHRlcj0ndXJsKCNuKScvPjwvc3ZnPg==");
  background-repeat:repeat;
  background-size:120px 120px;
}

/* The section trays (--surface-panel) and the white cards (--surface) are opaque, so all
   three layers only ever show in the air between them. Nothing below needs a z-index to
   clear them, and no text ever sits on them — every string on the page has an opaque box
   behind it. */

/* anyone who has asked the OS for maximum contrast gets the flat base back */
@media (prefers-contrast: more){
  body::before, body::after{ display:none; }
}
/* Nothing here animates. But a fixed grain tile under a scrolling page can read as a faint
   swim to anyone sensitive to that, so reduced-motion drops the texture and keeps the glow,
   which is smooth enough to have nothing to swim against. */
@media (prefers-reduced-motion: reduce){
  body::after{ display:none; }
}
/* =====================================================================
   SESSION 21 — the deals filter chips move into the section header.
   ===================================================================== */

/* Only this section has filters in its header, so the three-child layout is a modifier
   rather than something every `.section-title` pays for. `.section-title` is already
   `display:flex; justify-content:space-between`, and its `h2` already carries `order:-1`
   so the heading reads first in RTL — the chips take the middle and the "مشاهده همه" link
   is pushed to the far end, which is exactly the arrangement the other sections have
   minus the chips. */
.section-title-filters{ flex-wrap:wrap; align-items:center; gap:var(--space-4); }
.section-title-filters h2{ order:-1; }
.section-title-filters .deals-tabs{ order:0; }
.section-title-filters > a{ order:1; }

@media (max-width:860px){
  /* `flex:1 0 100%` drops the chips onto a line of their own the moment the header stops
     fitting, instead of squeezing the title and the link out of shape. */
  .section-title-filters{ gap:var(--space-3); }
  .section-title-filters .deals-tabs{ flex:1 0 100%; }
}
/* `safe center` only while the chips are still ONE scrolling row: with plain centring,
   content wider than the rail becomes unreachable at the start edge. Below 520 session 6
   wraps them into centred rows instead, where nothing can overflow and plain `center` is
   the value that rule intends — and since this selector is a class more specific, leaving
   justify-content unset here is what lets session 6 keep winning down there. */
@media (max-width:860px) and (min-width:521px){
  .section-title-filters .deals-tabs{ justify-content:safe center; }
}
/* Cherry-picked from the still-undeployed session 13, because this section cannot be
   tidy on mobile without it. `.deals-layout` is `align-items:flex-start`, which in a
   COLUMN governs the horizontal axis — so `.deals-products` shrink-wrapped to 266px
   inside a 303px column and the whole grid sat inset from its own section header, which
   is the other half of what made this block look untidy. Session 13 already carries this
   exact rule a few hundred lines up in the local file; this duplicate exists so the fix
   is actually LIVE. Delete it when session 13 finally deploys — the values are identical,
   so until then it is a no-op locally and the whole fix on the server. */
@media (max-width:860px){
  .deals-layout{ align-items:stretch; }
}

/* =====================================================================
   SESSION 22 — PJAX navigation affordances.
   ===================================================================== */

/* A swap keeps the header, top bar and mega-menu mounted and replaces only #main-content,
   so there is no white flash to suppress: the old content stays on screen, fully painted,
   until the response is in hand. What is left to communicate is that something is
   happening — a progress bar at the top edge, and the outgoing content easing back just
   far enough to read as "being replaced" without going blank. */
html.is-navigating::before{
  content:'';
  position:fixed; inset-block-start:0; inset-inline:0; height:3px;
  z-index:9999;                 /* above the sticky header and both FABs */
  background:linear-gradient(90deg, var(--purple), var(--pink));
  transform-origin:left center; /* physical, not logical: the bar sweeps the same way in RTL */
  animation:pjaxBar 1.2s var(--ease-decelerate) infinite;
}
@keyframes pjaxBar{
  0%   { transform:scaleX(0);   opacity:1; }
  70%  { transform:scaleX(.85); opacity:1; }
  100% { transform:scaleX(1);   opacity:0; }
}
#main-content{ transition:opacity var(--dur-3) var(--ease-standard); }
html.is-navigating #main-content{ opacity:.5; }
/* the router moves focus here after a swap, the way a real navigation moves the reading
   position. It is a programmatic target, not a control — it must not draw a focus ring. */
#main-content:focus{ outline:none; }

@media (prefers-reduced-motion: reduce){
  html.is-navigating::before{ animation:none; transform:scaleX(1); }
  #main-content{ transition:none; }
  html.is-navigating #main-content{ opacity:1; }
}

/* =====================================================================
   SESSION 23 — the "به‌زودی" panel gets a real backdrop.
   ===================================================================== */

/* The panel was a flat graphite gradient. It now carries a dark blurred bokeh photograph
   with a scrim over it. The existing `background:linear-gradient(--grey-900,--grey-800)`
   above is deliberately LEFT in place: it is what shows while the lazy image is still in
   flight and if it never arrives, so the panel degrades to exactly what it looked like
   before rather than to bare white.

   Layering inside the panel, which is already position:relative + overflow:hidden with a
   24px radius (so both layers below are clipped to it, no radius repeated here):
     0  the photograph
     1  the scrim
     2  the vertical label and the card track
     3  the prev/next arrows
   The label and track are static in the normal flow, so without an explicit stacking
   position the scrim — being positioned — would paint straight over them. */
.colored-carousel.coming-soon picture{ display:contents; }
.colored-carousel.coming-soon .cc-bg{
  position:absolute; inset:0;
  width:100%; height:100%;
  object-fit:cover; object-position:center;
  z-index:0;
}
.colored-carousel.coming-soon::before{
  content:''; position:absolute; inset:0; z-index:1;
  /* Built from the panel's own two greys rather than flat black, so the tint stays in the
     same family as the box it replaces.
     The strengths are measured. Compositing this scrim over the source art and scanning
     EVERY pixel of the visible strip gives the worst-case contrast for white text:
       .50 -> 4.08:1   .55 -> 4.60:1   .62 -> 5.48:1   .70 -> 6.74:1   .80 -> 8.80:1
     .72/.62 therefore clears 4.5:1 (the normal-text AA bar, so badges and any future small
     copy are covered, not just the 22px label) everywhere, while staying light enough that
     the photograph still reads as a photograph. Anything heavier buys margin nobody needs
     and hides the image that is the whole point of the change. */
  background:linear-gradient(135deg, rgba(19,25,39,.72), rgba(33,41,54,.62));
}
.colored-carousel.coming-soon > .colored-carousel-label,
.colored-carousel.coming-soon > .colored-carousel-track{ position:relative; z-index:2; }
.colored-carousel.coming-soon > .colored-carousel-arrow{ z-index:3; }

/* =====================================================================
   SESSION 24 — hand-drawn dividers between the major homepage sections.
   ===================================================================== */

/* Inline SVG, so no request and the stroke can take the brand gradient directly. The wave
   is irregular on purpose — see sectionDividerHtml() — because an even sine reads as a
   machine-drawn squiggle rather than something sketched. */
/* The divider OWNS the rhythm wherever it appears, rather than adding to whatever the
   element above happens to carry. Its neighbours genuinely differ: `.section` has
   --section-gap (72px), `.feature-strip` has 56px, `.hero-slider` has none. Leaving that
   alone gave gaps of -13/43/59/59px down one page; zeroing only `.section` still left the
   feature strip at 115px against everything else's 91px.
   Matching on "whatever direct child of main precedes a divider" covers every block type
   without listing them, and stays scoped enough not to touch anything else. Where :has()
   is unsupported the only consequence is a roomier gap — nothing breaks. */
#main-content > :has(+ .section-divider){ margin-bottom:0; }
.section-divider{
  display:flex; justify-content:center;
  padding-inline:64px;            /* same gutter as .section, so it lines up with content */
  margin-block:32px;
  pointer-events:none;            /* decoration must never sit in front of a real target */
}
.section-divider-ink{ display:block; width:100%; max-width:680px; height:auto; }
.sd-line{
  fill:none;
  stroke:url(#sectionDividerInk);
  stroke-width:2.4;
  stroke-linecap:round; stroke-linejoin:round;
  /* the viewBox scales down hard on phones; without this the stroke would scale with it
     and thin out to well under a pixel */
  vector-effect:non-scaling-stroke;
  opacity:.5;
}
/* RTL needs nothing: the wave is centred and symmetric in intent, and the gradient runs in
   SVG user space, which does not flip with direction. */

@media (max-width:1100px){ .section-divider{ padding-inline:28px; } }
@media (max-width:860px){
  .section-divider{ padding-inline:24px; margin-block:24px; }
  .section-divider-ink{ max-width:440px; }
}
@media (max-width:560px){
  .section-divider{ padding-inline:18px; }
  .section-divider-ink{ max-width:300px; }
}

/* Draw-in, gated. `.reveal` already gives these the shared fade/rise and its own
   reduced-motion escape hatch; this adds the stroke drawing itself on top, and ONLY for
   users who have not asked for less motion. The dash length is a generous constant rather
   than a measured pathLength — it only has to exceed the path, and 1400 does. */
@media (prefers-reduced-motion: no-preference){
  html.reveal-ready .section-divider .sd-line{ stroke-dasharray:1400; stroke-dashoffset:1400; }
  html.reveal-ready .section-divider.is-visible .sd-line{ animation:sdDraw 1100ms var(--ease-decelerate) forwards; }
}
@keyframes sdDraw{ to{ stroke-dashoffset:0; } }
