Very Advanced Reviews Slider

Shopify Liquid July 23, 2026

Very Advanced Reviews Slider + Grid with all controls:

Create a Section with this code:

{% comment %}
  bpers - Testimonials Slider (layout matched to screenshot)
  NEW:
  ✅ Added Layout option: Slider / Grid
  - Slider = current behavior (arrows, swipe, dots, autoplay, transform)
  - Grid   = same cards shown in a responsive grid (no slider JS, no arrows/dots)
  FIXES kept:
  1) Drag only starts after move threshold (clicks work)
  2) Clamp applied on inner wrapper (.bpers-content-inner)
  3) Read more / Read less toggles via delegated click
{% endcomment %}
{%- liquid
  assign sid = section.id
  assign slides_desktop = section.settings.slides_desktop | default: 4
  assign slides_tablet  = section.settings.slides_tablet  | default: 2
  assign slides_mobile  = section.settings.slides_mobile  | default: 1
  assign gap = section.settings.card_gap | default: 24
  assign layout_mode = section.settings.layout_mode | default: 'slider'
-%}
<style>
  /* =========================
     bpers Testimonials (Screenshot Layout)
     ========================= */
  #bpers-testimonials-{{ sid }}{
    --bpers-gap: {{ gap }}px;
    --bpers-slides-desktop: {{ slides_desktop }};
    --bpers-slides-tablet: {{ slides_tablet }};
    --bpers-slides-mobile: {{ slides_mobile }};
    --bpers-maxw: {{ section.settings.max_width }}px;
    --bpers-bg: {{ section.settings.section_bg }};
    --bpers-card: {{ section.settings.card_bg }};
    --bpers-text: {{ section.settings.text_color }};
    --bpers-muted: {{ section.settings.muted_color }};
    --bpers-star: {{ section.settings.star_color }};
    --bpers-verified: {{ section.settings.verified_color }};
    --bpers-initial-bg: {{ section.settings.initial_bg }};
    --bpers-initial-text: {{ section.settings.initial_text }};
    --bpers-arrow-bg: {{ section.settings.arrow_bg }};
    --bpers-arrow-color: {{ section.settings.arrow_color }};
    --bpers-border: {{ section.settings.card_border_width }}px solid {{ section.settings.card_border_color }};
    --bpers-radius: {{ section.settings.card_radius }}px;
    --bpers-shadow: {{ section.settings.card_shadow }};
    --bpers-avatar: {{ section.settings.avatar_size }}px;
    --bpers-clamp: {{ section.settings.clamp_lines }};
    background: var(--bpers-bg);
    color: var(--bpers-text);
    padding: {{ section.settings.padding_top }}px 0 {{ section.settings.padding_bottom }}px;
  }
  #bpers-testimonials-{{ sid }} .bpers-wrap{
    max-width: var(--bpers-maxw);
    margin: 0 auto;
    padding: 0 {{ section.settings.side_padding }}px;
  }
  /* Header */
  #bpers-testimonials-{{ sid }} .bpers-head{
    text-align: center;
    margin-bottom: {{ section.settings.header_spacing }}px;
  }
  #bpers-testimonials-{{ sid }} .bpers-title{
    margin: 0;
    font-size: clamp(28px, 3vw, 56px);
    line-height: 1.05;
    letter-spacing: -0.02em;
  }
  #bpers-testimonials-{{ sid }} .bpers-subtitle{
    margin: 14px auto 0;
    max-width: {{ section.settings.subtitle_max_width }}px;
    color: var(--bpers-muted);
    font-size: 15px;
    line-height: 1.6;
  }
  /* Shell */
  #bpers-testimonials-{{ sid }} .bpers-slider{
    position: relative;
  }
  /* --- Slider mode styles --- */
  #bpers-testimonials-{{ sid }}[data-bpers-layout="slider"] .bpers-viewport{
    overflow: hidden;
    padding: 6px 0;
    touch-action: pan-y;
  }
  #bpers-testimonials-{{ sid }}[data-bpers-layout="slider"] .bpers-track{
    display: flex;
    gap: var(--bpers-gap);
    will-change: transform;
    transition: transform {{ section.settings.transition_ms }}ms ease;
  }
  /* Slides per view (only in slider mode) */
  #bpers-testimonials-{{ sid }}[data-bpers-layout="slider"] .bpers-slide{
    flex: 0 0 calc((100% - (var(--bpers-gap) * (var(--bpers-slides-desktop) - 1))) / var(--bpers-slides-desktop));
    min-width: 0;
  }
  @media (max-width: 989px){
    #bpers-testimonials-{{ sid }}[data-bpers-layout="slider"] .bpers-slide{
      flex-basis: calc((100% - (var(--bpers-gap) * (var(--bpers-slides-tablet) - 1))) / var(--bpers-slides-tablet));
    }
  }
  @media (max-width: 749px){
    #bpers-testimonials-{{ sid }}[data-bpers-layout="slider"] .bpers-slide{
      flex-basis: calc((100% - (var(--bpers-gap) * (var(--bpers-slides-mobile) - 1))) / var(--bpers-slides-mobile));
    }
  }
  /* --- Grid mode styles --- */
  #bpers-testimonials-{{ sid }}[data-bpers-layout="grid"] .bpers-viewport{
    overflow: visible;
    padding: 0;
  }
  #bpers-testimonials-{{ sid }}[data-bpers-layout="grid"] .bpers-track{
    display: grid;
    gap: var(--bpers-gap);
    transform: none !important;
    transition: none !important;
    grid-template-columns: repeat(var(--bpers-slides-desktop), minmax(0, 1fr));
  }
  @media (max-width: 989px){
    #bpers-testimonials-{{ sid }}[data-bpers-layout="grid"] .bpers-track{
      grid-template-columns: repeat(var(--bpers-slides-tablet), minmax(0, 1fr));
    }
  }
  @media (max-width: 749px){
    #bpers-testimonials-{{ sid }}[data-bpers-layout="grid"] .bpers-track{
      grid-template-columns: repeat(var(--bpers-slides-mobile), minmax(0, 1fr));
    }
  }
  #bpers-testimonials-{{ sid }}[data-bpers-layout="grid"] .bpers-slide{
    min-width: 0;
  }
  /* Card */
  #bpers-testimonials-{{ sid }} .bpers-card{
    height: 100%;
    background: var(--bpers-card);
    border: var(--bpers-border);
    border-radius: var(--bpers-radius);
    box-shadow: var(--bpers-shadow);
    padding: {{ section.settings.card_padding }}px {{ section.settings.card_padding_x }}px;
    text-align: center;
    display: flex;
    flex-direction: column;
    align-items: center;
  }
  /* Avatar */
  #bpers-testimonials-{{ sid }} .bpers-avatar-wrap{
    position: relative;
    width: var(--bpers-avatar);
    height: var(--bpers-avatar);
    margin-top: 6px;
    margin-bottom: 12px;
  }
  #bpers-testimonials-{{ sid }} .bpers-avatar{
    width: 100%;
    height: 100%;
    border-radius: 999px;
    overflow: hidden;
    display: grid;
    place-items: center;
    background: rgba(0,0,0,.04);
  }
  #bpers-testimonials-{{ sid }} .bpers-avatar img{
    width: 100%;
    height: 100%;
    object-fit: cover;
    display:block;
  }
  #bpers-testimonials-{{ sid }} .bpers-initial{
    width: 100%;
    height: 100%;
    border-radius: 999px;
    display: grid;
    place-items: center;
    font-weight: 700;
    font-size: calc(var(--bpers-avatar) * .42);
    background: var(--bpers-initial-bg);
    color: var(--bpers-initial-text);
  }
  #bpers-testimonials-{{ sid }} .bpers-badge{
    position: absolute;
    right: -2px;
    bottom: -2px;
    width: 18px;
    height: 18px;
    border-radius: 999px;
    background: #fff;
    display: grid;
    place-items: center;
    box-shadow: 0 2px 6px rgba(0,0,0,.12);
  }
  #bpers-testimonials-{{ sid }} .bpers-badge svg{
    width: 14px;
    height: 14px;
    display:block;
  }
  /* Name + date */
  #bpers-testimonials-{{ sid }} .bpers-name{
    margin: 0;
    font-weight: 700;
    font-size: 16px;
    line-height: 1.25;
  }
  #bpers-testimonials-{{ sid }} .bpers-date{
    margin: 6px 0 10px;
    color: var(--bpers-muted);
    font-size: 13px;
    line-height: 1.2;
  }
  /* Stars */
  #bpers-testimonials-{{ sid }} .bpers-stars-row{
    display: inline-flex;
    align-items: center;
    gap: 8px;
    margin-bottom: 12px;
  }
  #bpers-testimonials-{{ sid }} .bpers-stars{
    display: inline-flex;
    gap: 2px;
    align-items: center;
  }
  #bpers-testimonials-{{ sid }} .bpers-star{
    width: 16px;
    height: 16px;
    color: var(--bpers-star);
  }
  #bpers-testimonials-{{ sid }} .bpers-star svg{
    width: 16px;
    height: 16px;
    fill: currentColor;
    display:block;
  }
  #bpers-testimonials-{{ sid }} .bpers-verified{
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 18px;
    height: 18px;
    color: var(--bpers-verified);
  }
  #bpers-testimonials-{{ sid }} .bpers-verified svg{
    width: 18px;
    height: 18px;
    fill: currentColor;
    display:block;
  }
  /* Content + clamp */
  #bpers-testimonials-{{ sid }} .bpers-content{
    width: 100%;
    margin: 0;
    color: var(--bpers-text);
    font-size: 14px;
    line-height: 1.7;
  }
  #bpers-testimonials-{{ sid }} .bpers-content-inner{
    width: 100%;
  }
  #bpers-testimonials-{{ sid }} .bpers-content-inner > *:first-child{ margin-top: 0; }
  #bpers-testimonials-{{ sid }} .bpers-content-inner > *:last-child{ margin-bottom: 0; }
  #bpers-testimonials-{{ sid }} .bpers-content-inner p{ margin: 0 0 10px; }
  #bpers-testimonials-{{ sid }} .bpers-content-inner.is-clamped{
    display: -webkit-box;
    -webkit-box-orient: vertical;
    overflow: hidden;
    -webkit-line-clamp: var(--bpers-clamp);
  }
  #bpers-testimonials-{{ sid }} .bpers-readmore{
    margin-top: 14px;
    border: 0;
    background: transparent;
    padding: 0;
    cursor: pointer;
    color: var(--bpers-muted);
    text-decoration: none;
    font-size: 14px;
  }
  #bpers-testimonials-{{ sid }} .bpers-readmore:hover{
    text-decoration: underline;
    text-underline-offset: 3px;
  }
  #bpers-testimonials-{{ sid }} .bpers-readmore.is-hidden{ display:none; }
  /* Arrows overlay (only slider mode) */
  #bpers-testimonials-{{ sid }} .bpers-arrow{
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    width: 44px;
    height: 44px;
    border-radius: 999px;
    border: 1px solid rgba(0,0,0,.12);
    background: var(--bpers-arrow-bg);
    color: var(--bpers-arrow-color);
    display: grid;
    place-items: center;
    cursor: pointer;
    z-index: 5;
    transition: opacity .12s ease, transform .12s ease;
  }
  #bpers-testimonials-{{ sid }} .bpers-arrow:active{ transform: translateY(-50%) scale(.98); }
  #bpers-testimonials-{{ sid }} .bpers-arrow[disabled]{ opacity: .45; cursor: not-allowed; }
  #bpers-testimonials-{{ sid }} .bpers-prev{ left: -6px; }
  #bpers-testimonials-{{ sid }} .bpers-next{ right: -6px; }
  @media (max-width: 749px){
    #bpers-testimonials-{{ sid }} .bpers-prev{ left: -2px; }
    #bpers-testimonials-{{ sid }} .bpers-next{ right: -2px; }
  }
  /* Hide arrows/dots when grid mode */
  #bpers-testimonials-{{ sid }}[data-bpers-layout="grid"] .bpers-arrow{ display:none !important; }
  #bpers-testimonials-{{ sid }}[data-bpers-layout="grid"] .bpers-dots{ display:none !important; }
  /* Optional dots */
  #bpers-testimonials-{{ sid }} .bpers-dots{
    display: none;
    justify-content: center;
    gap: 8px;
    margin-top: 16px;
  }
  #bpers-testimonials-{{ sid }} .bpers-dot{
    width: 9px; height: 9px;
    border-radius: 999px;
    border: 1px solid rgba(0,0,0,.25);
    background: transparent;
    cursor: pointer;
    padding: 0;
  }
  #bpers-testimonials-{{ sid }} .bpers-dot.is-active{
    background: {{ section.settings.dot_active }};
    border-color: {{ section.settings.dot_active }};
  }
  {% if section.settings.show_dots %}
    #bpers-testimonials-{{ sid }} .bpers-dots{ display:flex; }
  {% endif %}
  .visually-hidden{
    position:absolute!important;
    height:1px;width:1px;
    overflow:hidden;
    clip:rect(1px,1px,1px,1px);
    white-space:nowrap;
  }
