{% comment %}
  Dynasort search results (Chunk S2) — the storefront takeover of the theme's
  search RESULTS page grid. Pilot: hand-add this as sections/dynasort-search-results.liquid
  and point templates/search.json at it instead of the theme's main-search section.

  Model: relevance decides MEMBERSHIP, the recipe decides ORDER. The section reads
  the query server-side ({{ search.terms }}), calls the App Proxy
  (/apps/dynasort/search → proxy-search.php), and renders the recipe-ranked §9
  cards — Shopify's Storefront search supplies the matched set, we reorder + render.

  We OWN the grid + simple client-side pagination over the returned set (≤250).
  DEFERRED for pilot: facets/filters, sort dropdown, collection-scoped search.
  Cards reuse the recommendation card contract (rec_card_from_product) so the
  markup/CSS mirror the recs widget; settings are sensible pilot defaults (no
  per-recipe search settings system yet — that's the SearchPage.js admin, S3).
{% endcomment %}

<div
  class="dynasort-search dynasort-search--grid"
  id="ds-search-{{ block.id }}"
  data-dynasort-search
  data-q="{{ search.terms | escape }}"
  data-per-page="{{ block.settings.per_page | default: 24 }}"
  style="--ds-cols-mobile: {{ block.settings.columns_mobile | default: 2 }}; --ds-cols-tablet: {{ block.settings.columns_tablet | default: 3 }}; --ds-cols-desktop: {{ block.settings.columns_desktop | default: 4 }};"
  hidden
>
  <div class="dynasort-search__toolbar">
    {% if block.settings.show_summary %}
      <p class="dynasort-search__summary" data-summary hidden></p>
    {% endif %}
    <select class="dynasort-search__sort" data-sort hidden aria-label="{{ 'sort.label' | t }}">
      <option value="">{{ 'sort.relevance' | t }}</option>
      <option value="price-ascending">{{ 'sort.price_ascending' | t }}</option>
      <option value="price-descending">{{ 'sort.price_descending' | t }}</option>
    </select>
  </div>
  <div class="dynasort-search__grid" data-grid aria-live="polite"></div>
  <div class="dynasort-search__pager" data-pager hidden>
    <button type="button" class="dynasort-search__page-btn" data-page-prev>&#8249; {{ 'pagination.previous' | t }}</button>
    <span class="dynasort-search__page-info" data-page-info></span>
    <button type="button" class="dynasort-search__page-btn" data-page-next>{{ 'pagination.next' | t }} &#8250;</button>
  </div>
  <p class="dynasort-search__empty" data-empty hidden></p>
</div>

<style>
  .dynasort-search {
    --ds-gap: 1em;
    --ds-radius: var(--media-radius, var(--border-radius, 8px));
    --ds-max-width: var(--page-width, 120rem);
    --ds-muted: color-mix(in srgb, currentColor 58%, transparent);
    --ds-border: color-mix(in srgb, currentColor 16%, transparent);
    --ds-surface: var(--color-background, #fff);
    --ds-overlay-bg: rgba(255,255,255,0.85);
    --ds-overlay-text: #1a1a1a;
    --ds-cols-mobile: 2; --ds-cols-tablet: 3; --ds-cols-desktop: 4;
    --ds-aspect: 1 / 1; --ds-swatch-size: 1.5em; --ds-control-radius: 0.4em;
    color: inherit; font-size: inherit;
    margin: 2rem auto; max-width: var(--ds-max-width); box-sizing: border-box;
  }
  .dynasort-search *, .dynasort-search *::before, .dynasort-search *::after { box-sizing: border-box; }
  .dynasort-search[hidden] { display: none; }
  .dynasort-search :where(button) {
    appearance: none; -webkit-appearance: none;
    margin: 0; padding: 0; border: 0; background: none; box-shadow: none;
    font: inherit; color: inherit; line-height: 1; min-width: 0; min-height: 0; width: auto; text-align: inherit; cursor: pointer;
  }
  .dynasort-search__toolbar { display: flex; align-items: center; justify-content: space-between; gap: 1em; margin: 0 0 1em; flex-wrap: wrap; }
  .dynasort-search__summary { font-size: 0.95em; color: var(--ds-muted); margin: 0; }
  .dynasort-search__sort { font: inherit; font-size: 0.9em; color: inherit; background: var(--ds-surface); border: 1px solid var(--ds-border); border-radius: var(--ds-control-radius); padding: 0.45em 0.7em; }
  .dynasort-search__sort[hidden] { display: none; }
  .dynasort-search__grid { display: grid; grid-template-columns: repeat(var(--ds-cols-mobile), minmax(0, 1fr)); gap: var(--ds-gap); }
  @media (min-width: 750px) { .dynasort-search__grid { grid-template-columns: repeat(var(--ds-cols-tablet), minmax(0, 1fr)); } }
  @media (min-width: 990px) { .dynasort-search__grid { grid-template-columns: repeat(var(--ds-cols-desktop), minmax(0, 1fr)); } }

  .dynasort-search__card { position: relative; display: flex; flex-direction: column; color: inherit; min-width: 0; }
  .dynasort-search__media { position: relative; aspect-ratio: var(--ds-aspect); border-radius: var(--ds-radius); overflow: hidden; background: var(--ds-border); }
  .dynasort-search__media-link { display: block; width: 100%; height: 100%; }
  .dynasort-search__media img { width: 100%; height: 100%; object-fit: cover; display: block; }
  .dynasort-search__skeleton { aspect-ratio: var(--ds-aspect); border-radius: var(--ds-radius); background: var(--ds-border); opacity: 0.5; }
  .dynasort-search__badges { position: absolute; top: 0.5em; left: 0.5em; display: flex; gap: 0.3em; flex-wrap: wrap; z-index: 1; }
  .dynasort-search__badge { font-size: 0.72em; padding: 0.15em 0.5em; border-radius: 0.25em; line-height: 1.5; font-weight: 500; }
  .dynasort-search__title { display: block; font-size: 1em; margin: 0.4em 0 0.2em; line-height: 1.3; text-decoration: none; color: inherit; }
  .dynasort-search__colorname { font-size: 0.85em; color: var(--ds-muted); }
  .dynasort-search__price { font-size: 0.98em; margin-top: 0.25em; }
  .dynasort-search__price del { color: var(--ds-muted); margin-right: 0.4em; }
  .dynasort-search__price ins { text-decoration: none; }
  .dynasort-search__rating { font-size: 0.85em; color: var(--ds-muted); margin-top: 0.15em; letter-spacing: 0.05em; }
  .dynasort-search__swatches { display: flex; gap: 0.4em; flex-wrap: wrap; margin-top: 0.6em; align-items: center; }
  .dynasort-search__swatch { width: var(--ds-swatch-size); height: var(--ds-swatch-size); border-radius: 50%; border: 1px solid var(--ds-border); background-size: cover; background-position: center; position: relative; flex: 0 0 auto; }
  .dynasort-search__swatch[aria-pressed="true"] { border: 2px solid currentColor; box-shadow: inset 0 0 0 2px var(--ds-surface); }
  .dynasort-search__swatch--soldout::after { content: ""; position: absolute; inset: 0; background: linear-gradient(to top left, transparent 45%, var(--ds-muted) 46%, var(--ds-muted) 54%, transparent 55%); border-radius: 50%; }
  .dynasort-search__swatch-more { font-size: 0.85em; color: var(--ds-muted); text-decoration: none; margin-left: 0.1em; }

  .dynasort-search__sizes { position: absolute; left: 0.6em; right: 0.6em; bottom: 0.6em; display: flex; flex-wrap: wrap; gap: 0.35em; padding: 0.5em; border-radius: var(--ds-control-radius); background: var(--ds-overlay-bg); backdrop-filter: blur(2px); transform: translateY(calc(100% + 0.6em)); opacity: 0; pointer-events: none; transition: transform 0.15s ease, opacity 0.15s ease; }
  @media (hover: hover) and (pointer: fine) { .dynasort-search__card--inline:hover .dynasort-search__sizes { transform: translateY(0); opacity: 1; pointer-events: auto; } }
  .dynasort-search__size { font-size: 0.82em; padding: 0.3em 0.6em; border: 1px solid var(--ds-overlay-text); border-radius: var(--ds-control-radius); background: transparent; color: var(--ds-overlay-text); }
  .dynasort-search__size:disabled { opacity: 0.45; text-decoration: line-through; cursor: default; }
  .dynasort-search__added { position: absolute; left: 0.6em; right: 0.6em; bottom: 0.6em; padding: 0.5em; text-align: center; font-size: 0.85em; border-radius: var(--ds-control-radius); background: var(--ds-overlay-bg); color: var(--ds-overlay-text); backdrop-filter: blur(2px); transform: translateY(calc(100% + 0.6em)); opacity: 0; pointer-events: none; transition: transform 0.18s ease, opacity 0.18s ease; z-index: 3; }
  .dynasort-search__added.is-shown { transform: translateY(0); opacity: 1; }

  .dynasort-search__pager { display: flex; align-items: center; justify-content: center; gap: 1.2em; margin-top: 1.8em; }
  .dynasort-search__page-btn { font-size: 0.95em; padding: 0.5em 1em; border: 1px solid var(--ds-border); border-radius: var(--ds-control-radius); }
  .dynasort-search__page-btn:disabled { opacity: 0.35; cursor: default; }
  .dynasort-search__page-info { font-size: 0.9em; color: var(--ds-muted); }
  .dynasort-search__empty { font-size: 1em; color: var(--ds-muted); }

  @media (prefers-reduced-motion: reduce) { .dynasort-search__sizes, .dynasort-search__added { transition: none; } }
</style>

<script>
(function () {
  // Per-instance root via block.id + init-once guard (same multi-instance
  // hazard as the recommendations block; see its comment).
  var root = document.getElementById('ds-search-{{ block.id }}');
  if (!root || root.__dsInit) return;
  root.__dsInit = true;
  var query = (root.getAttribute('data-q') || '').trim();
  var perPage = parseInt(root.getAttribute('data-per-page'), 10) || 24;
  var gridEl = root.querySelector('[data-grid]');
  var pagerEl = root.querySelector('[data-pager]');
  var pageInfoEl = root.querySelector('[data-page-info]');
  var prevBtn = root.querySelector('[data-page-prev]');
  var nextBtn = root.querySelector('[data-page-next]');
  var emptyEl = root.querySelector('[data-empty]');
  var summaryEl = root.querySelector('[data-summary]');
  var canHover = window.matchMedia && window.matchMedia('(hover: hover) and (pointer: fine)').matches;

  var products = [], currency = 'USD', page = 0, addedLabel = 'Added to cart', serveId = 0;

  // CX tracking (plans/cx-tracking-jul-2026.md): serves carry the pixel's own
  // session cookie so attribution joins line up; card clicks and inline adds
  // beacon back with the serve id + position. sendBeacon survives navigation.
  // Beacons go to the recommend proxy path: the /search subpath dispatches to
  // the search endpoint before the cx branch is reached.
  function dsSession() {
    var m = document.cookie.match(/(?:^|; )mmm_cookies=([^;]+)/);
    return m ? decodeURIComponent(m[1]) : '';
  }
  function cxBeacon(evt, cardEl) {
    if (!serveId || !cardEl) return;
    var url = '/apps/dynasort/recommend?service=cx&serve_id=' + encodeURIComponent(serveId) + '&e=' + evt +
      '&pid=' + encodeURIComponent(cardEl.getAttribute('data-pid') || '') +
      '&pos=' + encodeURIComponent(cardEl.getAttribute('data-pos') || '');
    if (navigator.sendBeacon) { navigator.sendBeacon(url); }
    else { fetch(url, { keepalive: true }).catch(function () {}); }
  }
  // Presentation settings (from the app admin via the proxy); defaults = all on.
  var settings = { layout: { columns: { mobile: 2, tablet: 3, desktop: 4 }, per_page: perPage }, fields: { swatches: true, badges: true, rating: true, inline_add: true } };
  function fld(k) { return settings.fields && settings.fields[k] !== false; }

  function esc(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) { return ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]); }); }
  function money(v, c) { if (v == null) return ''; try { return new Intl.NumberFormat(undefined, { style: 'currency', currency: c || 'USD' }).format(v); } catch (e) { return '$' + Number(v).toFixed(2); } }
  function priceRange(min, max, c) { if (min == null) return ''; return (max != null && max > min) ? money(min, c) + '–' + money(max, c) : money(min, c); }

  function badgesHtml(card) {
    var out = [];
    if (card.on_sale) out.push({ label: 'Sale', bg: '#d32f2f', text: '#fff' });
    if (card.badges && card.badges.is_new) out.push({ label: 'New', bg: '#1a7f37', text: '#fff' });
    if (card.badges && card.badges.bestseller) out.push({ label: 'Bestseller', bg: '#9a6700', text: '#fff' });
    if (card.badges && card.badges.low_stock) out.push({ label: 'Low stock', bg: '#b45309', text: '#fff' });
    if (!out.length) return '';
    return '<div class="dynasort-search__badges">' + out.map(function (x) {
      return '<span class="dynasort-search__badge" style="background:' + x.bg + ';color:' + x.text + '">' + esc(x.label) + '</span>';
    }).join('') + '</div>';
  }
  function ratingHtml(card) {
    if (!card.rating || !card.rating.count) return '';
    var v = Math.round(card.rating.value);
    return '<div class="dynasort-search__rating">' + ('★★★★★'.slice(0, v) + '☆☆☆☆☆'.slice(0, 5 - v)) + ' (' + card.rating.count + ')</div>';
  }
  function swatchesHtml(card) {
    if (!card.swatches || card.swatches.length < 2) return '';
    var cap = 5, shown = card.swatches.slice(0, cap), overflow = card.swatches.length - shown.length;
    var dots = shown.map(function (s, i) {
      var bg = s.swatch ? ('background-color:' + s.swatch) : (s.image ? ('background-image:url(' + s.image + ')') : '');
      var cls = 'dynasort-search__swatch' + (s.available ? '' : ' dynasort-search__swatch--soldout');
      return '<button type="button" class="' + cls + '" style="' + bg + '" title="' + esc(s.name) + '" data-swatch="' + i + '" aria-label="' + esc(s.name) + '" aria-pressed="' + (i === 0 ? 'true' : 'false') + '"></button>';
    }).join('');
    var more = overflow > 0 ? '<a class="dynasort-search__swatch-more" href="' + esc(card.url) + '">+' + overflow + '</a>' : '';
    return '<div class="dynasort-search__swatches" data-swatches>' + dots + more + '</div>';
  }

  function cardHtml(card, cardIdx) {
    var img = card.featured_image ? '<img src="' + esc(card.featured_image) + '" alt="' + esc(card.title) + '" loading="lazy" data-main-img>' : '';
    var showSwatches = fld('swatches');
    var colorName = (showSwatches && card.swatches && card.swatches.length) ? '<div class="dynasort-search__colorname" data-colorname>' + esc(card.swatches[0].name) + '</div>' : '';
    var priceCmp = (card.compare_at_min != null) ? '<del>' + money(card.compare_at_min, currency) + '</del>' : '';
    var price = '<div class="dynasort-search__price" data-price>' + priceCmp + '<ins>' + priceRange(card.price_min, card.price_max, currency) + '</ins></div>';

    var inlineOk = fld('inline_add') && canHover && card.axis_count >= 1 && card.axis_count <= 2;
    var sizes = '';
    if (inlineOk) {
      var sel = (card.swatches && card.swatches.length) ? card.swatches[0].value : null;
      var btns = (card.variants || []).map(function (v) {
        var label = card.axis_count === 2 ? v.option2 : v.option1;
        if (label == null) return '';
        var inColor = card.axis_count === 2 ? (v.option1 === sel) : true;
        return '<button type="button" class="dynasort-search__size" data-variant="' + esc(v.id) + '" data-color="' + esc(v.option1 || '') + '"' + (v.available ? '' : ' disabled') + (inColor ? '' : ' hidden') + '>' + esc(label) + '</button>';
      }).join('');
      if (btns) sizes = '<div class="dynasort-search__sizes" data-sizes>' + btns + '</div>';
    }

    var cls = 'dynasort-search__card' + (sizes ? ' dynasort-search__card--inline' : '');
    // Global rank across pagination, so CX click positions match the served list.
    var pos = page * perPage + cardIdx + 1;
    return '<div class="' + cls + '" data-card data-handle="' + esc(card.handle) + '" data-pid="' + esc(card.id) + '" data-pos="' + pos + '" data-visual-axis="' + card.visual_axis + '">' +
      '<div class="dynasort-search__media">' +
        '<a class="dynasort-search__media-link" href="' + esc(card.url) + '">' + img + '</a>' +
        (fld('badges') ? badgesHtml(card) : '') + sizes +
        '<div class="dynasort-search__added" data-added></div>' +
      '</div>' +
      '<a class="dynasort-search__title" href="' + esc(card.url) + '">' + esc(card.title) + '</a>' +
      colorName + (fld('rating') ? ratingHtml(card) : '') + price + (showSwatches ? swatchesHtml(card) : '') +
      '</div>';
  }

  function ensureImages(cardEl, card) {
    if (cardEl._dsImg) return Promise.resolve(cardEl._dsImg);
    return fetch('/products/' + card.handle + '.js', { headers: { 'Accept': 'application/json' } })
      .then(function (r) { return r.json(); })
      .then(function (prod) {
        var ai = (card.visual_axis || 1) - 1, map = {};
        (prod.variants || []).forEach(function (v) { var val = v.options && v.options[ai]; if (val && !map[val] && v.featured_image && v.featured_image.src) map[val] = v.featured_image.src; });
        cardEl._dsImg = map; return map;
      }).catch(function () { cardEl._dsImg = {}; return {}; });
  }

  function wireCard(cardEl, card) {
    var swatchWrap = cardEl.querySelector('[data-swatches]');
    var priceEl = cardEl.querySelector('[data-price]');
    var colorNameEl = cardEl.querySelector('[data-colorname]');
    var sizesEl = cardEl.querySelector('[data-sizes]');
    var mainImg = cardEl.querySelector('[data-main-img]');

    if (swatchWrap) {
      swatchWrap.addEventListener('click', function (e) {
        var btn = e.target.closest('[data-swatch]'); if (!btn) return;
        var s = card.swatches[parseInt(btn.getAttribute('data-swatch'), 10)]; if (!s) return;
        Array.prototype.forEach.call(swatchWrap.querySelectorAll('[data-swatch]'), function (b) { b.setAttribute('aria-pressed', 'false'); });
        btn.setAttribute('aria-pressed', 'true');
        if (priceEl) {
          var cmp = (s.compare_at_min != null) ? '<del>' + money(s.compare_at_min, currency) + '</del>' : '';
          priceEl.innerHTML = cmp + '<ins>' + priceRange(s.price_min, s.price_max, currency) + '</ins>';
        }
        if (colorNameEl) colorNameEl.textContent = s.name;
        if (sizesEl && card.axis_count === 2) {
          Array.prototype.forEach.call(sizesEl.querySelectorAll('[data-variant]'), function (sb) { if (sb.getAttribute('data-color') === s.value) sb.removeAttribute('hidden'); else sb.setAttribute('hidden', ''); });
        }
        if (mainImg) ensureImages(cardEl, card).then(function (map) { if (map[s.value]) mainImg.src = map[s.value]; });
      });
    }
    if (sizesEl) {
      sizesEl.addEventListener('click', function (e) {
        var sb = e.target.closest('[data-variant]'); if (!sb || sb.disabled) return;
        addToCart(sb.getAttribute('data-variant'), cardEl);
      });
    }
    // CX: click-through to the PDP (any card link; the beacon outlives navigation).
    cardEl.addEventListener('click', function (e) {
      if (e.target.closest('a[href]')) { cxBeacon('click', cardEl); }
    });
  }

  // Theme cart UI refresh after an AJAX add. Themes have no shared contract for
  // this, so cover the common families: update known count nodes from /cart.js,
  // re-render Dawn-family's cart-icon-bubble section when present, and broadcast
  // the event names the major themes listen for. All paths are harmless no-ops
  // on themes that lack them.
  function refreshCartUi() {
    fetch('/cart.js', { headers: { 'Accept': 'application/json' } })
      .then(function (r) { return r.json(); })
      .then(function (cart) {
        var n = cart.item_count;
        ['[data-cart-count]', '.cart-count-bubble span:not(.visually-hidden)', '.cart-count', '#CartCount', '.cart-count-number'].forEach(function (sel) {
          document.querySelectorAll(sel).forEach(function (el) {
            el.textContent = n;
            if (el.hasAttribute('data-cart-count')) el.setAttribute('data-cart-count', n);
          });
        });
        if (document.getElementById('cart-icon-bubble')) {
          fetch(window.location.pathname + '?sections=cart-icon-bubble')
            .then(function (r) { return r.json(); })
            .then(function (sections) {
              if (!sections || !sections['cart-icon-bubble']) return;
              // Assign INNER html into the existing element (Dawn's own approach):
              // replacing the node itself swaps the inline bubble for the rendered
              // section's full-width wrapper and it falls out of the header.
              var doc = new DOMParser().parseFromString(sections['cart-icon-bubble'], 'text/html');
              var src = doc.getElementById('cart-icon-bubble') || doc.querySelector('.shopify-section') || doc.body;
              var cur = document.getElementById('cart-icon-bubble');
              if (src && cur) cur.innerHTML = src.innerHTML;
            }).catch(function () {});
        }
        document.dispatchEvent(new CustomEvent('cart:refresh'));
        document.dispatchEvent(new CustomEvent('cart:updated', { detail: { cart: cart } }));
        document.dispatchEvent(new CustomEvent('cart:change', { detail: { cart: cart } }));
        document.documentElement.dispatchEvent(new CustomEvent('cart:build'));
        // Horizon family (web-component themes): cart icon + drawer listen for the
        // standard storefront event and read an attached promise resolving to
        // {cart:{totalQuantity}} / {detail:{itemCount}}. We attach no sections HTML,
        // which makes Horizon's cart-items component fall back to re-rendering its
        // own section, so the drawer contents refresh too.
        try {
          var se = new Event('shopify:cart:lines-update', { bubbles: true, cancelable: true });
          se.promise = Promise.resolve({ cart: { totalQuantity: n }, detail: { itemCount: n } });
          document.dispatchEvent(se);
        } catch (err) {}
      }).catch(function () {});
  }

  function addToCart(variantId, cardEl) {
    if (!variantId) return;
    fetch('/cart/add.js', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify({ id: variantId, quantity: 1 }) })
      .then(function (r) { return r.json(); })
      .then(function () {
        var added = cardEl.querySelector('[data-added]');
        if (added) { added.textContent = addedLabel; added.classList.add('is-shown'); setTimeout(function () { added.classList.remove('is-shown'); }, 2200); }
        cxBeacon('inline_add', cardEl);
        refreshCartUi();
      }).catch(function () {});
  }

  function totalPages() { return Math.max(1, Math.ceil(products.length / perPage)); }

  function renderPage(p) {
    var tp = totalPages();
    page = Math.max(0, Math.min(p, tp - 1));
    var slice = products.slice(page * perPage, page * perPage + perPage);
    gridEl.innerHTML = slice.map(cardHtml).join('');
    Array.prototype.forEach.call(gridEl.querySelectorAll('[data-card]'), function (el, i) { wireCard(el, slice[i]); });
    if (tp > 1) {
      pagerEl.removeAttribute('hidden');
      prevBtn.disabled = page === 0;
      nextBtn.disabled = page >= tp - 1;
      pageInfoEl.textContent = 'Page ' + (page + 1) + ' of ' + tp;
    } else {
      pagerEl.setAttribute('hidden', '');
    }
  }

  function skeleton() {
    var n = Math.min(perPage, 8), cells = '';
    for (var i = 0; i < n; i++) cells += '<div class="dynasort-search__skeleton"></div>';
    gridEl.innerHTML = cells;
  }

  function render(data) {
    products = data.products || [];
    currency = data.currency || 'USD';
    serveId = data.serve_id || 0;
    // Apply admin presentation settings (columns + page size); fields used in cardHtml.
    if (data.settings && typeof data.settings === 'object') { settings = data.settings; }
    var L = (settings.layout && settings.layout.columns) || {};
    if (L.mobile)  root.style.setProperty('--ds-cols-mobile', L.mobile);
    if (L.tablet)  root.style.setProperty('--ds-cols-tablet', L.tablet);
    if (L.desktop) root.style.setProperty('--ds-cols-desktop', L.desktop);
    if (settings.layout && settings.layout.per_page) { perPage = parseInt(settings.layout.per_page, 10) || perPage; }
    if (summaryEl) {
      summaryEl.textContent = products.length + (products.length === 1 ? ' result for ' : ' results for ') + '“' + query + '”';
      summaryEl.removeAttribute('hidden');
    }
    if (!products.length) {
      gridEl.innerHTML = '';
      emptyEl.textContent = 'No results for “' + query + '”.';
      emptyEl.removeAttribute('hidden');
      pagerEl.setAttribute('hidden', '');
      return;
    }
    renderPage(0);
  }

  if (prevBtn) prevBtn.addEventListener('click', function () { renderPage(page - 1); root.scrollIntoView({ behavior: 'smooth', block: 'start' }); });
  if (nextBtn) nextBtn.addEventListener('click', function () { renderPage(page + 1); root.scrollIntoView({ behavior: 'smooth', block: 'start' }); });

  if (!query) return; // no query → leave the section dark, let the theme handle it

  // Filter + sort passthrough. Theme filter links use dotted keys
  // (filter.v.price.gte) which cannot cross the App Proxy as KEYS (PHP mangles
  // dots and the proxy signature breaks), so all filter params are packed into
  // ONE `dsf` value. Repeated keys are comma-joined for the same reason.
  var sortEl = root.querySelector('[data-sort]');
  function currentFilterString() {
    var seen = {};
    var usp = new URLSearchParams(window.location.search);
    usp.forEach(function (v, k) {
      if (k.indexOf('filter.') !== 0 || v === '') return;
      seen[k] = seen[k] ? seen[k] + ',' + v : v;
    });
    return Object.keys(seen).sort().map(function (k) {
      return encodeURIComponent(k) + '=' + encodeURIComponent(seen[k]);
    }).join('&');
  }
  var currentSort = (new URLSearchParams(window.location.search).get('sort_by')) || '';
  if (currentSort !== 'price-ascending' && currentSort !== 'price-descending') currentSort = '';

  function fetchResults() {
    skeleton();
    var dsf = currentFilterString();
    var url = '/apps/dynasort/search?q=' + encodeURIComponent(query) + '&limit=250&_=' + Date.now() +
      (dsSession() ? '&ds_session=' + encodeURIComponent(dsSession()) : '') +
      '&vw=' + (window.innerWidth || 0) +
      (dsf ? '&dsf=' + encodeURIComponent(dsf) : '') +
      (currentSort ? '&sort_by=' + currentSort : '');
    fetch(url, { headers: { 'Accept': 'application/json' }, cache: 'no-store' })
      .then(function (r) { return r.json(); })
      .then(function (data) {
        render(data);
        if (sortEl) { sortEl.value = currentSort; sortEl.removeAttribute('hidden'); }
      })
      .catch(function () { root.setAttribute('hidden', ''); });
  }

  if (sortEl) {
    sortEl.addEventListener('change', function () {
      currentSort = sortEl.value;
      var u = new URL(window.location.href);
      if (currentSort) { u.searchParams.set('sort_by', currentSort); } else { u.searchParams.delete('sort_by'); }
      window.history.replaceState({}, '', u.toString());
      fetchResults();
    });
  }

  root.removeAttribute('hidden');
  fetchResults();
})();
</script>

{% schema %}
{
  "target": "section",
  "name": "Dynasort Search Results",
  "settings": [
    { "type": "paragraph", "content": "Replaces the theme's search results grid with Dynasort recipe-ranked results. Shopify still decides which products match; the active Search recipe decides their order." },
    { "type": "checkbox", "id": "show_summary", "label": "Show results summary", "default": true },
    { "type": "range", "id": "per_page", "label": "Results per page", "min": 12, "max": 48, "step": 4, "default": 24 },
    { "type": "range", "id": "columns_desktop", "label": "Columns (desktop)", "min": 2, "max": 6, "step": 1, "default": 4 },
    { "type": "range", "id": "columns_tablet", "label": "Columns (tablet)", "min": 2, "max": 4, "step": 1, "default": 3 },
    { "type": "range", "id": "columns_mobile", "label": "Columns (mobile)", "min": 1, "max": 3, "step": 1, "default": 2 }
  ]
}
{% endschema %}
