.ts-reviews { --ts-star-color: #FDD314; --ts-card-min: 280px; }
.ts-reviews__viewport { overflow: hidden; }
.ts-reviews__track { list-style: none; margin: 0; padding: 2px; display: flex; gap: var(--gap-md); }
.ts-reviews--slider {
  /* Declared here, not on .ts-reviews__viewport, so it's in scope for
     the peeks below too -- they're siblings of the viewport (positioned
     relative to the root so they hold still while it scrolls, see
     .ts-reviews__peek), not its descendants, so a custom property scoped
     to the viewport alone wouldn't reach them. */
  --ts-card-basis: clamp(320px, 46vw, 560px);
  position: relative;
  /* Clips the decorative peeks below, which sit at a negative offset
     outside the visible carousel -- without this they'd add invisible
     horizontal overflow to the page. */
  overflow: hidden;
}
.ts-reviews--slider .ts-reviews__viewport {
  position: relative;
  /* Stacked above the peeks (z-index 0, see below) so every real,
     opaque card the user actually scrolls to covers the decorative
     sliver sitting behind it in that same screen position. */
  z-index: 1;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
  padding-inline: calc(50% - (var(--ts-card-basis) / 2));
  -webkit-mask-image: linear-gradient(to right, transparent 0, black var(--ts-fade-width, 64px), black calc(100% - var(--ts-fade-width, 64px)), transparent 100%);
  mask-image: linear-gradient(to right, transparent 0, black var(--ts-fade-width, 64px), black calc(100% - var(--ts-fade-width, 64px)), transparent 100%);
}
.ts-reviews--slider .ts-reviews__viewport::-webkit-scrollbar { display: none; }
.ts-reviews--slider .ts-review {
  scroll-snap-align: center;
  flex: 0 0 var(--ts-card-basis);
  opacity: .5;
  transform: scale(.92);
  transition: opacity .35s ease, transform .35s ease;
}
.ts-reviews--slider .ts-review.is-active { opacity: 1; transform: scale(1); }
/* Fixed in the same screen position regardless of scroll offset (siblings
   of .ts-reviews__viewport, not inside it), positioned exactly where the
   real previous/next card would sit if the track had one -- i.e. one
   card-width plus one gap outside the real track's own edge padding. See
   render_peek() in the PHP for why they exist and why they're safe to
   ignore for navigation.
   Qualified as .ts-reviews--slider .ts-reviews__peek (not just
   .ts-reviews__peek) so this beats the plain .ts-review rule further
   below on specificity, not source order -- both set `position`, and
   .ts-review's is the one that must lose.
   Each one only shows while its own edge is genuinely empty: peek-start
   only on .is-first (nothing real precedes card 0), peek-end only on
   .is-last (nothing real follows the last card). Without this gating
   they'd sit permanently at those screen positions and collide with
   whatever REAL card is legitimately peeking there on every other slide
   -- is-first ships in the initial HTML (see shortcode()), is-last is
   added/removed by ts-reviews.js as the active card changes. */
.ts-reviews--slider .ts-reviews__peek { display: none; }
.ts-reviews--slider.is-first .ts-reviews__peek--start,
.ts-reviews--slider.is-last .ts-reviews__peek--end {
  display: block;
  position: absolute;
  top: 2px;
  width: var(--ts-card-basis);
  z-index: 0;
  pointer-events: none;
}
.ts-reviews--slider.is-first .ts-reviews__peek--start {
  left: calc(50% - (var(--ts-card-basis) * 1.5) - var(--gap-md));
  -webkit-mask-image: linear-gradient(to right, transparent 0, black var(--ts-fade-width, 64px));
  mask-image: linear-gradient(to right, transparent 0, black var(--ts-fade-width, 64px));
}
.ts-reviews--slider.is-last .ts-reviews__peek--end {
  right: calc(50% - (var(--ts-card-basis) * 1.5) - var(--gap-md));
  -webkit-mask-image: linear-gradient(to left, transparent 0, black var(--ts-fade-width, 64px));
  mask-image: linear-gradient(to left, transparent 0, black var(--ts-fade-width, 64px));
}
@media (max-width: 1023px) {
  .ts-reviews--slider { --ts-card-basis: 60vw; }
}
.ts-reviews--grid .ts-reviews__track { display: grid; grid-template-columns: repeat(auto-fit, minmax(var(--ts-card-min), 1fr)); }
.ts-reviews--masonry .ts-reviews__track { display: block; column-width: var(--ts-card-min); column-gap: var(--gap-md); }
.ts-reviews--masonry .ts-review { break-inside: avoid; margin-bottom: var(--gap-md); }
/* Desktop-only: hovering a card dims its siblings, echoing the slider's
   faded-inactive-card treatment. Applies to both non-slider layouts
   (grid and masonry). Full opacity is the resting state -- fading every
   card by default (with no active-card anchor the way the slider always
   has one) made the whole grid read as a permanent hazy wash across the
   viewport, so the fade is hover-triggered per group via :has() instead
   of an always-on default. Opacity only (no scale, unlike the slider) --
   scaling a card risks visually overlapping its neighbour, whether
   that's the next column in masonry or the next cell in grid. Scoped to
   real hover-capable pointers so touch devices always show full opacity
   (there's no hover state to fade out of on tap). */
@media (hover: hover) and (pointer: fine) {
  .ts-reviews--grid .ts-review,
  .ts-reviews--masonry .ts-review {
    transition: opacity .35s ease;
  }
  .ts-reviews--grid:has(.ts-review:hover) .ts-review:not(:hover),
  .ts-reviews--masonry:has(.ts-review:hover) .ts-review:not(:hover) {
    opacity: .5;
  }
}
.ts-review {
  position: relative;
  display: flex; flex-direction: column; gap: var(--gap-sm);
  background: var(--pure-white);
  border-radius: var(--radius-2);
  box-shadow: var(--box-shadow-shallow);
  padding: var(--gap-md);
}
.ts-review__quote { width: 28px; height: 21px; color: var(--bde-links-color); opacity: .8; flex-shrink: 0; }
.ts-review__quote path { fill: currentColor; }
.ts-review__text {
  margin: 0; flex-grow: 1;
  color: color-mix(in srgb, var(--bde-body-text-color), transparent 32%);
  font-size: 1rem; line-height: 1.5;
}
.ts-review__meta { display: flex; align-items: center; gap: 12px; margin: 0; }
.ts-review__avatar {
  display: flex; align-items: center; justify-content: center; flex-shrink: 0;
  width: 40px; height: 40px; border-radius: 50%;
  background: color-mix(in srgb, var(--bde-brand-primary-color), transparent 82%);
  color: var(--bde-brand-primary-color);
  font-weight: 700; font-size: .8rem; font-family: var(--bde-heading-font-family);
}
.ts-review__who { display: flex; flex-direction: column; line-height: 1.3; }
.ts-review__name { font-weight: 700; color: var(--bde-headings-color); font-size: .9rem; }
.ts-review__sub { font-size: .78rem; color: color-mix(in srgb, var(--bde-body-text-color), transparent 45%); }
.ts-review__stars { display: inline-flex; gap: 2px; flex-shrink: 0; }
.ts-star { width: 16px; height: 16px; flex-shrink: 0; }
.ts-star--filled { color: var(--ts-star-color); }
.ts-star--filled path { fill: currentColor; }
.ts-star--empty { color: color-mix(in srgb, var(--ts-star-color), transparent 75%); }
.ts-star--empty path { fill: currentColor; }
/* Centered to match the slider's own active card (see the 2026-09-11
   centring fix above) -- right-aligned read as off-balance once the
   carousel itself was centred instead of flush-left. */
.ts-reviews__nav { display: flex; justify-content: center; gap: 8px; margin-top: var(--gap-sm); }
.ts-reviews__btn {
  width: 40px; height: 40px; border-radius: 50%; border: none; cursor: pointer;
  background: var(--pure-white); box-shadow: var(--box-shadow-shallow);
  color: var(--bde-brand-primary-color); font-size: 1.25rem; line-height: 1;
  display: flex; align-items: center; justify-content: center;
  transition: background-color .2s ease, color .2s ease;
}
.ts-reviews__btn:hover { background: var(--bde-brand-primary-color); color: var(--pure-white); }
@media (hover: none) and (pointer: coarse) {
  .ts-reviews__nav { display: none; }
}
@media (max-width: 767px) {
  .ts-reviews--slider { --ts-card-basis: 82vw; }
}