/* ============================================================
   TrendiestStore — product cards + product page

   1  Hanging keychain on the card
   2  Card sizing
   3  Product page layout: base (stacked) state
   4  Gallery: one consistent media box, thumbnails on the same screen
   5  Desktop: the two columns (gallery | buy block)
   6  Full-width description below both columns
   7  Responsive

   Sections 3 and 5 are split, and in that order, on purpose: the
   desktop grid has to be declared AFTER the gallery rules in section 4
   so its --ts-stage override lands last in the cascade.
   ============================================================ */

/* ---------- 1. the keychain hanging off the card -------------- */
/* It hangs OUTSIDE the card, so the card wrapper cannot clip it and
   needs room above for the thread. The card itself keeps its own
   overflow so the photo hover-zoom still crops correctly. */
.card-wrapper.ts-has-hang {
  position: relative;
  overflow: visible;
  padding-top: 1.6rem; /* room for the knot + thread above the card */
}

.ts-hang {
  --hang-w: 8.6rem;
  position: absolute;
  top: 0;
  right: 1.2rem;
  z-index: 4;
  width: var(--hang-w);
  pointer-events: none;
  transform-origin: 50% 0;
}

.ts-hang__knot {
  position: absolute;
  top: 0;
  left: 50%;
  width: 1.1rem;
  height: 1.1rem;
  margin-left: -0.55rem;
  border-radius: 50%;
  background: var(--ts-accent, var(--ts-brand));
  box-shadow: 0 0 0 3px rgb(var(--color-background)), 0 2px 6px var(--ts-knot-shadow);
}

.ts-hang__swing {
  display: block;
  transform-origin: 50% 0;
  animation: ts-hang-idle 4.6s ease-in-out infinite;
  will-change: transform;
}

.ts-hang__thread {
  display: block;
  width: 2px;
  height: 2.8rem;
  margin: 0 auto;
  border-radius: 2px;
  background: linear-gradient(180deg, var(--ts-accent, var(--ts-brand)), var(--ts-thread-fade));
}

.ts-hang__img {
  display: block;
  width: 100%;
  height: auto;
  margin-top: calc(var(--hang-w) * -0.16);
  filter: var(--ts-sh-hang);
  animation: ts-hang-lag 4.6s ease-in-out infinite;
  transform-origin: 50% 14%;
  will-change: transform;
}

.card-wrapper.ts-has-hang:hover .ts-hang__swing,
.card-wrapper.ts-has-hang:hover .ts-hang__img {
  animation-duration: 2.4s;
}

/* Keyframes are declared here as well as in ts-hero-build.css: cards
   show up on collection, search and home pages where the hero
   stylesheet is not loaded. */
@keyframes ts-hang-idle {
  0%,
  100% {
    transform: rotate(-4.5deg);
  }
  50% {
    transform: rotate(4.5deg);
  }
}

@keyframes ts-hang-lag {
  0%,
  100% {
    transform: rotate(2.2deg);
  }
  50% {
    transform: rotate(-2.2deg);
  }
}

/* ---------- 2. card type ---------------------------------------
   Three voices on a card, and they are meant to be told apart at a
   glance rather than by reading:

     title     the display serif (--ts-font-display, set in
               layout/theme.liquid from the `ts_display_font` setting).
               A high-contrast serif next to Poppins is the whole reason
               these read as a beauty brand rather than as stock theme
               cards — it is the ONE place the serif is used at size.
     tagline   body font, italic, muted. Deliberately quiet: it is a
               half-sentence of atmosphere under a very long SEO title.
     price     body font, semibold, TABULAR figures, near-black. The
               loudest thing on the tile after the photo.

   The price is sans and not serif on purpose. A didone serif thins its
   strokes exactly where a price needs them — the digits and the decimal
   — and "Rs.599.00 PKR" at 20px in Playfair loses the point separator
   on a phone. Elegance belongs on the title; the price has one job.

   SPECIFICITY: component-card.css and component-price.css are
   stylesheet_tag-ed from inside the section body, so they load AFTER
   this file and win every (0,1,0) tie. Everything here carries
   `.card-wrapper` (or two classes) to take it one step clear — dropping
   that prefix silently reverts the card to Dawn's own type. */