</style>
<section
  id="bpers-testimonials-{{ sid }}"
  class="bpers-testimonials"
  data-bpers-section
  data-bpers-layout="{{ layout_mode }}"
>
  <div class="bpers-wrap">
    {% if section.settings.heading != blank or section.settings.subheading != blank %}
      <header class="bpers-head">
        {% if section.settings.heading != blank %}
          <h2 class="bpers-title">{{ section.settings.heading }}</h2>
        {% endif %}
        {% if section.settings.subheading != blank %}
          <div class="bpers-subtitle">{{ section.settings.subheading }}</div>
        {% endif %}
      </header>
    {% endif %}
    <div class="bpers-slider"
      data-bpers-slider
      data-autoplay="{{ section.settings.autoplay }}"
      data-interval="{{ section.settings.autoplay_interval | times: 1000 }}"
      data-loop="{{ section.settings.loop }}"
      data-pause-on-hover="{{ section.settings.pause_on_hover }}"
      data-announce="{{ section.settings.aria_announce }}"
    >
      {% if section.settings.show_arrows %}
        <button class="bpers-arrow bpers-prev" type="button" data-bpers-prev aria-label="Previous testimonials">‹</button>
        <button class="bpers-arrow bpers-next" type="button" data-bpers-next aria-label="Next testimonials">›</button>
      {% endif %}
      <div class="bpers-viewport" data-bpers-viewport>
        <div class="bpers-track" data-bpers-track>
          {% for block in section.blocks %}
            {%- liquid
              assign t_image = block.settings.image
              assign t_name = block.settings.name | default: 'Client Name'
              assign t_date = block.settings.date
              assign t_rating = block.settings.rating | default: 5
              assign t_content = block.settings.content
              assign initial = t_name | strip | slice: 0, 1 | upcase
            -%}
            <article class="bpers-slide" data-bpers-slide {{ block.shopify_attributes }}>
              <div class="bpers-card">
                <div class="bpers-avatar-wrap" aria-hidden="true">
                  <div class="bpers-avatar">
                    {% if t_image != blank %}
                      <img
                        src="{{ t_image | image_url: width: 240 }}"
                        srcset="
                          {{ t_image | image_url: width: 120 }} 120w,
                          {{ t_image | image_url: width: 180 }} 180w,
                          {{ t_image | image_url: width: 240 }} 240w
                        "
                        sizes="(max-width: 749px) {{ section.settings.avatar_size }}px, {{ section.settings.avatar_size }}px"
                        alt="{{ t_name | escape }}"
                        loading="lazy"
                        width="{{ section.settings.avatar_size }}"
                        height="{{ section.settings.avatar_size }}"
                      >
                    {% else %}
                      <div class="bpers-initial">{{ initial }}</div>
                    {% endif %}
                  </div>
                  {% if section.settings.show_small_badge %}
                    <span class="bpers-badge" aria-hidden="true">
                      <svg viewBox="0 0 24 24" focusable="false" aria-hidden="true">
                        <path fill="#4285F4" d="M21.35 11.1H12v2.96h5.35c-.24 1.5-1.75 4.4-5.35 4.4-3.22 0-5.85-2.66-5.85-5.93S8.78 6.6 12 6.6c1.83 0 3.06.78 3.76 1.45l2.56-2.46C16.74 4.1 14.58 3 12 3 6.98 3 2.9 7.08 2.9 12.53S6.98 22.06 12 22.06c5.2 0 8.65-3.65 8.65-8.8 0-.59-.07-1.04-.15-1.46z"/>
                      </svg>
                    </span>
                  {% endif %}
                </div>
                <p class="bpers-name">{{ t_name }}</p>
                {% if t_date != blank %}
                  <p class="bpers-date">{{ t_date }}</p>
                {% endif %}
                <div class="bpers-stars-row">
                  {% if section.settings.show_stars %}
                    <div class="bpers-stars" aria-label="Rating: {{ t_rating }} out of 5">
                      {% for i in (1..5) %}
                        <span class="bpers-star" aria-hidden="true">
                          {% if i <= t_rating %}
                            <svg viewBox="0 0 24 24"><path d="M12 17.3l-6.18 3.73 1.64-7.03L2 9.24l7.19-.62L12 2l2.81 6.62 7.19.62-5.46 4.76 1.64 7.03z"/></svg>
                          {% else %}
                            <svg viewBox="0 0 24 24"><path d="M12 17.3l-6.18 3.73 1.64-7.03L2 9.24l7.19-.62L12 2l2.81 6.62 7.19.62-5.46 4.76 1.64 7.03z" style="fill: rgba(0,0,0,.15)"/></svg>
                          {% endif %}
                        </span>
                      {% endfor %}
                    </div>
                  {% endif %}
                  {% if section.settings.show_verified_icon %}
                    <span class="bpers-verified" aria-label="Verified">
                      <svg viewBox="0 0 24 24" focusable="false" aria-hidden="true">
                        <path d="M12 2a10 10 0 1 0 10 10A10.01 10.01 0 0 0 12 2Zm-1.2 14.2-3.6-3.6 1.4-1.4 2.2 2.2 4.8-4.8 1.4 1.4Z"/>
                      </svg>
                    </span>
                  {% endif %}
                </div>
                {% if t_content != blank %}
                  <div class="bpers-content" data-bpers-content>
                    <div class="bpers-content-inner is-clamped" data-bpers-content-inner>
                      {{ t_content }}
                    </div>
                  </div>
                  <button
                    class="bpers-readmore"
                    type="button"
                    data-bpers-readmore
                    aria-expanded="false"
                  >
                    {{ section.settings.read_more_label }}
                  </button>
                {% endif %}
              </div>
            </article>
          {% endfor %}
        </div>
      </div>
      <div class="bpers-dots" data-bpers-dots aria-label="Testimonials pagination"></div>
      {% if section.settings.aria_announce %}
        <div class="visually-hidden" aria-live="polite" data-bpers-live></div>
      {% endif %}
    </div>
  </div>
