/* ==========================================================================
   Shop — digital-product listing (AE scripts, AE/C4D project files,
   tutorials). Same filter-pill + grid pattern as the portfolio archive,
   but cards carry commerce info (price, type badge, rating) instead of a
   hover-preview video.
   ========================================================================== */

.shop {
  padding: var(--space-16) 0;
  background: var(--color-bg);
}

/* --- Featured product banner (same recipe as the blog listing's featured
   hero card in css/blog.css, adapted for a price/rating context instead of
   an author/date one) --- */

/* min-height is the measured worst case across the 3 slides' own copy —
   same "reserve the tallest, let shorter content just leave slack"
   principle as the card grids below (see e.g. .shop__card in this same
   file). Without it, swapping to a slide whose title/excerpt wraps to a
   different number of lines changes the banner's own height, and since
   both grid columns stretch to match (align-items: stretch), the picture
   panel would visibly resize along with it — the "jitter" this avoids. */
.shop__featured {
  display: grid;
  grid-template-columns: minmax(0, 1.1fr) minmax(0, 0.9fr);
  align-items: stretch;
  gap: var(--space-8);
  min-height: 340px;
  margin-bottom: var(--space-12);
  background: var(--color-bg-elevated);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  overflow: hidden;
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.32);
}

/* overflow: hidden crops both slide images (below) to this box — each
   fills it completely (position:absolute, inset:0) and slides in/out via
   transform, so without this they'd spill past the edges mid-transition
   instead of staying cropped. */
/* No aspect-ratio in the 2-column layout: the media simply fills its own
   column (stretched to the banner's height by align-items: stretch) and its
   image crops via object-fit: cover. An aspect-ratio here fought that
   stretch — when the text column grew taller than the media's natural 16:9
   height, the ratio forced the media WIDER to keep 16:9, and it spilled
   over the text/price column beside it. The 16:9 crop is restored only once
   the banner stacks into a single column (image on top — see the ≤1024px
   block below), where there's no neighbour to overflow. */
.shop__featured-media {
  position: relative;
  overflow: hidden;
}

/* Every slide image (the current one, plus a second while transitioning)
   stacks in the same box and slides via transform alone — never
   width/height — so swapping slides never triggers layout. ease-in-out
   (eases into AND out of the motion, unlike --ease-standard's sharper
   ease-out lean) plus a longer duration reads as a deliberate, unhurried
   slide instead of a quick snap. */
.shop__featured-media img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  transition: transform 1000ms ease-in-out;
}

/* Set on a freshly-inserted incoming image for one frame (before its
   transform is released to 0) so it starts parked off-screen right;
   the outgoing image gets the mirror class to exit left — see
   js/shop-featured-slider.js. The -rev pair mirrors this for a backward
   (swipe-right / prev) transition: incoming enters from the LEFT, outgoing
   exits RIGHT, so the motion follows the finger instead of always
   running right-to-left. */
.shop__featured-media img.is-slide-incoming {
  transform: translateX(100%);
}

.shop__featured-media img.is-slide-outgoing {
  transform: translateX(-100%);
}

.shop__featured-media img.is-slide-incoming-rev {
  transform: translateX(-100%);
}

.shop__featured-media img.is-slide-outgoing-rev {
  transform: translateX(100%);
}

/* One fixed label, not per-slide content — it never moves or fades, so
   z-index: 1 just needs to keep it above whichever image is mid-slide (a
   `transform` other than none promotes an element to its own stacking
   context, so an image mid-slide would otherwise jump in front of this —
   both z-index:auto, later DOM order wins — and cover the badge). Same
   dark-glass recipe as .shop__card-badge (components.css) rather than
   the brand gradient, since this is a permanent fixture of the banner,
   not a per-slide accent. */