.card-wrapper .card__heading {
  /* Nested fallback, not a comma list: if --ts-font-display is ever
     undefined the whole `font-family: , Poppins` declaration would be
     invalid and dropped, taking the heading font with it. This way an
     unset variable lands on the heading font instead. */
  font-family: var(--ts-font-display, var(--font-heading-family));
  font-style: var(--ts-font-display-style, normal);
  font-weight: var(--ts-font-display-weight, 500);
  font-size: 1.95rem;
  line-height: 1.28;
  /* the serif is drawn on a wider body than Poppins, so it needs the
     tracking pulled back in to sit at the same optical width */
  letter-spacing: -0.005em;
  color: var(--ts-card-ink);
}

.card-wrapper .card__heading a {
  text-underline-offset: 0.22em;
  text-decoration-thickness: 0.06em;
}

/* the metafield half-sentence, under the title */
.card-wrapper .ts-card__tagline {
  font-style: italic;
  font-size: 1.35rem;
  line-height: 1.5;
  letter-spacing: 0.005em;
  color: var(--ts-card-muted);
}

/* ---- price ---------------------------------------------------
   `price--on-sale` swaps `.price__regular` out for `.price__sale`,
   which holds the struck compare-at and then the live price. Both are
   `.price-item`, so they are separated by role, not by element. */
.card-wrapper .price {
  font-size: 2rem;
  line-height: 1.35;
  letter-spacing: 0;
  /* one digit, one width — otherwise a column of cards has its decimal
     points in four different places */
  font-variant-numeric: tabular-nums;
  font-feature-settings: 'tnum' 1;
  color: var(--ts-card-ink);
}

.card-wrapper .price .price-item--regular,
.card-wrapper .price .price-item--sale {
  font-weight: 600;
}

/* the "was" figure: present, checkable, and clearly not the one being
   charged — smaller, lighter and struck, never the same weight as the
   live price */
.card-wrapper .price--on-sale .price-item--regular {
  font-size: 1.45rem;
  font-weight: 500;
  color: var(--ts-card-muted);
  text-decoration-thickness: 0.08em;
  text-underline-offset: 0;
}

.card-wrapper .price--on-sale .price-item--sale {
  color: var(--ts-brand-700);
}

.card-wrapper .price--sold-out .price-item {
  color: var(--ts-card-muted);
}

/* ---- sale / sold-out badge ------------------------------------
   A soft brand pill rather than Dawn's filled scheme block: it sits on
   the photo, so a solid slab reads as damage to the picture. */
.card-wrapper .card__badge .badge {
  font-family: var(--font-body-family);
  font-size: 1.15rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  padding: 0.5rem 1.1rem;
  border: 0;
  color: var(--ts-on-brand);
  background: var(--ts-brand);
  box-shadow: var(--ts-sh-brand);
}

/* The two badges are told apart only by the colour scheme each one is
   given in theme settings — there is no `badge--sold-out` class in
   Dawn's markup. Sold out is scheme-3 here (sale is scheme-5), so a
   pack that has run out reads as grey and factual rather than as
   another offer. Change the setting and this selector has to follow. */
.card-wrapper .card__badge .badge.color-scheme-3 {
  background: var(--ts-card-muted);
  box-shadow: none;
}

/* ---------- 3. product page layout ----------------------------- */
/* AliExpress-style split on desktop: the gallery holds the left column,
   the whole buy block (title, subtitle, price, offer, blurb, colour,
   quantity, add to cart, buy it now, what's inside) stacks down the
   right column, and the long description breaks out full-width below
   both — sections 5 and 6.

   Below 990px the grid collapses and the page simply stacks: gallery
   first, details under it. That is the base state declared here; the
   two columns are switched on inside the desktop media query. */
/* IMPORTANT — specificity note.
   Dawn loads section-main-product.css from inside the product section,
   i.e. AFTER this file in the <head>, so at equal specificity Dawn wins.
   Its column rules are `.product--large:not(.product--no-media)
   .product__media-wrapper` (three classes). Every override below is
   therefore prefixed with the `product-info` custom element, which adds
   a type selector and takes the rule one step clear. Dropping that
   prefix silently reverts the page to Dawn's own column widths. */
product-info .product {
  display: block;
}

product-info .product--large:not(.product--no-media) .product__info-wrapper,
product-info .product--medium:not(.product--no-media) .product__info-wrapper,
product-info .product--small:not(.product--no-media) .product__info-wrapper,
product-info .product__info-wrapper {
  max-width: 100%;
  width: 100%;
  padding: 0;
  margin: 2.8rem 0 0;
}