</section>
<script>
(() => {
  const root = document.querySelector('#bpers-testimonials-{{ sid }}');
  if (!root) return;
  const layout = root.dataset.bpersLayout || 'slider';
  const slider = root.querySelector('[data-bpers-slider]');
  const viewport = root.querySelector('[data-bpers-viewport]');
  const track = root.querySelector('[data-bpers-track]');
  const slides = Array.from(root.querySelectorAll('[data-bpers-slide]'));
  const prevBtn = root.querySelector('[data-bpers-prev]');
  const nextBtn = root.querySelector('[data-bpers-next]');
  const dotsWrap = root.querySelector('[data-bpers-dots]');
  const live = root.querySelector('[data-bpers-live]');
  if (!track || slides.length === 0) return;
  // =========================
  // READ MORE (works in grid + slider)
  // =========================
  const readMoreLabel = "{{ section.settings.read_more_label | escape }}";
  const readLessLabel = "{{ section.settings.read_less_label | escape }}";
  const measureReadMore = (inner, btn) => {
    if (!inner || !btn) return;
    const expanded = btn.getAttribute('aria-expanded') === 'true';
    if (expanded) {
      btn.classList.remove('is-hidden');
      btn.textContent = readLessLabel;
      return;
    }
    inner.classList.add('is-clamped');
    btn.textContent = readMoreLabel;
    btn.classList.remove('is-hidden');
    requestAnimationFrame(() => {
      requestAnimationFrame(() => {
        const overflowing = inner.scrollHeight > inner.clientHeight + 2;
        if (!overflowing) btn.classList.add('is-hidden');
      });
    });
  };
  const setupReadMore = () => {
    root.querySelectorAll('[data-bpers-content]').forEach((wrap) => {
      const inner = wrap.querySelector('[data-bpers-content-inner]');
      const card = wrap.closest('.bpers-card');
      const btn = card ? card.querySelector('[data-bpers-readmore]') : null;
      if (!inner || !btn) return;
      btn.setAttribute('aria-expanded', 'false');
      inner.classList.add('is-clamped');
      btn.textContent = readMoreLabel;
      measureReadMore(inner, btn);
    });
  };
  root.addEventListener('click', (e) => {
    const btn = e.target.closest('[data-bpers-readmore]');
    if (!btn || !root.contains(btn)) return;
    e.preventDefault();
    e.stopPropagation();
    const card = btn.closest('.bpers-card');
    const inner = card ? card.querySelector('[data-bpers-content-inner]') : null;
    if (!inner) return;
    const expanded = btn.getAttribute('aria-expanded') === 'true';
    if (expanded) {
      btn.setAttribute('aria-expanded', 'false');
      inner.classList.add('is-clamped');
      btn.textContent = readMoreLabel;
      measureReadMore(inner, btn);
    } else {
      btn.setAttribute('aria-expanded', 'true');
      inner.classList.remove('is-clamped');
      btn.classList.remove('is-hidden');
      btn.textContent = readLessLabel;
    }
  }, { passive: false });
  if ('ResizeObserver' in window) {
    if (root._bpersRO) root._bpersRO.disconnect();
    root._bpersRO = new ResizeObserver(() => {
      root.querySelectorAll('.bpers-card').forEach((card) => {
        const inner = card.querySelector('[data-bpers-content-inner]');
        const btn = card.querySelector('[data-bpers-readmore]');
        if (inner && btn) measureReadMore(inner, btn);
      });
    });
    root.querySelectorAll('[data-bpers-content-inner]').forEach((el) => root._bpersRO.observe(el));
  }
  // If GRID mode: no slider behavior needed
  if (layout === 'grid') {
    setupReadMore();
    // also re-check after fonts load
    if (document.fonts && document.fonts.ready) {
      document.fonts.ready.then(() => setupReadMore()).catch(() => {});
    }
    window.addEventListener('resize', () => {
      clearTimeout(root._bpersResizeT);
      root._bpersResizeT = setTimeout(() => setupReadMore(), 120);
    });
    return;
  }
  // =========================
  // SLIDER behavior (only when layout === 'slider')
  // =========================
  const opts = {
    autoplay: slider?.dataset.autoplay === 'true',
    interval: parseInt(slider?.dataset.interval || '5000', 10),
    loop: slider?.dataset.loop === 'true',
    pauseOnHover: slider?.dataset.pauseOnHover === 'true',
    announce: slider?.dataset.announce === 'true'
  };
  const getSlidesPerView = () => {
    const w = window.innerWidth;
    const styles = getComputedStyle(root);
    if (w <= 749) return parseFloat(styles.getPropertyValue('--bpers-slides-mobile')) || 1;
    if (w <= 989) return parseFloat(styles.getPropertyValue('--bpers-slides-tablet')) || 2;
    return parseFloat(styles.getPropertyValue('--bpers-slides-desktop')) || 4;
  };
  const getGap = () => parseFloat(getComputedStyle(root).getPropertyValue('--bpers-gap')) || 24;
  let index = 0;
  let autoplayTimer = null;
  const maxIndex = () => {
    const spv = getSlidesPerView();
    return Math.max(0, Math.ceil(slides.length - spv));
  };
  const clampIndex = (i) => {
    const max = maxIndex();
    if (opts.loop) {
      if (i < 0) return max;
      if (i > max) return 0;
      return i;
    }
    return Math.max(0, Math.min(i, max));
  };
  const slideWidth = () => slides[0].getBoundingClientRect().width;
  const setTransform = (x) => { track.style.transform = `translate3d(${x}px,0,0)`; };
  const updateButtons = () => {
    if (!prevBtn || !nextBtn) return;
    if (opts.loop) { prevBtn.disabled = false; nextBtn.disabled = false; return; }
    const max = maxIndex();
    prevBtn.disabled = index <= 0;
    nextBtn.disabled = index >= max;
  };
  const buildDots = () => {
    if (!dotsWrap) return;
    dotsWrap.innerHTML = '';
    const count = maxIndex() + 1;
    for (let i = 0; i < count; i++) {
      const b = document.createElement('button');
      b.type = 'button';
      b.className = 'bpers-dot' + (i === 0 ? ' is-active' : '');
      b.setAttribute('aria-label', `Go to testimonials ${i + 1}`);
      b.onclick = () => goTo(i);
      dotsWrap.appendChild(b);
    }
  };
  const setActiveDot = () => {
    if (!dotsWrap) return;
    const dots = Array.from(dotsWrap.querySelectorAll('button'));
    dots.forEach(d => d.classList.remove('is-active'));
    if (dots[index]) dots[index].classList.add('is-active');
  };
  const goTo = (i, { silent = false } = {}) => {
    index = clampIndex(i);
    const x = -1 * index * (slideWidth() + getGap());
    setTransform(x);
    updateButtons();
    setActiveDot();
    if (opts.announce && live && !silent) {
      live.textContent = `Showing testimonials ${index + 1} of ${slides.length}`;
    }
  };
  const next = () => goTo(index + 1);
  const prev = () => goTo(index - 1);
  const stopAutoplay = () => { if (autoplayTimer) clearInterval(autoplayTimer); autoplayTimer = null; };
  const startAutoplay = () => {
    stopAutoplay();
    if (!opts.autoplay) return;
    autoplayTimer = setInterval(next, Math.max(2000, opts.interval || 5000));
  };
  // Drag / Swipe (threshold-based)
  let isPointerDown = false;
  let isDragging = false;
  let startX = 0;
  let startTransform = 0;
  const isInteractiveTarget = (t) => {
    return !!(t && t.closest && t.closest('button, a, input, textarea, select, [data-bpers-readmore], [data-bpers-prev], [data-bpers-next], [data-bpers-dots]'));
  };
  const getCurrentTransformX = () => {
    const tr = getComputedStyle(track).transform;
    if (!tr || tr === 'none') return 0;
    const m = new DOMMatrixReadOnly(tr);
    return m.m41 || 0;
  };
  const pointerDown = (e) => {
    if (!viewport) return;
    if (isInteractiveTarget(e.target)) return;
    isPointerDown = true;
    isDragging = false;
    startX = e.clientX;
    startTransform = getCurrentTransformX();
  };
  const pointerMove = (e) => {
    if (!isPointerDown) return;
    const dx = e.clientX - startX;
    if (!isDragging) {
      if (Math.abs(dx) < 8) return;
      isDragging = true;
      track.style.transition = 'none';
      viewport.setPointerCapture?.(e.pointerId);
    }
    setTransform(startTransform + dx);
  };
  const pointerUp = (e) => {
    if (!isPointerDown) return;
    isPointerDown = false;
    if (!isDragging) return;
    isDragging = false;
    track.style.transition = '';
    const dx = e.clientX - startX;
    const threshold = Math.min(80, slideWidth() * 0.2);
    if (dx > threshold) prev();
    else if (dx < -threshold) next();
    else goTo(index, { silent: true });
  };
  prevBtn?.addEventListener('click', (e) => { e.stopPropagation(); prev(); });
  nextBtn?.addEventListener('click', (e) => { e.stopPropagation(); next(); });
  viewport?.addEventListener('pointerdown', pointerDown, { passive: true });
  viewport?.addEventListener('pointermove', pointerMove, { passive: true });
  viewport?.addEventListener('pointerup', pointerUp, { passive: true });
  viewport?.addEventListener('pointercancel', pointerUp, { passive: true });
  if (opts.pauseOnHover) {
    slider?.addEventListener('mouseenter', stopAutoplay);
    slider?.addEventListener('mouseleave', startAutoplay);
    slider?.addEventListener('focusin', stopAutoplay);
    slider?.addEventListener('focusout', startAutoplay);
  }
  const init = () => {
    buildDots();
    setupReadMore();
    goTo(0, { silent: true });
    startAutoplay();
    updateButtons();
  };
  let resizeT = null;
  window.addEventListener('resize', () => {
    clearTimeout(resizeT);
    resizeT = setTimeout(() => {
      buildDots();
      goTo(index, { silent: true });
      setupReadMore();
    }, 120);
  });
  if (document.fonts && document.fonts.ready) {
    document.fonts.ready.then(() => {
      setupReadMore();
      goTo(index, { silent: true });
    }).catch(() => {});
  }
  init();
})();
</script>
{% schema %}
{
  "name": "bpers Testimonials",
  "tag": "section",
  "class": "bpers-testimonials-section",
  "settings": [
    { "type": "text", "id": "heading", "label": "Heading", "default": "Trusted by Luxury Travelers" },
    {
      "type": "text",
      "id": "subheading",
      "label": "Subheading (plain text)",
      "default": "Discover why discerning guests trust your brand for their luxury stays in Los Angeles and beyond"
    },
    {
      "type": "select",
      "id": "layout_mode",
      "label": "Layout mode",
      "default": "slider",
      "options": [
        { "value": "slider", "label": "Slider" },
        { "value": "grid", "label": "Grid" }
      ]
    },
    { "type": "range", "id": "max_width", "label": "Max width", "min": 900, "max": 1600, "step": 50, "default": 1200 },
    { "type": "range", "id": "side_padding", "label": "Side padding", "min": 10, "max": 50, "step": 2, "default": 20 },
    { "type": "range", "id": "header_spacing", "label": "Space under header", "min": 10, "max": 70, "step": 2, "default": 34 },
    { "type": "range", "id": "subtitle_max_width", "label": "Subtitle max width", "min": 400, "max": 1000, "step": 20, "default": 760 },
    { "type": "header", "content": "Columns (also used for slider slides per view)" },
    { "type": "range", "id": "slides_desktop", "label": "Columns/slides (desktop)", "min": 1, "max": 6, "step": 1, "default": 4 },
    { "type": "range", "id": "slides_tablet", "label": "Columns/slides (tablet)", "min": 1, "max": 4, "step": 1, "default": 2 },
    { "type": "range", "id": "slides_mobile", "label": "Columns/slides (mobile)", "min": 1, "max": 3, "step": 1, "default": 1 },
    { "type": "range", "id": "card_gap", "label": "Gap between cards", "min": 10, "max": 40, "step": 1, "default": 28 },
    { "type": "range", "id": "transition_ms", "label": "Transition (ms)", "min": 120, "max": 900, "step": 20, "default": 360 },
    { "type": "checkbox", "id": "show_arrows", "label": "Show arrows (slider only)", "default": true },
    { "type": "checkbox", "id": "show_dots", "label": "Show dots (slider only)", "default": false },
    { "type": "checkbox", "id": "autoplay", "label": "Autoplay (slider only)", "default": false },
    { "type": "range", "id": "autoplay_interval", "label": "Autoplay interval (seconds)", "min": 2, "max": 12, "step": 1, "default": 5 },
    { "type": "checkbox", "id": "pause_on_hover", "label": "Pause autoplay on hover/focus (slider only)", "default": true },
    { "type": "checkbox", "id": "loop", "label": "Loop slides (slider only)", "default": true },
    { "type": "checkbox", "id": "aria_announce", "label": "Announce slide changes (a11y)", "default": true },
    { "type": "header", "content": "Card" },
    { "type": "range", "id": "card_padding", "label": "Card padding (Y)", "min": 14, "max": 44, "step": 1, "default": 22 },
    { "type": "range", "id": "card_padding_x", "label": "Card padding (X)", "min": 14, "max": 44, "step": 1, "default": 22 },
    { "type": "range", "id": "card_radius", "label": "Card radius", "min": 6, "max": 28, "step": 1, "default": 8 },
    { "type": "range", "id": "card_border_width", "label": "Card border width", "min": 0, "max": 3, "step": 1, "default": 0 },
    { "type": "color", "id": "card_border_color", "label": "Card border color", "default": "#E5E5E5" },
    { "type": "text", "id": "card_shadow", "label": "Card shadow (CSS)", "default": "0 10px 24px rgba(0,0,0,.06)" },
    { "type": "range", "id": "avatar_size", "label": "Avatar size", "min": 48, "max": 100, "step": 2, "default": 64 },
    { "type": "header", "content": "Stars / Verified" },
    { "type": "checkbox", "id": "show_stars", "label": "Show stars", "default": true },
    { "type": "checkbox", "id": "show_verified_icon", "label": "Show blue verified icon next to stars", "default": true },
    { "type": "checkbox", "id": "show_small_badge", "label": "Show small badge on avatar (like screenshot)", "default": true },
    { "type": "header", "content": "Read more" },
    { "type": "range", "id": "clamp_lines", "label": "Clamped lines", "min": 2, "max": 10, "step": 1, "default": 4 },
    { "type": "text", "id": "read_more_label", "label": "Read more label", "default": "Read more" },
    { "type": "text", "id": "read_less_label", "label": "Read less label", "default": "Read less" },
    { "type": "header", "content": "Colors" },
    { "type": "color", "id": "section_bg", "label": "Section background", "default": "#FFFFFF" },
    { "type": "color", "id": "card_bg", "label": "Card background", "default": "#F3F3F3" },
    { "type": "color", "id": "text_color", "label": "Text color", "default": "#111111" },
    { "type": "color", "id": "muted_color", "label": "Muted text", "default": "#7A7A7A" },
    { "type": "color", "id": "star_color", "label": "Star color", "default": "#F5B301" },
    { "type": "color", "id": "verified_color", "label": "Verified icon color", "default": "#1A73E8" },
    { "type": "color", "id": "initial_bg", "label": "Initial circle background", "default": "#2F6B2F" },
    { "type": "color", "id": "initial_text", "label": "Initial circle text", "default": "#FFFFFF" },
    { "type": "color", "id": "arrow_bg", "label": "Arrow background", "default": "#FFFFFF" },
    { "type": "color", "id": "arrow_color", "label": "Arrow color", "default": "#111111" },
    { "type": "color", "id": "dot_active", "label": "Active dot color", "default": "#111111" },
    { "type": "header", "content": "Section spacing" },
    { "type": "range", "id": "padding_top", "label": "Padding top", "min": 0, "max": 120, "step": 4, "default": 40 },
    { "type": "range", "id": "padding_bottom", "label": "Padding bottom", "min": 0, "max": 120, "step": 4, "default": 40 }
  ],
  "blocks": [
    {
      "type": "testimonial",
      "name": "Testimonial",
      "settings": [
        { "type": "image_picker", "id": "image", "label": "Client image (optional)" },
        { "type": "text", "id": "name", "label": "Client name", "default": "Client Name" },
        { "type": "text", "id": "date", "label": "Date label", "default": "4 months ago" },
        { "type": "range", "id": "rating", "label": "Rating", "min": 1, "max": 5, "step": 1, "default": 5 },
        {
          "type": "richtext",
          "id": "content",
          "label": "Testimonial content",
          "default": "<p>I was in LA for 6 weeks and didn’t want a hotel. The service was smooth, the property was spotless, and everything felt five-star.</p>"
        }
      ]
    }
  ],
  "presets": [
    {
      "name": "bpers Testimonials (Slider/Grid)",
      "blocks": [
        { "type": "testimonial" },
        { "type": "testimonial" },
        { "type": "testimonial" },
        { "type": "testimonial" }
      ]
    }
  ]
}
{% endschema %}