.shop__featured-tag {
  position: absolute;
  z-index: 1;
  top: var(--space-5);
  left: var(--space-5);
  display: inline-flex;
  align-items: center;
  height: 32px;
  padding: 0 var(--space-4);
  border-radius: var(--radius-sm);
  background: rgba(5, 5, 6, 0.72);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  border: 1px solid rgba(255, 255, 255, 0.14);
  color: var(--color-text);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  -webkit-user-select: none;
  user-select: none;
}

/* Slide indicator + control dots, overlaid on the picture itself —
   z-index: 2 keeps them above the tag's own z-index: 1 and both sliding
   images. Real buttons (clicking one jumps straight to that slide), but
   styled as plain indicator dots rather than obviously-interactive
   buttons. */
.shop__featured-dots {
  position: absolute;
  z-index: 2;
  bottom: var(--space-5);
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  gap: var(--space-3);
}

/* Each dot is a ring; the active one additionally shows a filled center
   (::before) — inactive dots stay hollow. */
.shop__featured-dot {
  -webkit-appearance: none;
  appearance: none;
  position: relative;
  width: 10px;
  height: 10px;
  padding: 0;
  border-radius: 50%;
  border: 1.5px solid rgba(255, 255, 255, 0.6);
  /* Dark translucent fill (all dots) so the white centre dot reads clearly
     against a light banner image behind it. */
  background: rgba(5, 5, 6, 0.45);
  cursor: pointer;
  transition: border-color var(--duration-fast) var(--ease-standard);
}

/* Centre dot pinned to the exact middle (50/50 + translate) rather than an
   inset box — inset left it looking slightly off-centre inside the ring. */
.shop__featured-dot::before {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 4px;
  height: 4px;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  background: #fff;
  opacity: 0;
  transition: opacity var(--duration-fast) var(--ease-standard);
}

.shop__featured-dot.is-active {
  border-color: #fff;
}

.shop__featured-dot.is-active::before {
  opacity: 1;
}

/* Top/left-aligned, not centered — title always sits at a fixed distance
   from the top (padding-top below), never drifting up/down with how
   tall the excerpt happens to be. Equal left/right padding (unlike the
   asymmetric 0/space-10 this replaced). */
/* Fixed min-height = the worst case (a 2-line title). Every slide's body is
   this tall regardless of its title length, so the banner never changes
   height between slides (no jump). With the footer pinned to the bottom
   (margin-top: auto), a shorter (1-line) title just leaves more room
   between the excerpt and the price/button row — the reserved space lands
   at the bottom, not as a blank line under the title. */
.shop__featured-body {
  display: flex;
  flex-direction: column;
  min-height: 355px;
  padding: var(--space-8) var(--space-10);
  /* The column gap (space-8) already sits to the left of this block, so a
     full space-10 left padding on top of it makes the text's gap to the
     image nearly double its gap to the right edge. Trim the left padding so
     gap(32) + padding-left(8) == the 40px on the right → balanced. (Reset
     to symmetric on ≤1024 below, where the banner is a single column.) */
  padding-left: var(--space-2);
}

/* Title+rating+excerpt fade + rise together on entrance (transition-delay
   set by js/shop-featured-slider.js so they release at the same moment);
   the price gets its own slightly-later delay, same 2-group stagger idea
   as js/listing.js's cards. Exit (.is-slide-leaving): fades out, opacity
   only — no movement — at listing.js's own quicker 300ms leave speed.
   .shop__featured-buy (the Buy Now button) deliberately isn't part of
   either group — it's a fixed, always-visible control, not per-slide
   content, so it never fades or moves; only its href updates. */
.shop__featured-title,
.shop__featured-rating,
.shop__featured-excerpt,
.shop__featured-price {
  transition: opacity 1100ms var(--ease-standard), transform 1100ms var(--ease-standard);
}

.shop__featured-title.is-slide-hidden,
.shop__featured-rating.is-slide-hidden,
.shop__featured-excerpt.is-slide-hidden,
.shop__featured-price.is-slide-hidden {
  opacity: 0;
  transform: translateY(16px);
}