/* Full width, but the children are constrained so nothing stretches to
   a 1500px line: prose gets a readable measure, the controls get a
   comfortable button width. A left-hand rail keeps it aligned with the
   gallery above instead of drifting to the page centre. */
product-info .product__info-container {
  max-width: 100%;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}

product-info .product__info-container > * {
  width: 100%;
  max-width: 78rem;
}

/* pills and badges must keep their intrinsic width — the rule above
   would otherwise stretch the offer badge across the whole column */
product-info .product__info-container > .ts-product__offer {
  width: auto;
  max-width: 100%;
  margin: 0 0 1.6rem;
}

/* tighten the vertical rhythm between the buy-block rows */
product-info .product__info-container > .product__title {
  margin-bottom: 0.8rem;
}

product-info .product__info-container > [id^='price-'],
product-info .product__info-container > .price {
  margin-bottom: 0.6rem;
}

product-info .product__info-container > .product__tax {
  margin-bottom: 0.8rem;
}

/* .product__description is deliberately absent: the long description
   no longer renders inside this column at all — see section 6. */
.product__info-container > .ts-product__blurb,
.product__info-container > .ts-product__tagline {
  max-width: 90ch;
}

/* the actual buy controls sit at a sensible button width */
.product__info-container > variant-selects,
.product__info-container > .product-form__quantity,
.product__info-container > product-form,
.product__info-container > .product-form,
.product__info-container > .share-button {
  max-width: 46rem;
}

.product-form__buttons {
  max-width: 46rem;
}

.product-form__submit,
.shopify-payment-button__button {
  min-height: 5.2rem;
  font-size: 1.7rem;
}

.product__info-container > .ts-inside,
.product__info-container > .ts-swatches {
  max-width: 90rem;
}

.product__title,
.product__title h1 {
  font-size: 2.8rem;
  line-height: 1.25;
}

/* ---------- 4. gallery: one consistent media box --------------- */
/* Every image AND the video render into the same square stage, so the
   main view never resizes as you click through the thumbnails — that
   was the "inconsistent size" problem. Square is the neutral choice
   here because the media is a mix of portrait studio photos, landscape
   cut-outs and a portrait video; `contain` letterboxes each one into
   the identical box instead of reflowing the page.

   The stage is sized so it plus the thumbnail row fit one screen, the
   way the marketplace galleries do. */
product-info .product--large:not(.product--no-media) .product__media-wrapper,
product-info .product--medium:not(.product--no-media) .product__media-wrapper,
product-info .product--small:not(.product--no-media) .product__media-wrapper,
product-info .product__media-wrapper {
  /* --ts-stage sizes the square; the wrapper is wider so the thumbnail
     rail underneath can show more of the 17 media at once */
  --ts-stage: min(44rem, 100%, 45vh);
  width: min(76rem, 100%);
  max-width: min(76rem, 100%);
  padding: 0;
  margin-inline: auto;
}

.product__media-wrapper .product__media-list,
.product__media-wrapper .product__media-gallery {
  margin: 0;
  padding: 0;
}

/* The container is the ONE thing that defines the size — a square,
   clipped. Dawn otherwise sizes the inner .media from its own ratio
   variables, which came out 608px wide inside a 456px column and spilled
   over the thumbnails below.
   Everything inside is then pinned to fill that square absolutely, so
   image slides, the video poster and the video itself all land in an
   identical box and switching between them never moves the page. */
product-info .product__media-item .product-media-container {
  position: relative;
  aspect-ratio: 1 / 1;
  width: var(--ts-stage);
  max-width: 100%;
  margin-inline: auto;
  height: auto !important;
  min-height: 0 !important;
  /* Dawn builds its aspect box with `padding-top: var(--ratio-percent)`
     on .media (note: TOP, not bottom). Left alone, a 1086x1448 photo
     makes .media 133% of its own width — 608px inside a 456px column —
     which overflowed the stage and pushed under the thumbnail row. */
  padding-top: 0 !important;
  padding-bottom: 0 !important;
  overflow: hidden;
  background: var(--ts-plate);
  border-radius: var(--ts-r);
}

product-info .product__media-item .product__modal-opener,
product-info .product__media-item .product__media.media,
product-info .product__media-item .deferred-media,
product-info .product__media-item .deferred-media__poster,
product-info .product-media-container .media {
  position: absolute;
  inset: 0;
  width: 100% !important;
  height: 100% !important;
  max-width: 100%;
  aspect-ratio: auto !important;
  padding-top: 0 !important;
  padding-bottom: 0 !important;
  margin: 0;
}

