  * { box-sizing: border-box; }
  /* The new UAE Dirham currency symbol (U+20C3) — accepted by the
     Unicode Technical Committee in 2025 and scheduled for Unicode
     18.0 (September 2026), but no browser, OS, or system font
     renders it natively yet. unicode-range restricts this font to
     that single codepoint, so it never affects any other character
     on the page — everything else still renders in Plus Jakarta
     Sans exactly as before. Once operating systems ship native
     support, this @font-face and its font-family reference below
     can simply be deleted; every price on the site already uses the
     correct character (see money() in currency.js), so nothing else
     needs to change when that day comes. Relative paths, not
     Url::asset() — this is a static .css file, never parsed by PHP,
     so a url() here is resolved by the browser relative to this
     stylesheet's own location, not the page's. */
  @font-face {
    font-family: 'Dirham-Sans';
    src: url('../fonts/dirham-sans.woff2') format('woff2'),
         url('../fonts/dirham-sans.woff') format('woff'),
         url('../fonts/dirham-sans.ttf') format('truetype');
    font-weight: normal;
    font-style: normal;
    font-display: swap;
    unicode-range: U+20C3;
    /* Matches the digits' visual height, not just their font-size —
       measured directly, not guessed: the raw glyph rendered at
       1.36x the ink height of an adjacent digit at the same
       font-size, since size-adjust (like font-size itself) scales
       the em-square, not the visual glyph extent, and this font's
       own internal proportions are simply larger than Plus Jakarta
       Sans's digits at the same nominal size. 73% brings the two to
       a visual match; this scales every use of the symbol
       everywhere on the site from one place, rather than needing a
       size override at each of the many call sites that output it. */
    size-adjust: 73%;
  }
  body { font-family: 'Plus Jakarta Sans', 'Dirham-Sans', sans-serif; background: var(--tc-cream, #FFF9F2); }
  html, body { overflow-x: clip; }
  /* Lets a section escape the page's own max-width container and
     span the full browser width, regardless of how deep it sits in
     the markup — used by the header, footer, and homepage hero when
     an admin turns on "Full width" for that section, rather than
     restructuring every page template's own layout to support it.
     html/body's own overflow-x:clip above is what keeps this from
     ever introducing a horizontal scrollbar. */
  .tc-full-bleed { width: 100vw; margin-left: calc(-50vw + 50%); margin-right: calc(-50vw + 50%); }
  /* The top bar's own "hide once scrolled" opt-in. A prior version
     animated max-height/opacity/padding on this same element, which
     could desync under scroll load: three properties transitioning
     together on the element that also owns the strip's own padding
     meant a rapid, repeated toggle (see the rAF-throttled listener
     below) could catch the bar mid-transition, producing a visible
     flicker instead of a clean show/hide. Fixed by moving the
     strip's padding onto a dedicated .tc-topbar-inner wrapper (see
     header.php) and toggling this outer element's height with no
     transition at all — an instant, single-frame collapse has no
     mid-animation state to desync into. */
  .tc-topbar-collapsible { height: auto; overflow: hidden; }
  .tc-topbar-collapsible.tc-topbar-hidden { height: 0; }
  .tc-topbar-collapsible.tc-topbar-hidden .tc-topbar-inner { display: none; }
  /* Mobile header search fix: on a narrow screen, the logo plus the
     account/notification/cart icons alone leave the search box only
     a sliver of the row to work with — its placeholder text was
     visibly cut off, and the autocomplete dropdown beneath it
     (sized to match its own parent, not the viewport) was squeezed
     right along with it. Wrapping the row and giving the search
     box a full line of its own fixes both at once, since the
     dropdown's width already follows its parent's automatically. */
  @media (max-width: 639px) {
    .tc-header-row { flex-wrap: wrap; row-gap: 8px; }
    .tc-header-search-wrap { flex-basis: 100%; order: 3; }
  }
  /* 'Dirham-Sans' has to be repeated here, not just on body — a more
     specific font-family always wins over the inherited one, so a
     price shown inside .display loses the fallback chain entirely if
     it isn't listed again. 'Fraunces' and plain 'serif' both lack a
     U+20C3 glyph, and neither falls through to 'Dirham-Sans' unless
     told to here. */
  .display { font-family: 'Fraunces', 'Dirham-Sans', serif; }
  .star { color: var(--tc-accent, #FF6F91); }
  .starmuted { color: #F0DCC8; }
  .mesh { background: radial-gradient(circle at 15% 20%, var(--tc-mesh-2, #FFB86B) 0%, transparent 45%), radial-gradient(circle at 85% 15%, var(--tc-mesh-3, #FF6F91) 0%, transparent 50%), radial-gradient(circle at 50% 90%, var(--tc-mesh-1, #7EE8C4) 0%, transparent 55%), var(--tc-cream, #FFF9F2); }

  /* A lightweight, CSS-only firework-burst effect for the homepage
     hero when no banner has been configured yet -- keeps that space
     from reading as broken or forgotten while an admin sets one up,
     without pulling in any animation library or canvas/JS overhead
     on the highest-traffic page of the storefront. Each spark is
     just a small dot that expands outward from the center and fades,
     staggered across a handful of directions and delays so they read
     as a continuous, gentle sparkle rather than one obvious repeating
     loop. */
  .hero-spark-field {
    position: absolute;
    inset: 0;
    overflow: hidden;
  }
  .hero-spark {
    position: absolute;
    top: 50%;
    left: 50%;
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--tc-accent, #FF6F91);
    opacity: 0;
    animation: hero-spark-burst 3.2s ease-out infinite;
  }
  @keyframes hero-spark-burst {
    0%   { opacity: 0; transform: translate(-50%, -50%) rotate(var(--spark-angle, 0deg)) translateX(0) scale(0.6); }
    15%  { opacity: 1; }
    75%  { opacity: 0.35; }
    100% { opacity: 0; transform: translate(-50%, -50%) rotate(var(--spark-angle, 0deg)) translateX(70px) scale(0.2); }
  }
  @media (prefers-reduced-motion: reduce) {
    .hero-spark { animation: none; opacity: 0; }
  }
  .hover\:bg-\[\#443830\]:hover { background-color: #443830; }

  /* --- Mini-cart: bounce feedback + mobile layout ---
     Two visible bounces in one animation cycle (25%/75% keyframes)
     rather than chaining two separate animation runs — same visual
     effect, no JS timing logic needed to detect when to fire a
     second pass. */
  @keyframes cart-bounce {
    0%, 100% { transform: translateY(0); }
    25% { transform: translateY(-7px); }
    50% { transform: translateY(0); }
    75% { transform: translateY(-7px); }
  }
  #cart-trigger.cart-bounce { animation: cart-bounce 0.6s ease; }

  /* On narrow screens, the panel switches from "anchored to the cart
     icon's position" (which doesn't leave enough room on a phone-width
     screen for a fixed 340px panel without overflowing) to a
     viewport-anchored sheet instead — same content, positioned
     relative to the screen rather than the button. */
  @media (max-width: 480px) {
    #mini-cart-panel {
      position: fixed !important;
      left: 8px !important;
      right: 8px !important;
      top: 56px !important;
      width: auto !important;
      max-width: none !important;
    }
  }

  /* --- Shared product-filters partial (Shop, category, search, vendor pages) --- */
  .pf-layout { display: grid; grid-template-columns: 220px 1fr; gap: 24px; }
  @media (max-width: 768px) { .pf-layout { grid-template-columns: 1fr; } }
  .pf-sidebar { background: white; border: 1px solid #F0DCC8; border-radius: 16px; padding: 16px; align-self: start; }
  .pf-facet-group { margin-bottom: 16px; padding-bottom: 16px; border-bottom: 1px solid #F0DCC8; }
  .pf-facet-group:last-of-type { border-bottom: none; margin-bottom: 8px; }
  .pf-facet-title { display: block; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.03em; color: var(--tc-muted, #8A7D71); margin-bottom: 8px; }
  .pf-sidebar select, .pf-sidebar input[type="number"] { width: 100%; padding: 7px 9px; border: 1px solid #E5D5C3; border-radius: 8px; font-size: 12px; font-family: inherit; box-sizing: border-box; }
  .pf-clear-btn { width: 100%; padding: 8px; background: none; border: 1px solid #E5D5C3; border-radius: 8px; font-size: 12px; cursor: pointer; color: var(--tc-dark, #2B2320); }
  .pf-pager { display: flex; align-items: center; justify-content: center; flex-wrap: wrap; gap: 6px; margin-top: 20px; }
  /* Pager items are real <a href> links (so they're crawlable) rather
     than <button onclick>, which means they need their own styling —
     the old buttons borrowed a class that was never defined on the
     storefront. The same rules style both the server-rendered pager
     and the one the browser redraws after a filter change. */
  .pf-pager a, .pf-pager .pf-pg-current, .pf-pager .pf-pg-disabled {
    display: inline-flex; align-items: center; justify-content: center;
    min-width: 34px; height: 34px; padding: 0 10px; border-radius: 8px;
    font-size: 12.5px; text-decoration: none; color: inherit;
    border: 1px solid var(--tc-border, #E5D5C3); background: transparent;
  }
  .pf-pager a:hover { border-color: var(--tc-accent, #FF6F91); color: var(--tc-accent, #FF6F91); }
  .pf-pager .pf-pg-current { background: var(--tc-accent, #FF6F91); border-color: transparent; color: #fff; font-weight: 600; }
  .pf-pager .pf-pg-disabled { opacity: .4; }
  .pf-pager .pf-pg-gap { padding: 0 2px; color: var(--tc-muted, #8A7D71); }

  /* Mobile filter drawer — collapsed by default below 768px, opened via
     the "Filter" trigger button. Above 768px both the trigger and the
     drawer-only chrome (backdrop, close button, "Show results" button)
     stay hidden entirely and the sidebar reverts to its normal always-
     visible, in-flow desktop appearance — same DOM and CSS classes,
     just a different presentation per breakpoint rather than two
     separate implementations to keep in sync. */
  .pf-mobile-filter-btn { display: none; }
  .pf-sidebar-backdrop { display: none; }
  .pf-sidebar-mobile-head { display: none; }
  .pf-mobile-done-btn { display: none; }

  @media (max-width: 768px) {
    .pf-mobile-filter-btn {
      display: flex; align-items: center; justify-content: center; gap: 6px;
      width: 100%; padding: 11px; margin-bottom: 16px;
      background: white; border: 1px solid #E5D5C3; border-radius: 999px;
      font-size: 13px; font-weight: 600; color: var(--tc-dark, #2B2320); cursor: pointer;
    }
    .pf-active-count {
      display: inline-flex; align-items: center; justify-content: center;
      min-width: 18px; height: 18px; padding: 0 5px; border-radius: 999px;
      background: var(--tc-accent, #FF6F91); color: white; font-size: 11px; font-weight: 700;
    }
    .pf-sidebar {
      position: fixed; top: 0; right: 0; bottom: 0; width: 84vw; max-width: 340px;
      border-radius: 0; border: none; z-index: 70; overflow-y: auto;
      transform: translateX(100%); transition: transform 0.2s ease-out;
      box-shadow: -8px 0 24px rgba(43,35,32,0.15);
      display: flex; flex-direction: column; padding-bottom: 80px;
    }
    .pf-sidebar.pf-sidebar-open { transform: translateX(0); }
    .pf-sidebar-backdrop {
      display: block; position: fixed; inset: 0; background: rgba(43,35,32,0.4);
      z-index: 65; opacity: 0; pointer-events: none; transition: opacity 0.2s ease-out;
    }
    .pf-sidebar-backdrop.pf-sidebar-open { opacity: 1; pointer-events: auto; }
    .pf-sidebar-mobile-head {
      display: flex; align-items: center; justify-content: space-between;
      padding: 16px; border-bottom: 1px solid #F0DCC8; margin-bottom: 4px;
    }
    .pf-sidebar-close {
      width: 28px; height: 28px; border-radius: 999px; border: none; background: #F0DCC8;
      font-size: 14px; cursor: pointer; display: flex; align-items: center; justify-content: center;
    }
    .pf-facet-group { padding-left: 16px; padding-right: 16px; }
    .pf-clear-btn { margin-left: 16px; width: calc(100% - 32px); }
    .pf-mobile-done-btn {
      display: block; position: fixed; left: 0; right: 0; bottom: 0; width: 84vw; max-width: 340px;
      margin-left: auto; padding: 13px; background: var(--tc-dark, #2B2320); color: white; border: none;
      font-size: 13px; font-weight: 600; cursor: pointer;
    }
  }

  /* --- Compact product grid (Amazon-density inspired) ---
     Desktop column count is admin-configurable per section (set via
     --cols-desktop inline on the grid element), since the whole
     point is a variable, admin-chosen density — a fixed set of
     Tailwind grid-cols-N utility classes can't express "whatever
     number the admin typed in", so this uses CSS custom properties
     driven by inline style instead. Mobile/tablet stay fixed at
     sensible defaults regardless of the desktop setting, since
     packing 8+ columns onto a phone screen is never right no matter
     what an admin configures for desktop. */
  .product-grid {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 10px 12px;
  }
  @media (min-width: 640px) {
    .product-grid { grid-template-columns: repeat(var(--cols-tablet, 4), 1fr); }
  }
  @media (min-width: 1024px) {
    .product-grid { grid-template-columns: repeat(var(--cols-desktop, 6), 1fr); gap: 14px; }
  }

  .product-card { display: block; text-decoration: none; color: inherit; background: var(--tc-product-card-bg, white); border: 1px solid #F0DCC8; border-radius: 12px; padding: 8px; transition: box-shadow .15s; }
  .product-card:hover { box-shadow: 0 4px 14px rgba(43,35,32,0.08); }
  .product-card .pc-media { aspect-ratio: 1; border-radius: 8px; overflow: hidden; margin-bottom: 8px; position: relative; }
  .product-card .pc-media img { width: 100%; height: 100%; object-fit: cover; display: block; }
  .product-card .pc-badges { position: absolute; top: 6px; left: 6px; display: flex; flex-direction: column; gap: 4px; z-index: 2; }

  /* Skeleton loading state — shown the instant a filter changes or a
     category/shop page first loads, replaced the moment real results
     arrive. Reuses .product-card's own padding/border/radius exactly
     (same markup shape, just grey blocks instead of content), so
     nothing visibly shifts or resizes when the real cards swap in —
     the alternative, an empty grid or plain "Loading…" text, reads
     as a slower page even when the actual network time is identical. */
  .pc-skeleton { pointer-events: none; }
  .pc-skeleton .pc-media { background: #F0DCC8; }
  .pc-skel-line { background: #F0DCC8; border-radius: 4px; }
  .pc-skeleton .pc-media,
  .pc-skel-line {
    position: relative;
    overflow: hidden;
  }
  .pc-skeleton .pc-media::after,
  .pc-skel-line::after {
    content: "";
    position: absolute;
    inset: 0;
    background: linear-gradient(90deg, transparent, rgba(255,255,255,0.55), transparent);
    animation: pc-skeleton-shimmer 1.4s ease-in-out infinite;
  }
  @keyframes pc-skeleton-shimmer {
    0% { transform: translateX(-100%); }
    100% { transform: translateX(100%); }
  }
  .pc-badge-glow {
    display: inline-block;
    font-size: 9px;
    font-weight: 700;
    line-height: 1;
    padding: 3px 6px;
    border-radius: 999px;
    letter-spacing: 0.02em;
    animation: pc-badge-glow-pulse 2.2s ease-in-out infinite;
  }
  @keyframes pc-badge-glow-pulse {
    0%, 100% { box-shadow: 0 0 2px 0px var(--glow-color); }
    50% { box-shadow: 0 0 8px 3px var(--glow-color); }
  }
  @media (prefers-reduced-motion: reduce) {
    .pc-badge-glow { animation: none; }
  }
  /* Same glow animation as pc-badge-glow, sized for product-detail.php's
     larger, standalone badge display rather than a compact grid card. */
  .pd-badge-glow {
    display: inline-block;
    padding: 3px 10px;
    border-radius: 999px;
    font-size: 11px;
    font-weight: 600;
    animation: pc-badge-glow-pulse 2.2s ease-in-out infinite;
  }
  @media (prefers-reduced-motion: reduce) {
    .pd-badge-glow { animation: none; }
  }
  /* A generic version of the same pulse, for anything that isn't a
     small pill badge — a button, a nav link — where the caller
     supplies its own sizing/padding and just wants the glow added
     on top via --glow-color, same animation and easing as the two
     badge variants above for visual consistency across the site. */
  .tc-glow {
    animation: pc-badge-glow-pulse 2.2s ease-in-out infinite;
  }
  @media (prefers-reduced-motion: reduce) {
    .tc-glow { animation: none; }
  }
  /* Simple, static divider between product-page sections — no pulse
     animation (an earlier version had one; simplicity was explicitly
     requested), just a plain line in a color the theme customizer
     controls independently of any other color on the page. */
  .pd-section-divider {
    height: 1px;
    border: none;
    margin: 0;
    background: var(--tc-divider, #E5D5C3);
  }
  /* Boxed mode: each lower section becomes its own bordered card
     instead of being separated by a plain line — a genuine either/or
     look, not two toggles combined, so switching modes hides the
     dividers entirely rather than showing both border and line. */
  .pd-sections-boxed .pd-section-divider { display: none; }
  .pd-sections-boxed .pd-lower-section {
    border: 1px solid var(--tc-divider, #E5D5C3);
    border-radius: 16px;
    margin: 20px auto;
    background: white;
  }

  /* --- Product image gallery: transition effects, next/prev nav,
     thumbnail polish, hover-magnify lens ---
     Single <img> element swap, not two crossfading elements — so
     each effect needs a genuine two-stage enter/exit, not just one
     toggled class, or a direction-sensitive effect (slide, flip,
     spiral) would animate identically both ways instead of feeling
     like "next" and "previous" are actually opposite motions.
     pd-fx-out plays first (the current image visibly leaving), then
     setMainImage() swaps the src and immediately applies pd-fx-in
     (the new image's starting position, applied with no transition
     so it snaps there instantly, invisibly) before removing it a
     frame later — which is what actually plays the "entering"
     animation, transitioning from that starting position back to
     normal. Every effect is scoped under [data-pd-effect="..."] on
     the wrapper, set server-side from the platform setting, so only
     one is ever active at a time and switching the setting needs no
     JS changes — the same setMainImage() drives all eight. */
  .pd-image-wrap { position: relative; perspective: 1200px; }
  .pd-main-image {
    transition: opacity 0.35s ease, transform 0.35s ease, filter 0.35s ease, clip-path 0.35s ease;
    transform-style: preserve-3d;
    clip-path: circle(75% at 50% 50%);
  }

  [data-pd-effect="fade"] .pd-fx-out { opacity: 0; }
  [data-pd-effect="fade"] .pd-fx-in { opacity: 0; transition: none; }

  [data-pd-effect="slide"] .pd-fx-out { opacity: 0; transform: translateX(-8%); }
  [data-pd-effect="slide"] .pd-fx-in { opacity: 0; transform: translateX(8%); transition: none; }
  [data-pd-effect="slide"] .pd-fx-out.pd-nav-reverse { transform: translateX(8%); }
  [data-pd-effect="slide"] .pd-fx-in.pd-nav-reverse { transform: translateX(-8%); }

  [data-pd-effect="flip"] .pd-fx-out { opacity: 0; transform: rotateY(90deg); }
  [data-pd-effect="flip"] .pd-fx-in { opacity: 0; transform: rotateY(-90deg); transition: none; }
  [data-pd-effect="flip"] .pd-fx-out.pd-nav-reverse { transform: rotateY(-90deg); }
  [data-pd-effect="flip"] .pd-fx-in.pd-nav-reverse { transform: rotateY(90deg); }

  [data-pd-effect="spiral"] .pd-fx-out { opacity: 0; transform: rotate(190deg) scale(0.35); }
  [data-pd-effect="spiral"] .pd-fx-in { opacity: 0; transform: rotate(-190deg) scale(0.35); transition: none; }
  [data-pd-effect="spiral"] .pd-fx-out.pd-nav-reverse { transform: rotate(-190deg) scale(0.35); }
  [data-pd-effect="spiral"] .pd-fx-in.pd-nav-reverse { transform: rotate(190deg) scale(0.35); }

  /* Iris wipe — the "normal" 75% radius (set on .pd-main-image
     above) is the smallest circle that still fully covers a square
     image's corners; shrinking it to 0 and back is what gives the
     open/close look. */
  [data-pd-effect="round"] .pd-fx-out { clip-path: circle(0% at 50% 50%); }
  [data-pd-effect="round"] .pd-fx-in { clip-path: circle(0% at 50% 50%); transition: none; }

  /* A halftone dot mask sits on the image permanently in this mode
     (not just during the transition) — CSS can transition a mask's
     opacity but not smoothly regrow its dot pattern, so the dotted
     texture itself has to be a constant, with the actual motion
     coming from opacity + a slight scale instead. */
  [data-pd-effect="dotted"] .pd-main-image {
    -webkit-mask-image: radial-gradient(circle 2.5px at center, black 99%, transparent 100%);
    mask-image: radial-gradient(circle 2.5px at center, black 99%, transparent 100%);
    -webkit-mask-size: 10px 10px;
    mask-size: 10px 10px;
  }
  [data-pd-effect="dotted"] .pd-fx-out { opacity: 0; transform: scale(1.06); }
  [data-pd-effect="dotted"] .pd-fx-in { opacity: 0; transform: scale(0.94); transition: none; }

  [data-pd-effect="vanishing"] .pd-fx-out { opacity: 0; transform: scale(0.5); filter: blur(3px); }
  [data-pd-effect="vanishing"] .pd-fx-in { opacity: 0; transform: scale(1.5); filter: blur(3px); transition: none; }

  [data-pd-effect="smoke"] .pd-fx-out { opacity: 0; filter: blur(18px) brightness(1.25); transform: scale(1.08); }
  [data-pd-effect="smoke"] .pd-fx-in { opacity: 0; filter: blur(18px) brightness(1.25); transform: scale(0.94); transition: none; }

  /* Next/prev arrows — overlaid on the image itself, not the thumbnail
     row, so the same click target works identically whether or not
     thumbnails are even present (a single-image product has none).
     Shown on hover for a mouse; always visible with no hover state to
     rely on for touch, which the fine/any-hover media query tells
     apart without needing a JS touch-capability check. */
  /* Heart/save button, shared visual language with the pill-shaped
     nav arrows below it (white circle, soft shadow) so it reads as
     part of the same UI family rather than a bolted-on extra. Filled
     red heart + subtle pop when saved is the standard, instantly
     recognizable "wishlisted" affordance — no label needed. */
  .pd-wishlist-btn, .pc-wishlist-btn {
    width: 40px; height: 40px; border-radius: 50%;
    background: white; border: 1px solid #E5D5C3; cursor: pointer;
    display: flex; align-items: center; justify-content: center;
    color: #5C5148; flex-shrink: 0;
    transition: color 0.15s ease, transform 0.15s ease, border-color 0.15s ease;
  }
  .pd-wishlist-btn:hover, .pc-wishlist-btn:hover { border-color: #EF4444; color: #EF4444; }
  .pd-wishlist-btn.wishlist-saved, .pc-wishlist-btn.wishlist-saved {
    color: #EF4444; border-color: #EF4444;
  }
  .pd-wishlist-btn.wishlist-saved svg, .pc-wishlist-btn.wishlist-saved svg { fill: #EF4444; }
  .pd-wishlist-btn.wishlist-saved, .pc-wishlist-btn.wishlist-saved { animation: pd-wishlist-pop 0.25s ease; }
  @keyframes pd-wishlist-pop {
    0% { transform: scale(1); } 40% { transform: scale(1.2); } 100% { transform: scale(1); }
  }
  .pc-wishlist-btn {
    position: absolute; top: 8px; right: 8px; z-index: 5;
    width: 32px; height: 32px;
  }

  .pd-nav-arrow {
    position: absolute; top: 50%; transform: translateY(-50%);
    width: 36px; height: 36px; border-radius: 50%;
    background: rgba(255,255,255,0.9); border: none; cursor: pointer;
    display: flex; align-items: center; justify-content: center;
    font-size: 16px; color: #0C2D48; z-index: 15;
    box-shadow: 0 2px 8px rgba(12,45,72,0.18);
    opacity: 0; transition: opacity 0.15s ease, background 0.15s ease;
  }
  .pd-nav-arrow:hover { background: white; }
  .pd-nav-prev { left: 10px; }
  .pd-nav-next { right: 10px; }
  @media (hover: none) {
    .pd-nav-arrow { opacity: 1; }
  }
  .pd-image-wrap:hover .pd-nav-arrow { opacity: 1; }

  .pd-thumb { transition: border-color 0.15s ease, transform 0.15s ease; }
  .pd-thumb:hover { transform: scale(1.06); }
  .pd-thumb.selected { transform: scale(1.02); }
  /* Zoom modal: fade + scale in/out instead of an instant toggle. */
  #image-zoom-overlay {
    opacity: 0;
    transition: opacity 0.2s ease;
  }
  #image-zoom-overlay.pd-zoom-open { opacity: 1; }
  #image-zoom-img {
    transform: scale(0.92);
    transition: transform 0.2s ease;
  }
  #image-zoom-overlay.pd-zoom-open #image-zoom-img { transform: scale(1); }
  @media (prefers-reduced-motion: reduce) {
    .pd-main-image, .pd-thumb, .pd-nav-arrow, #image-zoom-overlay, #image-zoom-img { transition: none; }
  }
  /* Details/Specifications tab bar — only rendered when a product has
     both sections (a single tab would be pointless UI), toggled via
     plain JS rather than <details> since two mutually-exclusive panels
     read more naturally as tabs than as two independent disclosures. */
  .pd-content-tabs { display: flex; gap: 4px; border-bottom: 1px solid #F0DCC8; margin-bottom: 18px; }
  .pd-content-tab-btn {
    background: none; border: none; padding: 10px 16px; margin: 0; width: auto;
    font-size: 13px; font-weight: 600; color: var(--tc-muted, #8A7D71); cursor: pointer;
    border-bottom: 2px solid transparent; margin-bottom: -1px;
  }
  .pd-content-tab-btn.active { color: var(--tc-accent); border-bottom-color: var(--tc-accent); }
  /* Restores basic list/paragraph/link styling for rich-text-editor
     content (Quill-generated HTML for product descriptions and static
     page sections) — Tailwind's own base reset strips list-style,
     margins, and other defaults from every element, which is correct
     for utility-class layouts but leaves editor output looking wrong
     (bullet points that don't show, no space between paragraphs)
     without this scoped override. */
  .pd-rich-content p { margin: 0 0 0.8em; }
  .pd-rich-content p:last-child { margin-bottom: 0; }
  .pd-rich-content ul { list-style: disc; padding-left: 1.4em; margin: 0 0 0.8em; }
  .pd-rich-content ol { list-style: decimal; padding-left: 1.4em; margin: 0 0 0.8em; }
  .pd-rich-content li { margin-bottom: 0.3em; }
  .pd-rich-content a { color: var(--tc-accent, #FF6F91); text-decoration: underline; }
  .pd-rich-content strong { font-weight: 700; }
  .pd-rich-content em { font-style: italic; }
  .product-card .pc-name { font-size: 12px; line-height: 1.35; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; margin-bottom: 4px; min-height: 2.4em; color: var(--tc-dark, #2B2320); }
  .product-card .pc-rating { font-size: 11px; color: var(--tc-muted, #8A7D71); margin-bottom: 3px; }
  .product-card .pc-price { font-size: 14px; font-weight: 700; color: var(--tc-dark, #2B2320); }
  .product-card .pc-vendor { font-size: 10px; color: var(--tc-muted, #8A7D71); margin-top: 2px; }
  @media (min-width: 1024px) { .product-card .pc-name { font-size: 13px; } .product-card .pc-price { font-size: 15px; } }
  /* ============================================================
     Hero banner carousel transitions (14 selectable effects) —
     .hero-fx-{name} on #hero-carousel picks which set applies;
     .hero-entering / .hero-exiting are toggled by the carousel's
     own JS on the incoming/outgoing slide for the animation's
     duration, then removed once it's done. Every effect animates
     only transform/opacity/filter/clip-path — properties the
     browser can run on its own compositor thread — so switching
     between slides never causes a layout recalculation on every
     frame, which matters since this runs on every single visit to
     the homepage, not an occasional interaction.
     ============================================================ */
  #hero-carousel .hero-slide { will-change: transform, opacity, filter, clip-path; }

  /* 1. Fade — the safe, classic crossfade */
  @keyframes hero-fade-in { from { opacity: 0; } to { opacity: 1; } }
  @keyframes hero-fade-out { from { opacity: 1; } to { opacity: 0; } }
  .hero-fx-fade .hero-entering { animation: hero-fade-in .6s ease forwards; }
  .hero-fx-fade .hero-exiting { animation: hero-fade-out .6s ease forwards; }

  /* 2. Slide — new banner pushes in from the right */
  @keyframes hero-slide-in-r { from { transform: translateX(100%); } to { transform: translateX(0); } }
  @keyframes hero-slide-out-l { from { transform: translateX(0); } to { transform: translateX(-100%); } }
  .hero-fx-slide .hero-entering { animation: hero-slide-in-r .55s cubic-bezier(.4,0,.2,1) forwards; }
  .hero-fx-slide .hero-exiting { animation: hero-slide-out-l .55s cubic-bezier(.4,0,.2,1) forwards; }

  /* 3. Slide (reverse) — same idea, opposite direction */
  @keyframes hero-slide-in-l { from { transform: translateX(-100%); } to { transform: translateX(0); } }
  @keyframes hero-slide-out-r { from { transform: translateX(0); } to { transform: translateX(100%); } }
  .hero-fx-slide-reverse .hero-entering { animation: hero-slide-in-l .55s cubic-bezier(.4,0,.2,1) forwards; }
  .hero-fx-slide-reverse .hero-exiting { animation: hero-slide-out-r .55s cubic-bezier(.4,0,.2,1) forwards; }

  /* 4. Move up — new banner rises in from below */
  @keyframes hero-up-in { from { transform: translateY(100%); } to { transform: translateY(0); } }
  @keyframes hero-up-out { from { transform: translateY(0); opacity: 1; } to { transform: translateY(-35%); opacity: 0; } }
  .hero-fx-move-up .hero-entering { animation: hero-up-in .55s cubic-bezier(.4,0,.2,1) forwards; }
  .hero-fx-move-up .hero-exiting { animation: hero-up-out .55s cubic-bezier(.4,0,.2,1) forwards; }

  /* 5. Move down — reverse of the above */
  @keyframes hero-down-in { from { transform: translateY(-100%); } to { transform: translateY(0); } }
  @keyframes hero-down-out { from { transform: translateY(0); opacity: 1; } to { transform: translateY(35%); opacity: 0; } }
  .hero-fx-move-down .hero-entering { animation: hero-down-in .55s cubic-bezier(.4,0,.2,1) forwards; }
  .hero-fx-move-down .hero-exiting { animation: hero-down-out .55s cubic-bezier(.4,0,.2,1) forwards; }

  /* 6. Zoom fade — a subtle Ken Burns-style settle */
  @keyframes hero-zoom-in { from { opacity: 0; transform: scale(1.1); } to { opacity: 1; transform: scale(1); } }
  @keyframes hero-zoom-out { from { opacity: 1; transform: scale(1); } to { opacity: 0; transform: scale(0.94); } }
  .hero-fx-zoom-fade .hero-entering { animation: hero-zoom-in .7s ease forwards; }
  .hero-fx-zoom-fade .hero-exiting { animation: hero-zoom-out .7s ease forwards; }

  /* 7. Vibrate — a quick attention-grabbing shake as the new banner lands */
  @keyframes hero-vibrate-in {
    0% { opacity: 0; transform: translateX(0); }
    12% { opacity: 1; transform: translateX(-10px); }
    24% { transform: translateX(9px); }
    36% { transform: translateX(-7px); }
    48% { transform: translateX(6px); }
    60% { transform: translateX(-4px); }
    72% { transform: translateX(3px); }
    84% { transform: translateX(-1px); }
    100% { transform: translateX(0); opacity: 1; }
  }
  .hero-fx-vibrate .hero-entering { animation: hero-vibrate-in .6s ease forwards; }
  .hero-fx-vibrate .hero-exiting { animation: hero-fade-out .35s ease forwards; }

  /* 8. Dots — a circular iris wipe opening from the center point */
  @keyframes hero-dots-in { from { clip-path: circle(0% at 50% 50%); } to { clip-path: circle(75% at 50% 50%); } }
  .hero-fx-dots .hero-entering { animation: hero-dots-in .65s ease forwards; }
  .hero-fx-dots .hero-exiting { animation: hero-fade-out .3s ease forwards; }

  /* 9. Smoke — materializes out of a heavy blur, like it's clearing */
  @keyframes hero-smoke-in { from { opacity: 0; filter: blur(20px); } to { opacity: 1; filter: blur(0); } }
  @keyframes hero-smoke-out { from { opacity: 1; filter: blur(0); } to { opacity: 0; filter: blur(20px); } }
  .hero-fx-smoke .hero-entering { animation: hero-smoke-in .75s ease forwards; }
  .hero-fx-smoke .hero-exiting { animation: hero-smoke-out .75s ease forwards; }

  /* 10. Flip — a 3D card flip (#hero-carousel's own perspective makes this read as genuinely 3D, not just squashed) */
  @keyframes hero-flip-in { from { opacity: 0; transform: rotateY(85deg); } to { opacity: 1; transform: rotateY(0deg); } }
  @keyframes hero-flip-out { from { opacity: 1; transform: rotateY(0deg); } to { opacity: 0; transform: rotateY(-85deg); } }
  .hero-fx-flip .hero-entering { animation: hero-flip-in .6s ease forwards; backface-visibility: hidden; }
  .hero-fx-flip .hero-exiting { animation: hero-flip-out .6s ease forwards; backface-visibility: hidden; }

  /* 11. Curtain — opens from the center outward, like a stage curtain */
  @keyframes hero-curtain-in { from { clip-path: inset(0 50% 0 50%); } to { clip-path: inset(0 0 0 0); } }
  .hero-fx-curtain .hero-entering { animation: hero-curtain-in .6s ease forwards; }
  .hero-fx-curtain .hero-exiting { animation: hero-fade-out .3s ease forwards; }

  /* 12. Diagonal — a diagonal sweep reveal, corner to corner */
  @keyframes hero-diagonal-in { from { clip-path: polygon(0 100%, 0 100%, 0 100%, 0 100%); } to { clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%); } }
  .hero-fx-diagonal .hero-entering { animation: hero-diagonal-in .6s ease forwards; }
  .hero-fx-diagonal .hero-exiting { animation: hero-fade-out .3s ease forwards; }

  /* 13. Bounce — a playful pop-in with a slight overshoot */
  @keyframes hero-bounce-in {
    0% { opacity: 0; transform: scale(.82); }
    55% { opacity: 1; transform: scale(1.05); }
    80% { transform: scale(.98); }
    100% { transform: scale(1); }
  }
  .hero-fx-bounce .hero-entering { animation: hero-bounce-in .55s cubic-bezier(.34,1.56,.64,1) forwards; }
  .hero-fx-bounce .hero-exiting { animation: hero-fade-out .3s ease forwards; }

  /* 14. Rotate fade — a slight tilt settling into place */
  @keyframes hero-rotate-in { from { opacity: 0; transform: rotate(-4deg) scale(1.04); } to { opacity: 1; transform: rotate(0deg) scale(1); } }
  @keyframes hero-rotate-out { from { opacity: 1; transform: rotate(0deg) scale(1); } to { opacity: 0; transform: rotate(3deg) scale(0.97); } }
  .hero-fx-rotate-fade .hero-entering { animation: hero-rotate-in .6s ease forwards; }
  .hero-fx-rotate-fade .hero-exiting { animation: hero-rotate-out .6s ease forwards; }

  /* A person who's asked the browser to minimize motion shouldn't get
     any of the above regardless of what the admin picked — every
     effect degrades to the same instant, motion-free swap. */
  @media (prefers-reduced-motion: reduce) {
    #hero-carousel .hero-entering,
    #hero-carousel .hero-exiting { animation: none !important; }
  }

/* Countdown badge — next to a section heading, shown only when that
   section has a real, admin-set end date. Deliberately eye-catching
   (a bold pill with its own glowing border) since the whole point of
   a countdown is to be noticed — the honesty is in never showing this
   without a real deadline behind it, not in downplaying it visually
   once it's earned its place. --glow-color is set inline per badge
   from the section's own admin-chosen color, so this CSS never
   hardcodes which color a store's countdowns use. */
.countdown-badge {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 6px 16px 6px 12px;
  border-radius: 999px;
  font-size: 13px;
  font-weight: 700;
  vertical-align: middle;
  white-space: nowrap;
  border: 1.5px solid var(--glow-color);
  background: color-mix(in srgb, var(--glow-color) 10%, white);
  box-shadow: 0 0 0 0 color-mix(in srgb, var(--glow-color) 45%, transparent);
  animation: countdown-badge-glow 1.8s ease-in-out infinite;
}
.countdown-badge-icon {
  font-size: 14px;
  line-height: 1;
  animation: countdown-badge-icon-bounce 1.8s ease-in-out infinite;
}
.countdown-badge-label {
  color: color-mix(in srgb, var(--glow-color) 75%, black);
  font-weight: 600;
  font-size: 12px;
}
.countdown-badge-value {
  color: var(--glow-color);
  font-family: 'JetBrains Mono', monospace;
  font-weight: 800;
  letter-spacing: 0.02em;
  font-size: 13.5px;
}
@keyframes countdown-badge-glow {
  0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--glow-color) 45%, transparent); }
  50% { box-shadow: 0 0 14px 3px color-mix(in srgb, var(--glow-color) 45%, transparent); }
}
@keyframes countdown-badge-icon-bounce {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.18); }
}

/* The section frame — wraps an entire section that has a countdown
   running, so the urgency reads at a glance before a visitor even
   gets to the badge itself, not just in one small pill off to the
   side. Same --glow-color custom property, set once on the wrapper
   and inherited by the badge inside it rather than repeated. */
.countdown-section-frame {
  border: 2px solid color-mix(in srgb, var(--glow-color) 55%, transparent);
  border-radius: 24px;
  padding: 18px 14px;
  margin: 0 -2px;
  animation: countdown-frame-glow 2.4s ease-in-out infinite;
}
@keyframes countdown-frame-glow {
  0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--glow-color) 25%, transparent); }
  50% { box-shadow: 0 0 22px 4px color-mix(in srgb, var(--glow-color) 25%, transparent); }
}
@media (prefers-reduced-motion: reduce) {
  .countdown-badge,
  .countdown-badge-icon,
  .countdown-section-frame { animation: none; }
}

/* Banner section — three variants (strip, image+text, image-only),
   sharing the same rounded, soft-shadow language as the rest of the
   storefront's cards rather than introducing a new visual style. */
.banner-strip {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 14px;
  flex-wrap: wrap;
  padding: 14px 20px;
  border-radius: 16px;
  background: var(--tc-accent);
  color: white;
  text-decoration: none;
  text-align: center;
}
.banner-strip-text {
  font-weight: 600;
  font-size: 14px;
}
.banner-strip-cta {
  font-weight: 700;
  font-size: 13px;
  text-decoration: underline;
  text-underline-offset: 2px;
}

.banner-image-text {
  display: flex;
  align-items: stretch;
  gap: 24px;
  border-radius: 24px;
  overflow: hidden;
  background: var(--tc-category-box-bg);
}
.banner-image-text-reverse { flex-direction: row-reverse; }
.banner-image-text-media {
  flex: 1 1 40%;
  min-height: 200px;
}
.banner-image-text-media picture,
.banner-image-text-media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.banner-image-text-body {
  flex: 1 1 60%;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: 24px;
}
@media (max-width: 640px) {
  .banner-image-text,
  .banner-image-text-reverse { flex-direction: column; }
  .banner-image-text-media { min-height: 160px; }
}

.banner-image-only {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  min-height: 180px;
  border-radius: 24px;
  padding: 24px;
  text-align: center;
  text-decoration: none;
}
.banner-image-only-title {
  background: rgba(255,255,255,0.85);
  padding: 8px 16px;
  border-radius: 999px;
  color: var(--tc-dark);
}
.banner-image-only-subtitle {
  background: rgba(255,255,255,0.85);
  padding: 4px 12px;
  border-radius: 999px;
  color: var(--tc-dark);
  opacity: 0.8;
}

.banner-cta-btn {
  display: inline-block;
  align-self: flex-start;
  padding: 10px 22px;
  border-radius: 999px;
  background: var(--tc-accent);
  color: white;
  font-weight: 600;
  font-size: 13px;
  text-decoration: none;
}

/* Free-delivery progress — a motorbike travels from a warehouse to a
   house as the cart total approaches the real, admin-configured
   free-delivery threshold. Three layouts (Linear Road, Compact Pill,
   Dashed Route), one chosen site-wide from Theme Settings, sharing
   this one stylesheet since only one renders on any given page. */
.fd-progress { font-family: inherit; }
.fd-endpoint-icon { font-size: 18px; line-height: 1; }

/* Layout 1 — Linear Road: message above a full-width bar, endpoint
   icons below it — the most spacious of the three, suited to the
   product page and checkout where there's real width to use. */
.fd-progress-linear .fd-progress-message { font-size: 12.5px; margin: 0 0 8px; color: var(--tc-dark); }
.fd-progress-linear.fd-qualified .fd-progress-message { color: #1F7A47; font-weight: 600; }
.fd-linear-track {
  position: relative;
  height: 8px;
  border-radius: 999px;
  background: #F0E4D4;
  margin: 14px 0 14px;
}
.fd-linear-fill {
  display: block;
  height: 100%;
  border-radius: 999px;
  background: var(--tc-accent);
  transition: width 0.5s ease;
}
.fd-linear-rider {
  position: absolute;
  top: 50%;
  transform: translate(-50%, -50%);
  font-size: 34px;
  line-height: 1;
  transition: left 0.5s ease;
  filter: drop-shadow(0 1px 2px rgba(0,0,0,0.25));
}
.fd-linear-endpoints { display: flex; justify-content: space-between; }

/* Layout 2 — Compact Pill: a single-row, minimal-height strip built
   for tight spaces — the mini-cart panel above all, where a full
   Linear Road layout would push the cart items too far down. */
.fd-progress-pill {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 12px;
  border-radius: 999px;
  background: #FBF3E9;
  border: 1px solid #F0E4D4;
  flex-wrap: wrap;
}
.fd-pill-endpoint { font-size: 18px; }
.fd-pill-track {
  position: relative;
  flex: 1 1 60px;
  min-width: 50px;
  height: 6px;
  border-radius: 999px;
  background: #F0E4D4;
}
.fd-pill-fill {
  display: block;
  height: 100%;
  border-radius: 999px;
  background: var(--tc-accent);
  transition: width 0.5s ease;
}
.fd-pill-rider {
  position: absolute;
  top: 50%;
  transform: translate(-50%, -50%);
  font-size: 26px;
  line-height: 1;
  transition: left 0.5s ease;
}
.fd-pill-text { font-size: 11.5px; font-weight: 600; color: var(--tc-dark); white-space: nowrap; }
.fd-progress-pill.fd-qualified .fd-pill-text { color: #1F7A47; }

/* Layout 3 — Dashed Route: a message above a wider illustrated road
   with a dashed line the motorbike appears to ride along — the most
   visually rich of the three, for the cart and checkout pages where
   the free-delivery nudge deserves a bit more presence. */
.fd-progress-route { text-align: center; }
.fd-route-message { font-size: 13px; margin: 0 0 10px; color: var(--tc-dark); }
.fd-progress-route.fd-qualified .fd-route-message { color: #1F7A47; font-weight: 600; }
.fd-route-track {
  position: relative;
  display: flex;
  align-items: center;
  gap: 8px;
  height: 44px;
}
.fd-route-path { flex: 1; height: 10px; }
.fd-route-dashes { stroke: #E0C9A6; }
.fd-progress-route.fd-qualified .fd-route-dashes { stroke: var(--tc-accent); }
.fd-route-rider {
  position: absolute;
  top: 50%;
  transform: translate(-50%, -60%);
  font-size: 38px;
  line-height: 1;
  transition: left 0.6s ease;
  filter: drop-shadow(0 2px 3px rgba(0,0,0,0.25));
}