.shop__featured-title.is-slide-leaving,
.shop__featured-rating.is-slide-leaving,
.shop__featured-excerpt.is-slide-leaving,
.shop__featured-price.is-slide-leaving {
  transition-duration: 300ms;
  opacity: 0;
}

/* Natural height, but capped at 2 lines ("…" past that). It does NOT
   reserve a fixed 2-line box: a 1-line title stays one line so the rating
   sits directly beneath it. The banner's constant height is held by
   .shop__featured-body's own min-height (below) instead — the leftover of a
   short title lands as extra space above the pinned price/button footer,
   not as a blank second line under the title. */
.shop__featured-title {
  font-family: var(--font-heading);
  font-size: 26px;
  line-height: 1.2;
  color: var(--color-text);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* No hover reaction here by design — with the banner now auto-rotating
   every 10s, a color shift under a cursor that's just passing through
   mid-transition read as a glitch rather than an intentional link cue. */
.shop__featured-title a {
  color: inherit;
}

/* Same visual language as .shop__card-rating (its own file, below) —
   stars + review count — just a touch bigger to match the banner's
   larger scale. Star fill color is shared with every other star icon on
   the site via the components.css rule keyed on .shop__featured-star. */
.shop__featured-rating {
  margin-top: var(--space-3);
  display: flex;
  align-items: center;
  gap: 6px;
}

.shop__featured-star {
  width: 15px;
  height: 15px;
}

.shop__featured-rating-count {
  font-size: 13px;
  color: var(--color-text-faint);
}

/* Clamped to 3 lines (ellipsis past that) rather than left to wrap
   freely — same "cap it, don't let it grow unbounded" idea as the
   .clamp-2 utility (components.css) used on every card title/desc, just
   one line taller since this sits in a wider column. Caps how tall the
   excerpt can ever get, so .shop__featured's own min-height (below)
   reserves a real, calculable worst case instead of a guessed one. */
/* Fixed 3-line height too (already clamped to 3 via .clamp-3) — a shorter
   excerpt reserves the same 3 lines, so the body height stays constant
   across slides. Together with the fixed title above, the banner never
   changes height between slides → no jump during the slide animation. */
.shop__featured-excerpt {
  margin-top: var(--space-5);
  font-size: var(--fs-body);
  line-height: 1.7;
  color: var(--color-text-muted);
  /* Full column width (reaches the right edge) + a real 3-line clamp so a long
     description truncates instead of overflowing onto the price. min-height
     reserves the 3 lines so the banner keeps a constant height between slides. */
  max-width: none;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  overflow: hidden;
  min-height: 5.1em;
}

/* margin-top: auto pins this to the bottom of the body regardless of how
   many lines the excerpt above actually took — its position never
   depends on the excerpt's length. */
.shop__featured-footer {
  margin-top: auto;
  padding-top: var(--space-6);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-6);
}