product-info .product__media-item img,
product-info .product__media-item video,
product-info .product__media-item iframe,
product-info .product__media-item model-viewer {
  width: 100% !important;
  height: 100% !important;
  object-fit: contain;
}

/* the zoom hit-area and the play button stay clickable above the media */
product-info .product__media-item .product__media-toggle {
  position: absolute;
  inset: 0;
  z-index: 2;
}

/* centred on the poster, not stacked under it */
product-info .product__media-item .deferred-media__poster-button {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  margin: 0;
  z-index: 3;
}

/* thumbnails: one row directly under the stage, always on screen.
   The stage height is capped in vh as well as rem so that this row
   stays above the fold on a short laptop window, not just a tall one. */
.thumbnail-slider,
.product__media-gallery .thumbnail-list {
  margin-top: 0.8rem;
}

/* Dawn pads the slider for its scrollbar and buttons; that padding was
   adding ~70px of dead height under the rail. */
product-info .thumbnail-slider {
  padding: 0;
}

product-info .thumbnail-slider .thumbnail-list {
  padding-top: 0;
  padding-bottom: 0.4rem;
  margin-bottom: 0;
}

.thumbnail-list {
  display: flex;
  flex-wrap: nowrap;
  gap: 0.9rem;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  padding-bottom: 0.4rem;
  /* `justify-content: center` on a scroller whose content OVERFLOWS puts
     the leading items at a negative offset, and scrollLeft cannot go
     below 0 — with 17 media on a phone the first six thumbnails sat at
     x = -528 and could never be scrolled back into view. `safe center`
     centres only while everything fits and falls back to flex-start the
     moment it does not; the plain `flex-start` above it is the fallback
     for engines that do not know the `safe` keyword. Order matters. */
  justify-content: flex-start;
  justify-content: safe center;
  align-items: flex-start;
}

.thumbnail-list__item {
  flex: 0 0 auto;
  width: 7.6rem;
  max-width: 7.6rem;
  padding-bottom: 0 !important;
  scroll-snap-align: start;
}

/* Dawn builds the thumbnail's square with `::before { padding-bottom:
   100% }` and then lays the button over it with `position: absolute`.
   The rule below deliberately makes that button `position: relative` so
   it can carry its own height — which drops it BELOW the spacer instead
   of over it, leaving a full thumbnail's worth of dead air above every
   tile and doubling the rail to 158px for a 79px thumbnail. The spacer
   has no job left once the button sizes itself, so it goes. */
/* `.thumbnail-list` is carried so this is (0,2,0): section-main-product.css
   declares the spacer at (0,1,0) but is stylesheet_tag-ed from inside the
   product section, so it lands after this file and would otherwise win. */
.thumbnail-list .thumbnail-list__item::before {
  content: none;
}

.thumbnail-list__item .thumbnail {
  position: relative;
  height: 7.6rem;
  border-radius: calc(var(--ts-r) * 0.5);
  background: var(--ts-plate);
}

.thumbnail img {
  object-fit: contain;
  padding: 0.3rem;
}

/* the slider arrows would eat width the thumbnails need */
.thumbnail-slider .slider-button {
  display: none;
}

/* ---------- 5. desktop: the two columns ----------------------- */
/* The gallery used to be centred inside a full-width block, which on a
   laptop read as one photo stranded in the middle of the page. It now
   fills the left column of a real grid and the buy block owns the
   right one. */
@media screen and (min-width: 990px) {
  /* `.product.grid { gap: 0 }` in Dawn is (0,2,0), so the `.grid`
     class has to be carried here or the column gap is zeroed out. */
  product-info .product,
  product-info .product.grid {
    display: grid;
    /* the details column is given the slightly larger share: it carries
       the title, price, options and both buttons, and a square gallery
       gets tall fast if its column is allowed to grow */
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.1fr);
    column-gap: clamp(3rem, 3.8vw, 5.5rem);
    row-gap: 0;
    align-items: start;
  }

  /* Dawn's `.grid__item` caps every child at `50% - gap/2` and pins a
     width on it. A grid item still honours `width`, so left alone that
     25% would win over the track sizing above.

     The two rules that follow have to OUT-rank this one, so each of
     them carries a `.product >` form at the same (0,2,1) and lands
     later in the file. Weakening either back to
     `product-info .product__media-wrapper` silently hands the width
     back to this reset. */
  product-info .product > .grid__item {
    width: auto;
    max-width: none;
    min-width: 0;
  }

  product-info .product--large:not(.product--no-media) .product__media-wrapper,
  product-info .product--medium:not(.product--no-media) .product__media-wrapper,
  product-info .product--small:not(.product--no-media) .product__media-wrapper,
  product-info .product > .product__media-wrapper {
    grid-column: 1;
    /* the stage fills its own column now, so there is no leftover
       space for it to sit centred in */
    --ts-stage: 100%;
    width: 100%;
    max-width: 100%;
    margin-inline: 0;
  }

  product-info .product--large:not(.product--no-media) .product__info-wrapper,
  product-info .product--medium:not(.product--no-media) .product__info-wrapper,
  product-info .product--small:not(.product--no-media) .product__info-wrapper,
  product-info .product > .product__info-wrapper {
    grid-column: 2;
    width: auto;
    max-width: none;
    margin: 0;
    padding: 0;
  }

  /* In the stacked layout the info column was full page width, so its
     children were capped to keep a readable measure. In a ~700px column
     those caps only make the controls look under-sized, so everything
     goes back to filling the column. */
  product-info .product__info-container {
    max-width: 100%;
  }

  product-info .product__info-container > *,
  product-info .product__info-container > variant-selects,
  product-info .product__info-container > .product-form__quantity,
  product-info .product__info-container > product-form,
  product-info .product__info-container > .product-form,
  product-info .product__info-container > .share-button,
  product-info .product__info-container > .ts-inside,
  product-info .product__info-container > .ts-swatches {
    max-width: 100%;
  }

  product-info .product__info-container .product-form__buttons {
    max-width: 100%;
  }

  /* pills keep their intrinsic width — the rule above would otherwise
     stretch the offer badge across the column */
  product-info .product__info-container > .ts-product__offer {
    width: auto;
    max-width: 100%;
  }
}

/* Short laptop windows: a square stage that fills a 640px column is
   taller than the window. Capping the WRAPPER (not the stage) keeps the
   thumbnail rail the same width as the image above it. */
@media screen and (min-width: 990px) and (max-height: 880px) {
  product-info .product--large:not(.product--no-media) .product__media-wrapper,
  product-info .product--medium:not(.product--no-media) .product__media-wrapper,
  product-info .product--small:not(.product--no-media) .product__media-wrapper,
  product-info .product > .product__media-wrapper {
    width: min(100%, 64vh);
  }
}

/* ---------- 6. full-width description ------------------------- */
/* Rendered by sections/main-product.liquid outside the grid above, so
   it spans the whole page under both columns. */
.ts-product__full {
  margin-top: clamp(3rem, 5vw, 5.5rem);
  padding-top: clamp(2.4rem, 4vw, 3.6rem);
  border-top: 1px solid rgba(var(--color-foreground), 0.14);
}

.ts-product__full-h {
  margin: 0 0 1.6rem;
  font-size: clamp(2.1rem, 2.4vw, 2.8rem);
  font-weight: 800;
  letter-spacing: -0.03em;
  line-height: 1.2;
}

.ts-product__full-body {
  font-size: 1.55rem;
  line-height: 1.72;
  color: rgba(var(--color-foreground), 0.82);
  /* full-WIDTH block, but the prose inside still gets a readable
     measure rather than a 1400px line */
  max-width: 92ch;
}

.ts-product__full-body > :first-child {
  margin-top: 0;
}

.ts-product__full-body > :last-child {
  margin-bottom: 0;
}

.ts-product__full-body img {
  max-width: 100%;
  height: auto;
  border-radius: var(--ts-r);
}

.ts-product__full-body table {
  display: block;
  overflow-x: auto;
  max-width: 100%;
}

/* ---------- 7. responsive -------------------------------------- */
@media screen and (max-width: 989px) {
  product-info .product--large:not(.product--no-media) .product__media-wrapper,
  product-info .product--medium:not(.product--no-media) .product__media-wrapper,
  product-info .product--small:not(.product--no-media) .product__media-wrapper,
  product-info .product__media-wrapper {
    --ts-stage: min(38rem, 100%, 48vh);
  }
  .thumbnail-list__item {
    width: 6.6rem;
    max-width: 6.6rem;
  }
  .thumbnail-list__item .thumbnail {
    height: 6.6rem;
  }
  .product__title,
  .product__title h1 {
    font-size: 2.4rem;
  }
}