.shop__featured-price {
  font-family: var(--font-heading);
  font-size: 48px;
  font-weight: 700;
  background: var(--gradient-brand);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

.shop__featured-buy {
  gap: var(--space-2);
}

.shop__featured-buy svg {
  width: 18px;
  height: 18px;
}

/* Tablet/mobile: banner stacks — image on top (restored 16:9), text below.
   Kicks in earlier (≤1024, not the old 860) so the 2-column banner never
   gets narrow enough for the picture and the text/price block to crowd. */
@media (max-width: 1024px) {
  .shop__featured {
    grid-template-columns: 1fr;
    gap: var(--space-4); /* half the desktop gap between image and text when stacked */
  }

  /* iOS Safari collapses `aspect-ratio` when the element's only children are
     absolutely-positioned (our slide images), making the banner tiny. The
     padding-top ratio box gives a reliable 16:9 height everywhere; align-self
     stops the stretched grid row from fighting that fixed height. */
  .shop__featured-media {
    height: 0;
    padding-top: 56.25%; /* 16:9 */
    align-self: start;
  }

  /* Stacked: title and excerpt use the FULL block width (no 46ch cap), so
     the excerpt fits in fewer lines — 2 here instead of 3 — which shrinks
     the whole block. The body's reserved height is recomputed for this
     2-line excerpt (min-height below) so slides still don't jump. */
  .shop__featured-excerpt {
    max-width: none;
    -webkit-line-clamp: 2;
    min-height: 3.4em;
  }

  .shop__featured-body {
    padding: var(--space-4) var(--space-8) var(--space-8);
    min-height: 0; /* hug the content when stacked — no dead space below the text */
  }
  /* Stacked: the price/buy row follows the text directly instead of being pinned
     to the bottom of a tall panel (which left a big gap under the description). */
  .shop__featured-price-row {
    margin-top: var(--space-5);
  }
}

.shop__filters {
  margin-bottom: var(--space-10);
}

/* align-items: start stops CSS Grid's default row-stretch from inflating
   cards when a filter leaves only a partial row — the grid's own locked
   min-height (js/listing.js) reserves a full page's worth of row space so
   pagination never jumps, but without this, that reserved space stretches
   whichever lone row IS showing (and, via .shop__card-body/-desc's own
   flex: 1, drags the price/button footer down inside it) instead of
   leaving the reserved space empty below a normal-height card. */
/* Up to 3 columns. The number of products shown per page follows the column
   count automatically (js/listing.js reads data-page-rows="2" and sets page
   size = 2 rows x columns): 3 cols -> 6 (3x2), 2 cols -> 4 (2x2), 1 col ->
   2 (1x2). */
.shop__grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  align-items: start;
  gap: var(--space-6);
}

/* min-height is the measured worst case: a 2-line (clamped) title plus a
   2-line (clamped) description — see the identical note on .works__card
   in css/works.css for why this beats reserving space on the title/desc
   elements themselves. */
.shop__card {
  display: flex;
  flex-direction: column;
  min-height: 382px;
  border-radius: var(--radius-md);
  background: var(--color-bg-elevated);
  border: 1px solid var(--color-border);
  overflow: hidden;
}

/* No transition declared here for transform/border-color/box-shadow —
   those live on the .shop__card:hover rule itself in components.css now,
   specifically to avoid fighting [data-filter-item]'s own transition
   (opacity + transform, staggered) over control of `transform`. See the
   long comment there. .shop__card:hover (the lift) and .shop__card-badge
   are defined once in css/components.css, shared with the equivalent
   Product/Services/Blog card recipes. The image itself does NOT zoom on
   hover here (unlike those other cards) — only the card lifts — so
   .shop__card is excluded
   from components.css's shared media-zoom rule and gets no local
   transform of its own on .shop__card-media img. Only the media's own
   aspect-ratio changes here (16:9, matching the product-photography
   standard used on the banner and the product page) — everything else
   about the card (width, title/desc/price sizing, footer) stays exactly
   as it was, so the card just reads shorter/slimmer overall. */
.shop__card-media {
  aspect-ratio: 16 / 9;
}

.shop__card-body {
  padding: var(--space-4) var(--space-4) var(--space-5);
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  flex: 1;
}

/* The line-clamp (and therefore the truncation "…") lives on the <a>
   itself, not the h3 — the ellipsis is painted by whichever element owns
   -webkit-line-clamp, so putting the clamp AND the color/hover on the same
   directly-hovered <a> is the only way Chromium reliably repaints the "…"
   in the hover color too. The h3 is forced back to a plain block so its
   .clamp-2 utility class doesn't create a second, competing clamp box. */
.shop__card-title {
  font-size: 16px;
  font-weight: 700;
  line-height: 1.3;
  display: block;
}

.shop__card-title a {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
  color: var(--color-text);
  transition: color var(--duration-fast) var(--ease-standard);
}

.shop__card-title a:hover {
  color: var(--color-hover-accent);
}