@media screen and (max-width: 749px) {
  .ts-hang {
    --hang-w: 6.6rem;
    right: 0.8rem;
  }
  product-info .product--large:not(.product--no-media) .product__media-wrapper,
  product-info .product--medium:not(.product--no-media) .product__media-wrapper,
  product-info .product--small:not(.product--no-media) .product__media-wrapper,
  product-info .product__media-wrapper {
    --ts-stage: 100%;
  }
}

@media (prefers-reduced-motion: reduce) {
  .ts-hang__swing,
  .ts-hang__img {
    animation: none !important;
    transform: none !important;
  }
}

/* ---------- 8. zoom modal: closed means closed ----------------
   Dawn hides the media modal with `visibility: hidden; opacity: 0` on a
   position:fixed, full-viewport element. It is therefore still laid
   out, still the size of the screen, and still sitting at scroll
   position 0 — so the lazy-loading heuristic treats its images as being
   in the viewport and fetches them. On a 16-image product that meant
   full-resolution originals being downloaded for a zoom view most
   visitors never open (measured: ~100 KB on a 1440x900 desktop, more on
   a taller screen, and the first of them at the ORIGINAL 1086x1448
   because the modal's srcset offers nothing between 550w and the
   original).

   display:none while closed stops all of it. The [open] attribute is
   set by ModalDialog.show() BEFORE product-modal.js reads clientWidth
   and calls scrollIntoView(), so the modal is laid out by the time
   anything measures it, and there is no opacity transition on this
   element to lose. The :not([open]) selector also outranks Dawn's own
   `.product-media-modal` rule on specificity, so it wins regardless of
   which stylesheet the browser sees last. */
.product-media-modal:not([open]) {
  display: none;
}

/* ---------- 9. desktop: the collection grid is a BOX -----------
   THE BUG THIS FIXES. On a desktop the featured-collection grid ran
   the full width of the screen — first card flush against x=0, fourth
   card clipped by the right edge, cards 403px wide against 306px for
   the identical card on the collection page.

   It was not the column count. sections/featured-collection.liquid
   wraps the grid in `<slider-component class="page-width
   page-width-desktop">`, and `slider-component { display: block }`
   lives in component-slider.css — which that section only loads when
   `swipe_on_mobile` or `enable_desktop_slider` is on. Both are off, so
   the stylesheet never arrives and the custom element falls back to the
   unknown-element default, `display: inline`. An inline box ignores
   width, max-width and `margin: 0 auto`, so every constraint
   `.page-width` puts on it was computed and then thrown away, and the
   <ul> inside sized itself against the full-width section instead.

   Restoring the load is the wrong fix: that stylesheet was taken off
   the critical path deliberately (see the comment above the conditional
   in that section) and it would change the phone and tablet layouts
   too. One declaration, desktop only, does it — `.page-width` then
   works exactly as it already does on the collection page, and both
   grids land on the same 194→1514 content column as the section
   heading above them.

   SCOPED TO >= 990px on purpose. The element is inline on a phone as
   well, but ts-mobile.css already compensates there with its own
   `.slider-mobile-gutter .product-grid` gutter, and the one-per-row
   phone layout is signed off. Nothing below 990px is touched. */
@media screen and (min-width: 990px) {
  .collection slider-component.page-width,
  .collection slider-component.page-width-desktop {
    display: block;
  }

  /* Same height as well as same width. The cards stretch to a common
     height inside a flex row for free, but the CONTENT did not line up:
     a three-line tagline on one card pushed its price and its Add to
     cart 25px below the ones either side of it, and a card alone on a
     second row came out shorter than the four above it.

     Reserving the title at three lines and the tagline at two makes
     every card identical — 306x752 at 1707px wide — so the photo, the
     title, the price and the button all sit on the same four lines
     across the whole grid.

     The clamp hides the SEO tail of the title, never the product: the
     colourway and the product name are both inside the first three
     lines. The full title is still the link text and still what a
     screen reader announces, exactly as on the phone layout.

     `.card-wrapper` carries these to (0,2,0) so they clear
     component-card.css, which the section body loads after this file. */
  .card-wrapper .card__heading {
    line-height: 1.3;
    min-height: calc(3 * 1.3em);
  }

  .card-wrapper .card__heading > a {
    display: -webkit-box;
    -webkit-line-clamp: 3;
    line-clamp: 3;
    -webkit-box-orient: vertical;
    overflow: hidden;
  }

  .card-wrapper .ts-card__tagline {
    display: -webkit-box;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    line-height: 1.5;
    min-height: calc(2 * 1.5em);
  }
}