.shop__card-desc {
  font-size: var(--fs-small);
  color: var(--color-text-muted);
  line-height: 1.5;
}

/* margin-top: auto docks the rating (and .shop__card-footer right after
   it, in normal flow) to the BOTTOM of the card, consuming whatever
   space title+desc didn't need above it — so rating/price/buy always
   sit at the exact same distance from the card's bottom edge on every
   card, regardless of whether that card's title/desc run 1 line or 2.
   Reference point: "Character Rigging From Zero" (a 2-line title) is
   tall enough that this rule barely has to push anything; every other,
   shorter card gets pushed down to match it exactly. */
.shop__card-rating {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-top: auto;
}

.shop__card-star {
  width: 13px;
  height: 13px;
}

.shop__card-rating-count {
  font-size: 12px;
  color: var(--color-text-faint);
}

.shop__card-footer {
  margin-top: var(--space-1);
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.shop__card-price {
  font-family: var(--font-heading);
  font-size: 20px;
  font-weight: 700;
  background: var(--gradient-brand);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

/* Compact .btn--cta variant — same recipe, smaller footprint so it fits a
   grid card's footer row instead of the banner's full-size button, wide
   enough that "Buy Now" doesn't feel cramped against its own padding. */
.shop__card-buy {
  height: 34px;
  padding: 0 var(--space-4);
  gap: 5px;
  font-size: 12px;
  white-space: nowrap;
}

.shop__card-buy svg {
  width: 13px;
  height: 13px;
}

/* Tablet: 2 columns (2x2). */
@media (max-width: 1024px) {
  .shop__grid {
    grid-template-columns: repeat(2, 1fr);
  }
}

/* Mobile: keep a 2x2 grid but shrink the cards (shorter box, tighter body,
   smaller title/desc) so two fit comfortably on a phone. */
@media (max-width: 560px) {
  .shop__grid {
    grid-template-columns: repeat(2, 1fr);
    gap: var(--space-5) var(--space-3);
  }
  /* One fixed height for every card (measured worst case: 16:9 image +
     2-line title + 2-line desc + rating row + reserved gap + price/buy row),
     so cards in the same row read as the same box regardless of how long
     their title/desc run. Title/desc keep natural (clamped) height so a
     1-line title lets the desc sit right beneath it; the rating docks to
     the bottom (margin-top:auto), so a short card's leftover lands as
     reserved space between the desc and the rating/price, not as gaps in
     the text. */
  .shop__card {
    min-height: 262px;
  }
  .shop__card-body {
    padding: var(--space-3);
    padding-bottom: var(--space-2);
  }
  .shop__card-title {
    font-size: 14px;
  }
  .shop__card-desc {
    font-size: 12px;
  }
  /* Price 1.5x smaller than its 20px base and its vertical breathing room
     tightened (footer flush to the rating above, body bottom-padding trimmed
     above) so the price/buy row sits compactly on the narrow 2-up card. */
  .shop__card-price {
    font-size: calc(20px / 1.5);
  }
  .shop__card-footer {
    margin-top: 0;
  }
  /* Banner price 1.5x smaller on phones (48px base → 32px). */
  .shop__featured-price {
    font-size: 32px;
  }
}

/* Mobile: drop the cart icon on Buy buttons — the label alone is enough and
   the narrow 2-up cards have no room for it. */
@media (max-width: 767px) {
  .shop__card-buy svg,
  .shop__featured-buy svg {
    display: none;
  }
}


/* Approximate secondary currency ("≈ $24" / "≈ 990 ₴") next to the primary
   price — smaller and muted, wherever a price is shown (card / product / related). */
.shop__card-price-alt,
.shop__featured-price-alt,
.product__price-alt,
.product__related-price-alt {
  font-size: 0.72em;
  font-weight: 500;
  color: var(--color-text-faint);
  margin-left: 3px;
  white-space: nowrap;
}

/* Free "0+" price (shop cards / product page) */
.rm-free-price { white-space: nowrap; }
