// ../sdk/dist/index.js
var SeekmodoError = class extends Error {
  status;
  body;
  tool;
  constructor(message, status, body, tool) {
    super(message);
    this.name = "SeekmodoError";
    this.status = status;
    this.body = body;
    this.tool = tool;
  }
};
var SeekmodoAuthError = class extends SeekmodoError {
  constructor(status, body, tool) {
    super(`Seekmodo auth failed (HTTP ${status})`, status, body, tool);
    this.name = "SeekmodoAuthError";
  }
};
var SeekmodoQuotaError = class extends SeekmodoError {
  code;
  bucket;
  limit;
  used;
  constructor(body, tool) {
    super("Seekmodo over quota (HTTP 402)", 402, body, tool);
    this.name = "SeekmodoQuotaError";
    const env = body ?? {};
    this.code = env.code ?? "over_quota";
    this.bucket = env.bucket;
    this.limit = env.limit;
    this.used = env.used;
  }
};
var SeekmodoServerError = class extends SeekmodoError {
  constructor(status, body, tool) {
    super(`Seekmodo server error (HTTP ${status})`, status, body, tool);
    this.name = "SeekmodoServerError";
  }
};
var SeekmodoRequestError = class extends SeekmodoError {
  constructor(status, body, tool) {
    super(`Seekmodo request rejected (HTTP ${status})`, status, body, tool);
    this.name = "SeekmodoRequestError";
  }
};
var SeekmodoNetworkError = class extends SeekmodoError {
  constructor(cause, tool) {
    super(
      `Seekmodo network failure${cause instanceof Error ? `: ${cause.message}` : ""}`,
      0,
      cause,
      tool
    );
    this.name = "SeekmodoNetworkError";
  }
};
function isLikelyCorsBlock(err, opts) {
  if (opts?.scriptLoad) {
    return true;
  }
  if (err instanceof SeekmodoNetworkError) {
    return isLikelyCorsBlock(err.body);
  }
  if (err instanceof TypeError) {
    const msg = err.message.toLowerCase();
    return msg.includes("failed to fetch") || msg.includes("networkerror") || msg.includes("network request failed") || msg.includes("load failed");
  }
  if (err instanceof Error) {
    const msg = err.message.toLowerCase();
    return msg.includes("cors") || msg.includes("access-control-allow-origin") || msg.includes("cross-origin");
  }
  return false;
}
var DEFAULT_BASE_URL = "https://gateway.seekmodo.com";
var DEFAULT_TIMEOUT_MS = 8e3;
var SeekmodoTransport = class {
  config;
  cachedToken = null;
  constructor(config) {
    if (!config.tenantId) {
      throw new Error("Seekmodo SDK: tenantId is required");
    }
    if (typeof config.getToken !== "function") {
      throw new Error("Seekmodo SDK: getToken callback is required");
    }
    this.config = {
      tenantId: config.tenantId,
      getToken: config.getToken,
      baseUrl: (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, ""),
      fetch: config.fetch ?? globalThis.fetch.bind(globalThis),
      timeoutMs: config.timeoutMs ?? DEFAULT_TIMEOUT_MS,
      signal: config.signal,
      onError: config.onError,
      getRegion: config.getRegion
    };
  }
  /**
   * Drop any cached token. Call this when the host app knows the
   * stored token is stale (e.g. after a logout, after the connector
   * rotates the per-tenant browser secret).
   */
  clearTokenCache() {
    this.cachedToken = null;
  }
  async call(tool, args, opts = {}) {
    try {
      return await this.callOnce(tool, args, opts, false);
    } catch (err) {
      if (err instanceof SeekmodoAuthError) {
        this.clearTokenCache();
        try {
          return await this.callOnce(tool, args, opts, true);
        } catch (retryErr) {
          this.config.onError?.(retryErr, { tool });
          throw retryErr;
        }
      }
      this.config.onError?.(err, { tool });
      throw err;
    }
  }
  async callOnce(tool, args, opts, isRetry) {
    const token = await this.resolveToken(isRetry);
    const url = `${this.config.baseUrl}/v1/${encodeURIComponent(tool)}`;
    const controller = new AbortController();
    const timeoutMs = opts.timeoutMs ?? this.config.timeoutMs;
    const timer = setTimeout(() => controller.abort(), timeoutMs);
    const onParentAbort = () => controller.abort();
    this.config.signal?.addEventListener("abort", onParentAbort, {
      once: true
    });
    opts.signal?.addEventListener("abort", onParentAbort, { once: true });
    const headers = {
      "Content-Type": "application/json",
      Authorization: `Bearer ${token}`,
      "X-Seekmodo-Tenant": this.config.tenantId,
      "X-Seekmodo-SDK": "@seekmodo/sdk@0.1.0"
    };
    if (this.config.getRegion) {
      try {
        const slug = await this.config.getRegion();
        if (typeof slug === "string" && slug.length > 0) {
          headers["Seekmodo-Region"] = slug;
        }
      } catch {
      }
    }
    let res;
    try {
      res = await this.config.fetch(url, {
        method: "POST",
        headers,
        body: JSON.stringify(args),
        signal: controller.signal
      });
    } catch (cause) {
      throw new SeekmodoNetworkError(cause, tool);
    } finally {
      clearTimeout(timer);
      this.config.signal?.removeEventListener("abort", onParentAbort);
      opts.signal?.removeEventListener("abort", onParentAbort);
    }
    const text = await res.text();
    const body = text ? safeJson(text) : null;
    if (res.status === 401 || res.status === 403) {
      throw new SeekmodoAuthError(res.status, body, tool);
    }
    if (res.status === 402) {
      throw new SeekmodoQuotaError(body, tool);
    }
    if (res.status >= 500) {
      throw new SeekmodoServerError(res.status, body, tool);
    }
    if (!res.ok) {
      throw new SeekmodoRequestError(res.status, body, tool);
    }
    return body;
  }
  async resolveToken(forceRefresh) {
    const now = Date.now();
    if (!forceRefresh && this.cachedToken && this.cachedToken.expiresAt - 1e4 > now) {
      return this.cachedToken.token;
    }
    const result = await this.config.getToken();
    if (typeof result === "string") {
      this.cachedToken = { token: result, expiresAt: now + 6e4 };
      return result;
    }
    if (result && typeof result === "object" && typeof result.token === "string" && typeof result.expiresAt === "number") {
      this.cachedToken = { token: result.token, expiresAt: result.expiresAt };
      return result.token;
    }
    throw new Error(
      "Seekmodo SDK: getToken must return a string or { token, expiresAt }"
    );
  }
};
function safeJson(text) {
  try {
    return JSON.parse(text);
  } catch {
    return text;
  }
}
var SeekmodoClient = class {
  /** Low-level transport. Escape hatch for tools not on the namespaced surface. */
  transport;
  /** Recommendations family — one entry per algorithm in the gateway's `recommend.*`. */
  recommend;
  /** Bundle family. */
  bundle;
  constructor(config) {
    this.transport = new SeekmodoTransport(config);
    this.recommend = {
      related: (args, opts) => this.transport.call("recommend.related", { ...args }, opts ?? {}),
      alsoBought: (args, opts) => this.transport.call("recommend.also_bought", { ...args }, opts ?? {}),
      alsoViewed: (args, opts) => this.transport.call("recommend.also_viewed", { ...args }, opts ?? {}),
      trending: (args, opts) => this.transport.call("recommend.trending", { ...args }, opts ?? {})
    };
    this.bundle = {
      suggest: (args, opts) => this.transport.call("bundle.suggest", { ...args }, opts ?? {})
    };
  }
  search(args, opts) {
    return this.transport.call("search", { ...args }, opts ?? {});
  }
  suggest(args, opts) {
    return this.transport.call("suggest", { ...args }, opts ?? {});
  }
  /**
   * Sprint 8 PR 5 — image-as-query search. Resolves an image to a
   * CLIP vector via the gateway's embed-worker sidecar (or reads
   * it off the indexed doc in the `doc_id` "more like this" path)
   * and runs a Typesense `vector_query` against the
   * `image_embedding` field.
   *
   * Costs 2× the master `searches` bucket per request. Hobby/
   * Starter tenants receive a 402 `feature_not_in_plan` envelope
   * — storefront widgets should swallow it and degrade silently
   * to text-only search. Growth+ tenants must also enable the
   * per-merchant `image_search_enabled` toggle on the admin
   * /image-search page (PR 6) before the gateway will accept
   * calls.
   *
   * Honor `response.meta.suppressed` in the UI — when true, the
   * gateway decided the response would push the tenant past
   * their `searches` quota without prepaid credits. Render the
   * upgrade affordance instead of `response.hits`.
   *
   * Pass exactly one of `image_b64` / `image_url` / `doc_id`. The
   * `<seekmodo-image-search>` web component handles the resize +
   * base64 encoding pipeline for the common shopper path.
   */
  searchByImage(args, opts) {
    return this.transport.call("search.byImage", { ...args }, opts ?? {});
  }
  /**
   * Conversational search. The gateway runs the LLM loop server-side
   * and returns the assembled answer plus the product citations the
   * answer references. Sprint 7 PR 4 lands the gateway-side ChatTool;
   * this method ships in PR 1 so consumers can pin to a single minor
   * version and pick chat up automatically when the gateway exposes
   * it.
   *
   * Honor `response.meta.suppressed` in the UI — when true, the
   * gateway decided the response would push the tenant past their
   * quota without credits and you should render the upgrade affordance
   * instead of `response.message`.
   */
  chat(args, opts) {
    return this.transport.call("chat", { ...args }, opts ?? {});
  }
  /**
   * Single-turn Ask AI over the tenant catalog and policy docs.
   * Honor `response.meta.suppressed` in the UI.
   */
  ask(args, opts) {
    return this.transport.call("ask", { ...args }, opts ?? {});
  }
  /**
   * Fire a telemetry beacon. The connector calls this on every real-
   * user click / view / add-to-cart / purchase so LTR + revenue
   * attribution have ground truth. `event_type='purchase'` writes to
   * both the legacy click-events table and the Sprint 6
   * conversion-events table for attribution.
   *
   * Fire-and-forget by convention: storefronts should not block UI
   * on the response. The SDK does not retry — beacons are cheap and
   * a single loss is acceptable.
   */
  event(args, opts) {
    return this.transport.call("events", { ...args }, opts ?? {});
  }
};

// src/shared/client.ts
var clientPromise = null;
var cachedToken = null;
function readMeta(name) {
  if (typeof document === "undefined") return null;
  const el = document.head?.querySelector(
    `meta[name="${name}"]`
  );
  const v = el?.getAttribute("content");
  return v && v.length > 0 ? v : null;
}
function getSeekmodoClient() {
  if (clientPromise !== null) return clientPromise;
  clientPromise = build();
  return clientPromise;
}
async function build() {
  const tenantId = readMeta("seekmodo:tenant");
  if (!tenantId) {
    throw new Error(
      '@seekmodo/web-components: <meta name="seekmodo:tenant"> is required'
    );
  }
  const inlineToken = readMeta("seekmodo:token");
  const refreshUrl = readMeta("seekmodo:refresh");
  if (!inlineToken && !refreshUrl) {
    throw new Error(
      '@seekmodo/web-components: either <meta name="seekmodo:token"> or <meta name="seekmodo:refresh"> must be set'
    );
  }
  if (inlineToken) {
    cachedToken = { token: inlineToken, expiresAt: Date.now() + 3e4 };
  }
  const baseUrl = readMeta("seekmodo:gateway") ?? void 0;
  return new SeekmodoClient({
    tenantId,
    baseUrl,
    // Sprint 9 PR 6 — read the region from the cookie on every
    // call. Cheap (one RegExp) and lets the SDK pick up region
    // changes between calls without a singleton reset.
    getRegion: () => readSeekmodoRegionCookie(),
    getToken: async () => {
      const now = Date.now();
      if (cachedToken && cachedToken.expiresAt - 1e4 > now) {
        return { token: cachedToken.token, expiresAt: cachedToken.expiresAt };
      }
      if (!refreshUrl) {
        if (cachedToken) {
          return { token: cachedToken.token, expiresAt: cachedToken.expiresAt };
        }
        throw new Error("seekmodo:refresh meta missing; no way to refresh token");
      }
      const r = await fetch(refreshUrl, {
        method: "POST",
        credentials: "same-origin",
        headers: { "Content-Type": "application/json" }
      });
      if (!r.ok) {
        throw new Error(`seekmodo:refresh route returned HTTP ${r.status}`);
      }
      const body = await r.json();
      if (!body.token || typeof body.expires_at !== "number") {
        throw new Error("seekmodo:refresh route returned a malformed envelope");
      }
      cachedToken = {
        token: body.token,
        expiresAt: body.expires_at * 1e3
      };
      return { token: cachedToken.token, expiresAt: cachedToken.expiresAt };
    }
  });
}
function resetSeekmodoClient() {
  clientPromise = null;
  cachedToken = null;
}
var SEEKMODO_REGION_COOKIE = "seekmodo_region";
var SEEKMODO_REGION_EVENT = "seekmodo:region-change";
function sanitiseRegionSlug(input) {
  if (typeof input !== "string") return null;
  const trimmed = input.trim().toLowerCase();
  if (!/^[a-z0-9][a-z0-9_-]{1,63}$/.test(trimmed)) return null;
  return trimmed;
}
function readSeekmodoRegionCookie() {
  if (typeof document === "undefined") return null;
  const cookieStr = document.cookie ?? "";
  if (cookieStr.length === 0) return null;
  const escaped = SEEKMODO_REGION_COOKIE.replace(
    /[.*+?^${}()|[\]\\]/g,
    "\\$&"
  );
  const match = new RegExp(`(?:^|; )${escaped}=([^;]+)`).exec(cookieStr);
  if (!match) return null;
  try {
    return sanitiseRegionSlug(decodeURIComponent(match[1]));
  } catch {
    return null;
  }
}
function setSeekmodoRegion(slug, opts) {
  if (typeof document === "undefined") return null;
  if (slug === null || slug === "") {
    document.cookie = `${SEEKMODO_REGION_COOKIE}=; Path=/; Max-Age=0; SameSite=Lax`;
    document.dispatchEvent(
      new CustomEvent(SEEKMODO_REGION_EVENT, {
        detail: { region: null },
        bubbles: false
      })
    );
    return null;
  }
  const clean = sanitiseRegionSlug(slug);
  if (clean === null) return null;
  const maxAge = opts?.maxAgeSeconds ?? 60 * 60 * 24 * 365;
  const domainAttr = opts?.domain ? `; Domain=${opts.domain}` : "";
  document.cookie = `${SEEKMODO_REGION_COOKIE}=${encodeURIComponent(clean)}; Path=/; Max-Age=${maxAge}; SameSite=Lax${domainAttr}`;
  document.dispatchEvent(
    new CustomEvent(SEEKMODO_REGION_EVENT, {
      detail: { region: clean },
      bubbles: false
    })
  );
  return clean;
}
function getSeekmodoRegion() {
  return readSeekmodoRegionCookie();
}

// src/shared/base-element.ts
var SeekmodoElement = class extends HTMLElement {
  root;
  rafId = null;
  constructor() {
    super();
    this.root = this.attachShadow({ mode: "open" });
  }
  /**
   * Schedule a render on the next animation frame. Multiple calls
   * inside the same tick coalesce — useful when an attribute change
   * + a state change fire in quick succession.
   */
  scheduleRender() {
    if (this.rafId !== null) return;
    this.rafId = requestAnimationFrame(() => {
      this.rafId = null;
      try {
        this.render();
        this.afterRender();
      } catch (err) {
        console.warn("[seekmodo] render failure", err);
        try {
          this.renderError("internal_error");
        } catch {
          this.root.innerHTML = "";
        }
      }
    });
  }
  /**
   * Hook after a successful `render()`. Used by suggest to emit
   * post-paint hydration events so connectors never race the rAF.
   */
  afterRender() {
  }
  async getClient() {
    return getSeekmodoClient();
  }
  renderError(_reason) {
    this.root.innerHTML = "";
  }
  disconnectedCallback() {
    if (this.rafId !== null) {
      cancelAnimationFrame(this.rafId);
      this.rafId = null;
    }
  }
};

// src/shared/dom.ts
function h(tag, props, children) {
  const el = document.createElement(tag);
  if (props) {
    for (const [k, v] of Object.entries(props)) {
      if (v === null || v === void 0 || v === false) continue;
      if (k === "class") el.className = String(v);
      else if (k === "part") el.setAttribute("part", String(v));
      else if (k === "text") el.textContent = String(v);
      else if (k === "html") el.innerHTML = String(v);
      else if (k === "attrs" && typeof v === "object" && v !== null) {
        for (const [ak, av] of Object.entries(v)) {
          el.setAttribute(ak, av);
        }
      } else {
        el.setAttribute(k, String(v));
      }
    }
  }
  if (children) {
    for (const c of children) {
      if (c === null || c === void 0) continue;
      el.append(typeof c === "string" ? document.createTextNode(c) : c);
    }
  }
  return el;
}
function debounce(fn, ms) {
  let t = null;
  return (...args) => {
    if (t !== null) clearTimeout(t);
    t = setTimeout(() => fn(...args), ms);
  };
}
function emit(el, type, detail) {
  el.dispatchEvent(
    new CustomEvent(type, { detail, bubbles: true, composed: true })
  );
}

// src/components/search-box.ts
var TEMPLATE_STYLES = `
  :host {
    display: inline-block;
    width: var(--seekmodo-search-box-width, 100%);
    font-family: inherit;
  }
  form {
    display: flex;
    align-items: stretch;
    gap: var(--seekmodo-search-box-gap, 0);
  }
  input[part="input"] {
    flex: 1;
    box-sizing: border-box;
    padding: var(--seekmodo-input-padding, 0.5rem 0.75rem);
    border: var(--seekmodo-input-border, 1px solid #d4d4d8);
    border-radius: var(--seekmodo-input-radius, 0.375rem) 0 0 var(--seekmodo-input-radius, 0.375rem);
    font: inherit;
    color: inherit;
    background: var(--seekmodo-input-bg, white);
  }
  input[part="input"]:focus {
    outline: var(--seekmodo-input-focus-outline, 2px solid #2563eb);
    outline-offset: -2px;
  }
  button[part="submit"] {
    padding: var(--seekmodo-button-padding, 0.5rem 1rem);
    border: var(--seekmodo-input-border, 1px solid #d4d4d8);
    border-left: none;
    border-radius: 0 var(--seekmodo-input-radius, 0.375rem) var(--seekmodo-input-radius, 0.375rem) 0;
    background: var(--seekmodo-submit-bg, #2563eb);
    color: var(--seekmodo-submit-fg, white);
    cursor: pointer;
    font: inherit;
  }
  button[part="submit"]:hover { filter: brightness(1.05); }
`;
var SeekmodoSearchBox = class extends SeekmodoElement {
  static get observedAttributes() {
    return ["placeholder", "value"];
  }
  input = null;
  connectedCallback() {
    this.scheduleRender();
  }
  attributeChangedCallback(name, _old, val) {
    if (!this.input) return;
    if (name === "placeholder") this.input.placeholder = val ?? "Search products";
    if (name === "value" && val !== null && this.input.value !== val) {
      this.input.value = val;
    }
  }
  render() {
    if (this.input) return;
    const style = document.createElement("style");
    style.textContent = TEMPLATE_STYLES;
    const placeholder = this.getAttribute("placeholder") ?? "Search products";
    const initial = this.getAttribute("value") ?? "";
    this.input = h("input", {
      part: "input",
      type: "search",
      attrs: {
        autocomplete: "off",
        spellcheck: "false",
        "aria-label": placeholder
      },
      placeholder,
      value: initial
    });
    const submit = h("button", {
      part: "submit",
      type: "submit",
      text: this.getAttribute("submit-label") ?? "Search"
    });
    const form = h("form", { attrs: { role: "search" } }, [this.input, submit]);
    form.addEventListener("submit", (e) => {
      e.preventDefault();
      this.dispatchSearch();
    });
    this.input.addEventListener("input", () => {
      emit(this, "seekmodo:input", { query: this.input.value });
    });
    this.root.replaceChildren(style, form);
  }
  dispatchSearch() {
    const query = (this.input?.value ?? "").trim();
    emit(this, "seekmodo:search", { query });
  }
  /** Imperative submit — useful for forms wired via storefront JS. */
  submit() {
    this.dispatchSearch();
  }
  /** Imperative value setter that doesn't lose focus. */
  setQuery(q) {
    if (this.input && this.input.value !== q) {
      this.input.value = q;
    }
  }
};

// src/components/results.ts
var STYLES = `
  :host { display: block; font-family: inherit; }
  ul[part="list"] {
    list-style: none; padding: 0; margin: 0;
    display: grid;
    grid-template-columns: repeat(var(--seekmodo-grid-cols, 4), minmax(0, 1fr));
    gap: var(--seekmodo-grid-gap, 1rem);
  }
  @media (max-width: 700px) {
    ul[part="list"] { grid-template-columns: repeat(var(--seekmodo-grid-cols-mobile, 2), minmax(0, 1fr)); }
  }
  li[part="item"] {
    border: var(--seekmodo-card-border, 1px solid #e4e4e7);
    border-radius: var(--seekmodo-card-radius, 0.5rem);
    overflow: hidden;
    display: flex; flex-direction: column;
    transition: box-shadow 0.15s;
  }
  li[part="item"]:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
  a { color: inherit; text-decoration: none; display: flex; flex-direction: column; height: 100%; }
  img[part="image"] {
    width: 100%; aspect-ratio: 1 / 1; object-fit: cover;
    background: #f4f4f5;
  }
  .body { padding: 0.5rem 0.75rem; display: flex; flex-direction: column; gap: 0.25rem; }
  .title { font-weight: 500; font-size: 0.95rem; }
  .price { color: var(--seekmodo-price-color, #16a34a); font-weight: 600; }
  nav[part="pagination"] {
    display: flex; justify-content: space-between; padding: 1rem 0;
  }
  button {
    padding: 0.4rem 0.8rem; background: white;
    border: 1px solid #d4d4d8; border-radius: 0.375rem;
    cursor: pointer; font: inherit;
  }
  button:disabled { opacity: 0.5; cursor: not-allowed; }
  .empty { padding: 2rem; text-align: center; color: #71717a; }
`;
var SeekmodoResults = class extends SeekmodoElement {
  static get observedAttributes() {
    return ["query", "filter-by", "sort-by", "page", "per-page"];
  }
  current = null;
  loadToken = 0;
  connectedCallback() {
    this.scheduleRender();
    void this.fetchAndRender();
    document.addEventListener(SEEKMODO_REGION_EVENT, this.onRegionChange);
  }
  disconnectedCallback() {
    super.disconnectedCallback();
    document.removeEventListener(SEEKMODO_REGION_EVENT, this.onRegionChange);
  }
  onRegionChange = () => {
    void this.fetchAndRender();
  };
  attributeChangedCallback() {
    if (!this.isConnected) return;
    void this.fetchAndRender();
  }
  async fetchAndRender() {
    const token = ++this.loadToken;
    try {
      const client = await this.getClient();
      const res = await client.search({
        q: this.getAttribute("query") ?? "",
        filter_by: this.getAttribute("filter-by") ?? void 0,
        sort_by: this.getAttribute("sort-by") ?? void 0,
        page: parseInt(this.getAttribute("page") ?? "1", 10) || 1,
        per_page: parseInt(this.getAttribute("per-page") ?? "24", 10) || 24
      });
      if (token !== this.loadToken) return;
      this.current = res;
      this.scheduleRender();
      emit(this, "seekmodo:hits", {
        hits: res.hits,
        found: res.found,
        page: res.page
      });
      void client.event({
        event_type: "search_impression",
        query: this.getAttribute("query") ?? void 0
      }).catch(() => {
      });
    } catch (err) {
      if (token !== this.loadToken) return;
      console.warn("[seekmodo-results] fetch failed", err);
      emit(this, "seekmodo:error", { reason: err?.message ?? "unknown" });
      this.current = null;
      this.scheduleRender();
    }
  }
  render() {
    const style = document.createElement("style");
    style.textContent = STYLES;
    if (!this.current) {
      this.root.replaceChildren(style);
      return;
    }
    const { hits, found, page, per_page } = this.current;
    if (found === 0) {
      const empty = h("div", { class: "empty" });
      const slot = document.createElement("slot");
      slot.textContent = "No results found.";
      empty.append(slot);
      this.root.replaceChildren(style, empty);
      return;
    }
    const list = h("ul", { part: "list" });
    for (const hit of hits) {
      list.append(this.renderItem(hit));
    }
    const maxPage = Math.max(1, Math.ceil(found / per_page));
    const nav = h("nav", { part: "pagination" });
    const prev = h("button", { part: "prev", text: "Previous" });
    prev.toggleAttribute("disabled", page <= 1);
    prev.addEventListener("click", () => this.setAttribute("page", String(page - 1)));
    const next = h("button", { part: "next", text: "Next" });
    next.toggleAttribute("disabled", page >= maxPage);
    next.addEventListener("click", () => this.setAttribute("page", String(page + 1)));
    nav.append(prev, next);
    this.root.replaceChildren(style, list, nav);
  }
  renderItem(hit) {
    const titleField = this.getAttribute("title-field") ?? "title";
    const imageField = this.getAttribute("image-field") ?? "image_url";
    const priceField = this.getAttribute("price-field") ?? "price_cents";
    const urlField = this.getAttribute("url-field") ?? "url";
    const region = readRegion(this.current);
    const currency = this.getAttribute("currency") ?? region?.currency ?? "USD";
    const locale = region?.locale;
    const doc = hit.document;
    const title = String(doc[titleField] ?? "");
    const image = String(doc[imageField] ?? "");
    const priceCents = Number(doc[priceField] ?? 0);
    const url = String(doc[urlField] ?? "#");
    const docId = String(doc.id ?? "");
    const a = h("a", { href: url, attrs: { "data-doc-id": docId } });
    if (image) {
      a.append(
        h("img", {
          part: "image",
          attrs: { src: image, alt: title, loading: "lazy" }
        })
      );
    }
    const body = h("div", { class: "body" });
    body.append(
      h("div", { class: "title", part: "title", text: title }),
      h("div", {
        class: "price",
        part: "price",
        text: priceCents > 0 ? formatPrice(priceCents, currency, locale) : ""
      })
    );
    a.append(body);
    a.addEventListener("click", () => {
      void this.fireClick(docId);
    });
    const li = h("li", { part: "item" });
    li.append(a);
    return li;
  }
  async fireClick(docId) {
    if (!docId) return;
    try {
      const client = await this.getClient();
      void client.event({
        event_type: "click",
        doc_id: docId,
        query: this.getAttribute("query") ?? void 0
      }).catch(() => {
      });
    } catch {
    }
  }
};
function formatPrice(cents, currency, locale) {
  try {
    return new Intl.NumberFormat(locale ?? void 0, {
      style: "currency",
      currency
    }).format(cents / 100);
  } catch {
    return `${(cents / 100).toFixed(2)} ${currency}`;
  }
}
function readRegion(res) {
  const r = res?.meta?.region;
  if (!r || typeof r !== "object") return null;
  return {
    currency: typeof r.currency === "string" ? r.currency : void 0,
    locale: typeof r.locale === "string" ? r.locale : void 0
  };
}

// src/components/facets.ts
var STYLES2 = `
  :host { display: block; font-family: inherit; }
  details[part="group"] {
    border-bottom: 1px solid #e4e4e7;
    padding: 0.5rem 0;
  }
  summary { font-weight: 600; cursor: pointer; padding: 0.25rem 0; }
  ul { list-style: none; padding: 0; margin: 0.5rem 0 0; }
  li label {
    display: flex; align-items: center; gap: 0.5rem;
    padding: 0.2rem 0; cursor: pointer;
  }
  .count { color: #71717a; font-size: 0.85em; margin-left: auto; }
`;
var SeekmodoFacets = class extends SeekmodoElement {
  static get observedAttributes() {
    return ["source"];
  }
  facets = [];
  selected = {};
  subscribed = null;
  listener = (e) => {
    const detail = e.detail;
    if (detail && typeof detail === "object" && "facets" in detail) {
      const f = detail.facets ?? [];
      this.setFacets(f);
    }
  };
  connectedCallback() {
    this.subscribe();
    this.scheduleRender();
  }
  disconnectedCallback() {
    this.unsubscribe();
    super.disconnectedCallback();
  }
  attributeChangedCallback(name) {
    if (name === "source") {
      this.unsubscribe();
      this.subscribe();
    }
  }
  /** Imperative API for storefronts that fetch search themselves. */
  setHits(res) {
    this.setFacets(res.facets ?? []);
  }
  subscribe() {
    const id = this.getAttribute("source");
    if (!id) return;
    const target = document.getElementById(id);
    if (!target) return;
    this.subscribed = target;
    target.addEventListener("seekmodo:hits", this.listener);
  }
  unsubscribe() {
    if (this.subscribed) {
      this.subscribed.removeEventListener("seekmodo:hits", this.listener);
      this.subscribed = null;
    }
  }
  setFacets(facets) {
    this.facets = facets;
    this.scheduleRender();
  }
  render() {
    const style = document.createElement("style");
    style.textContent = STYLES2;
    const max = parseInt(this.getAttribute("max-per-facet") ?? "8", 10) || 8;
    const groups = [];
    for (const facet of this.facets) {
      const details = h("details", {
        part: "group",
        open: ""
      });
      details.append(
        h("summary", { part: "group-title", text: humaniseFieldName(facet.field_name) })
      );
      const ul = h("ul");
      const selectedForField = this.selected[facet.field_name] ?? [];
      for (const c of facet.counts.slice(0, max)) {
        const checked = selectedForField.includes(c.value);
        const id = `f-${facet.field_name}-${c.value}`;
        const label = h("label", { attrs: { for: id } });
        const cb = h("input", {
          attrs: { type: "checkbox", id, "data-field": facet.field_name, "data-value": c.value }
        });
        if (checked) cb.setAttribute("checked", "");
        cb.addEventListener("change", () => this.onToggle(facet.field_name, c.value, cb.checked));
        label.append(
          cb,
          h("span", { text: c.value }),
          h("span", { class: "count", text: String(c.count) })
        );
        const li = h("li");
        li.append(label);
        ul.append(li);
      }
      details.append(ul);
      groups.push(details);
    }
    this.root.replaceChildren(style, ...groups);
  }
  onToggle(field, value, checked) {
    const cur = new Set(this.selected[field] ?? []);
    if (checked) cur.add(value);
    else cur.delete(value);
    if (cur.size === 0) delete this.selected[field];
    else this.selected[field] = Array.from(cur);
    emit(this, "seekmodo:facet-change", {
      filter_by: this.toFilterBy(),
      selected: { ...this.selected }
    });
  }
  toFilterBy() {
    const parts = [];
    for (const [field, values] of Object.entries(this.selected)) {
      if (values.length === 0) continue;
      parts.push(`${field}:=[${values.map((v) => JSON.stringify(v)).join(",")}]`);
    }
    return parts.join(" && ");
  }
};
function humaniseFieldName(field) {
  return field.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
}

// src/shared/cors-notice.ts
var DEFAULT_CORS_BLOCKED_MESSAGE = "Search suggestions couldn't load because this site is blocked from reaching Seekmodo (CORS). Ask your store administrator to allowlist this domain on the Seekmodo gateway, or enable the connector's same-origin suggest proxy.";
function renderCorsBlockedPanel(root, styles, opts) {
  const style = document.createElement("style");
  style.textContent = styles;
  const wrap = h("div", {
    class: "wrap seekmodo-cors-blocked",
    part: "wrap cors-blocked",
    attrs: { role: "status" }
  });
  wrap.append(
    h("div", {
      class: "cors-notice",
      part: "cors-notice",
      text: opts?.message ?? DEFAULT_CORS_BLOCKED_MESSAGE
    })
  );
  root.replaceChildren(style, wrap);
}

// src/components/typeahead.ts
var STYLES3 = `
  :host {
    display: block;
    position: relative;
    font-family: inherit;
  }
  .wrap {
    background: white;
    border: 1px solid #d4d4d8;
    border-radius: 0.375rem;
    box-shadow: 0 4px 12px rgba(0,0,0,0.06);
    padding: 0.25rem 0;
    max-height: var(--seekmodo-typeahead-max-height, 60vh);
    overflow-y: auto;
  }
  .group-title {
    padding: 0.25rem 0.75rem;
    font-size: 0.75rem;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: #71717a;
  }
  button.item {
    width: 100%;
    text-align: left;
    background: none;
    border: none;
    padding: 0.4rem 0.75rem;
    cursor: pointer;
    font: inherit;
    display: flex;
    gap: 0.5rem;
    align-items: center;
  }
  button.item:hover, button.item:focus {
    background: #f4f4f5;
    outline: none;
  }
  .cors-notice {
    padding: 0.75rem;
    font-size: 0.875rem;
    line-height: 1.45;
    color: #5c4a00;
    background: #fff8e6;
  }
  img { width: 32px; height: 32px; object-fit: cover; border-radius: 0.25rem; }
  .hidden { display: none; }
`;
var SeekmodoTypeahead = class extends SeekmodoElement {
  static get observedAttributes() {
    return ["source", "min-length"];
  }
  current = null;
  corsBlocked = false;
  subscribed = null;
  debouncedFetch = debounce((q) => void this.fetch(q), 150);
  loadToken = 0;
  connectedCallback() {
    this.subscribe();
    this.scheduleRender();
  }
  disconnectedCallback() {
    this.unsubscribe();
    super.disconnectedCallback();
  }
  attributeChangedCallback(name) {
    if (name === "source") {
      this.unsubscribe();
      this.subscribe();
    }
  }
  subscribe() {
    const id = this.getAttribute("source");
    if (!id) return;
    const target = document.getElementById(id);
    if (!target) return;
    this.subscribed = target;
    target.addEventListener("seekmodo:input", this.onInput);
  }
  unsubscribe() {
    if (this.subscribed) {
      this.subscribed.removeEventListener("seekmodo:input", this.onInput);
      this.subscribed = null;
    }
  }
  onInput = (e) => {
    const q = e.detail?.query ?? "";
    const min = parseInt(this.getAttribute("min-length") ?? "2", 10) || 2;
    if (q.length < min) {
      this.current = null;
      this.corsBlocked = false;
      this.scheduleRender();
      return;
    }
    const ms = parseInt(this.getAttribute("debounce-ms") ?? "150", 10) || 150;
    if (ms === 150) {
      this.debouncedFetch(q);
    } else {
      this.debouncedFetch = debounce((qq) => void this.fetch(qq), ms);
      this.debouncedFetch(q);
    }
  };
  async fetch(q) {
    const token = ++this.loadToken;
    try {
      const client = await this.getClient();
      const res = await client.suggest({ q, limit: 10 });
      if (token !== this.loadToken) return;
      this.corsBlocked = false;
      this.current = res;
      this.scheduleRender();
    } catch (err) {
      if (token !== this.loadToken) return;
      this.corsBlocked = isLikelyCorsBlock(err);
      console.warn("[seekmodo-typeahead] fetch failed", err);
      this.current = null;
      this.scheduleRender();
    }
  }
  render() {
    const style = document.createElement("style");
    style.textContent = STYLES3;
    if (this.corsBlocked) {
      renderCorsBlockedPanel(this.root, STYLES3, {
        message: DEFAULT_CORS_BLOCKED_MESSAGE
      });
      return;
    }
    if (!this.current) {
      this.root.replaceChildren(style);
      return;
    }
    const wrap = h("div", { class: "wrap", part: "wrap" });
    const maxKw = parseInt(this.getAttribute("max-keyword") ?? "6", 10) || 6;
    const maxPd = parseInt(this.getAttribute("max-product") ?? "4", 10) || 4;
    const kws = (this.current.keywords ?? []).slice(0, maxKw);
    const prods = (this.current.products ?? []).slice(0, maxPd);
    const cats = (this.current.categories ?? []).slice(0, 4);
    if (kws.length > 0) {
      wrap.append(h("div", { class: "group-title", text: "Suggestions" }));
      for (const k of kws) {
        wrap.append(this.itemButton("keyword", k.keyword, void 0, k.keyword));
      }
    }
    if (prods.length > 0) {
      wrap.append(h("div", { class: "group-title", text: "Products" }));
      for (const p of prods) {
        const title = p.name ?? "";
        const img = p.image_url ?? p.image;
        wrap.append(this.itemButton("product", title, p.id, title, img));
      }
    }
    if (cats.length > 0) {
      wrap.append(h("div", { class: "group-title", text: "Categories" }));
      for (const c of cats) {
        wrap.append(this.itemButton("category", c.name, void 0, c.name));
      }
    }
    this.root.replaceChildren(style, wrap);
  }
  itemButton(kind, text, id, value, imageUrl) {
    const btn = h("button", {
      class: "item",
      part: "item",
      type: "button"
    });
    if (imageUrl) {
      btn.append(h("img", { attrs: { src: imageUrl, alt: "" } }));
    }
    btn.append(h("span", { text }));
    btn.addEventListener("click", () => {
      emit(this, "seekmodo:typeahead-select", { kind, value, id });
    });
    return btn;
  }
};

// src/components/suggest-shared.ts
function appendQueryParam(href, key, value) {
  try {
    const base = typeof window !== "undefined" ? window.location.origin : "http://localhost";
    const url = new URL(href, base);
    url.searchParams.set(key, value);
    if (/^https?:\/\//i.test(href)) {
      return url.toString();
    }
    return `${url.pathname}${url.search}${url.hash}`;
  } catch {
    const sep = href.includes("?") ? "&" : "?";
    return `${href}${sep}${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
  }
}
function contentTypeLabel(postType, url) {
  const pt = (postType ?? "").toLowerCase();
  let path = (url ?? "").toLowerCase();
  if (path.startsWith("http")) {
    try {
      path = new URL(path).pathname;
    } catch {
      path = "";
    }
  }
  if (pt === "page" && /\/tools\/[^/]+\/?$/.test(path)) {
    return "Tool";
  }
  if (pt === "page" && /\/tools\/?$/.test(path)) {
    return "Tools";
  }
  if (pt === "page") {
    return "Page";
  }
  if (pt === "post") {
    return "Article";
  }
  return "";
}
function productContentType(item) {
  const postType = typeof item.post_type === "string" ? item.post_type.toLowerCase() : "";
  const url = typeof item.url === "string" ? item.url : typeof item.permalink === "string" ? item.permalink : "";
  return {
    postType,
    label: contentTypeLabel(postType, url)
  };
}

// src/components/suggest-layouts.ts
var DEFAULT_SUGGEST_LAYOUT = "split-rail";
var WIDE_SUGGEST_PRODUCT_CAP = 15;
function wideLayoutProductLimit(layout, attrLimit) {
  const cols = {
    "split-rail": 5,
    "command-bar": 5,
    "cinema-grid": 6,
    "magazine": 6,
    "classic": 5
  };
  const columns = cols[layout] ?? 5;
  const desired = Math.max(attrLimit, columns * 3);
  return Math.min(WIDE_SUGGEST_PRODUCT_CAP, desired);
}
var WIDE_LAYOUT_STYLES = `
  .wrap.wide { padding: 0; overflow-x: hidden; overflow-y: auto; }
  .wrap.wide.split-rail-panel {
    overflow: hidden; display: flex; flex-direction: column;
    max-height: var(--_max-height);
  }
  .meta-bar {
    display: flex; align-items: center; justify-content: space-between;
    gap: 0.75rem; padding: 0.65rem 1rem; border-bottom: 1px solid var(--_border);
    background: var(--_row-hover); font-size: 0.8125rem;
  }
  .meta-bar .query { color: #2563eb; font-weight: 600; }
  .meta-bar .count { color: var(--_group-color); }
  .meta-bar .view-all,
  .meta-bar .view-all-cta {
    border-top: none; padding: 0; text-align: right;
    color: #2563eb; text-decoration: underline; font-weight: 600;
  }
  .filter-bar, .chip-row {
    display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem;
    padding: 0.55rem 1rem; border-bottom: 1px solid var(--_border);
  }
  .filter-label {
    font-size: 0.65rem; font-weight: 700; text-transform: uppercase;
    letter-spacing: 0.06em; color: var(--_group-color); margin-right: 0.15rem;
  }
  .chip {
    display: inline-flex; align-items: center; gap: 0.25rem;
    padding: 0.28rem 0.6rem; font-size: 0.75rem; border: 1px solid var(--_border);
    border-radius: 999px; background: var(--_bg); cursor: pointer;
  }
  .chip .badge { font-size: 0.65rem; opacity: 0.7; font-variant-numeric: tabular-nums; }
  .split-body {
    display: grid; grid-template-columns: 220px minmax(0, 1fr);
    align-items: stretch; min-width: 0;
  }
  /* DOM order is canvas \u2192 divider \u2192 rail for mobile stacking; pin
     rail left / products right on desktop via explicit placement. */
  .split-body .rail { grid-column: 1; grid-row: 1; }
  .split-body .canvas { grid-column: 2; grid-row: 1; }
  .split-body .split-divider { display: none; }
  .wrap.wide.split-rail-panel .split-body {
    flex: 1 1 auto; min-height: 0; overflow: hidden;
  }
  .rail {
    border-right: 1px solid var(--_border); background: var(--_row-hover);
    padding: 0.5rem 0.35rem; align-self: stretch;
    min-height: 0; overflow-y: auto; overscroll-behavior: contain;
  }
  .rail .row { padding: 0.4rem 0.55rem; font-size: 0.8125rem; }
  .canvas {
    padding: 0.65rem 0.75rem 0.75rem; min-width: 0;
    min-height: 0; overflow-y: auto; overscroll-behavior: contain;
    container-type: inline-size; container-name: suggest-canvas;
  }
  .product-grid {
    display: grid; gap: 0.35rem; min-width: 0;
  }
  .product-grid.cols-5 { grid-template-columns: repeat(5, minmax(0, 1fr)); }
  .product-grid.cols-6 { grid-template-columns: repeat(6, minmax(0, 1fr)); }
  @container suggest-canvas (max-width: 520px) {
    .product-grid.cols-5 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
    .product-grid.cols-6 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  }
  @container suggest-canvas (max-width: 680px) {
    .product-grid.cols-5 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
    .product-grid.cols-6 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  }
  .product-card {
    display: flex; flex-direction: column; gap: 0.35rem; padding: 0.45rem;
    border-radius: calc(var(--_radius) - 0.125rem); text-decoration: none;
    color: inherit; background: none; border: none; font: inherit; text-align: left;
    cursor: pointer; width: 100%;
  }
  .product-card:hover, .product-card.active { background: var(--_row-hover); }
  .thumb-frame {
    width: 100%; aspect-ratio: 1; display: flex; align-items: center;
    justify-content: center; overflow: hidden;
    border-radius: calc(var(--_radius) - 0.2rem); background: transparent;
  }
  .product-card .thumb-frame .thumb {
    max-width: 100%; max-height: 100%; width: auto; height: auto;
    object-fit: contain; object-position: center; display: block;
    border-radius: 0; background: transparent;
  }
  .product-card .thumb-frame .thumb-empty {
    width: 100%; height: 100%; aspect-ratio: unset;
    border-radius: 0;
    background: var(--_row-hover);
    border: 1px solid var(--_border);
    display: flex; align-items: center; justify-content: center;
    font-size: 1.25rem; font-weight: 700; color: var(--_group-color);
    line-height: 1;
  }
  .product-card .card-meta {
    display: block;
    font-size: 0.65rem;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--_group-color);
    margin-top: 0.15rem;
  }
  .product-card .card-title {
    font-size: 0.75rem; line-height: 1.3; display: -webkit-box;
    -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
  }
  .hero-card .card-title {
    font-size: 0.8125rem; line-height: 1.35; display: -webkit-box;
    -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
  }
  @container suggest-canvas (max-width: 560px) {
    .wrap.wide.product-title-tooltip .product-card .card-title,
    .wrap.wide.product-title-tooltip .hero-card .card-title {
      display: block; -webkit-line-clamp: unset; -webkit-box-orient: unset;
      overflow: visible; white-space: normal; word-break: break-word;
    }
    .wrap.wide.product-title-tooltip .product-grid.cols-5,
    .wrap.wide.product-title-tooltip .product-grid.cols-6 {
      grid-template-columns: repeat(2, minmax(0, 1fr));
    }
  }
  .product-card .card-price {
    font-size: 0.8125rem; font-weight: 600; font-variant-numeric: tabular-nums;
  }
  .product-card .card-price del {
    color: var(--_group-color); font-weight: 400; margin-right: 0.3rem; font-size: 0.85em;
  }
  .command-header {
    display: flex; align-items: center; gap: 0.75rem; padding: 0.75rem 1rem;
    background: linear-gradient(135deg, #1e3a5f 0%, #2563eb 100%); color: #fff;
  }
  .command-header .query-display { font-size: 1rem; font-weight: 600; flex: 1; }
  .command-header .result-pill {
    padding: 0.25rem 0.6rem; background: rgba(255,255,255,0.15);
    border-radius: 999px; font-size: 0.75rem;
  }
  .command-header .view-all-link {
    color: #fff; font-size: 0.75rem; font-weight: 600; text-decoration: none;
    padding: 0.3rem 0.65rem; background: rgba(255,255,255,0.2); border-radius: 999px;
  }
  .hero-row {
    display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.65rem;
    padding: 0.65rem 0; border-bottom: 1px solid var(--_border); margin-bottom: 0.5rem;
  }
  .hero-card {
    display: grid; grid-template-columns: 90px 1fr; gap: 0.65rem;
    padding: 0.65rem; border: 1px solid var(--_border);
    border-radius: calc(var(--_radius) - 0.125rem); background: var(--_row-hover);
    text-decoration: none; color: inherit; cursor: pointer; width: 100%;
    font: inherit; text-align: left;
  }
  .hero-card:hover, .hero-card.active { border-color: #2563eb; }
  .hero-card .thumb, .hero-card .thumb-empty {
    width: 90px; height: 90px; object-fit: contain;
    border-radius: calc(var(--_radius) - 0.2rem); background: var(--_row-active);
  }
  .hero-badge {
    display: inline-block; width: fit-content; padding: 0.1rem 0.4rem;
    font-size: 0.6rem; font-weight: 700; text-transform: uppercase;
    background: #2563eb; color: #fff; border-radius: 3px; margin-bottom: 0.2rem;
  }
  .brand-footer {
    display: flex; align-items: center; justify-content: flex-end; gap: 0.35rem;
    padding: 0.45rem 1rem; font-size: 0.65rem; color: var(--_group-color);
    border-top: 1px solid var(--_border); background: var(--_row-hover);
    text-decoration: none;
  }
  .brand-footer:hover { color: var(--_color); }
  .brand-by { white-space: nowrap; }
  .brand-logo { height: 16px; width: auto; display: block; }
  .did-you-mean-bar {
    padding: 0.5rem 1rem; font-size: 0.8125rem;
    background: #eff6ff; border-bottom: 1px solid #bfdbfe;
  }
  .did-you-mean-bar .swap {
    border: none; background: none; font: inherit; font-weight: 600;
    color: #2563eb; text-decoration: underline; cursor: pointer; padding: 0;
  }
  .split-divider { display: none; }
  .split-divider-icon {
    width: 2.75rem; height: 1rem; color: var(--_group-color);
    pointer-events: none; opacity: 0.9;
  }
  .products-pending {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 6rem;
    padding: 1rem;
    color: var(--_group-color);
    font-size: 0.875rem;
    text-align: center;
  }
  @media (max-width: 900px) {
    /* Stack products above suggestions so product hits stay above the
       mobile keyboard; desktop split-rail keeps rail left via grid. */
    .split-body {
      display: flex; flex-direction: column;
      grid-template-columns: unset; grid-template-rows: unset;
    }
    .split-body .rail,
    .split-body .canvas {
      grid-column: unset; grid-row: unset;
    }
    .split-body .canvas {
      flex: 1 1 auto;
      min-height: 4.5rem;
      min-width: 0;
      overflow-y: auto;
      overscroll-behavior: contain;
    }
    .split-body .rail {
      flex: 0 1 auto;
      border-right: none;
      border-top: 1px solid var(--_border);
      border-bottom: none;
      overflow-y: auto;
      overscroll-behavior: contain;
    }
    .wrap.wide.split-rail-panel.split-rail-mobile {
      height: min(var(--_max-height), 70vh);
      min-height: 16rem;
    }
    /* Static mobile stack (no drag handle): cap suggestion rail height. */
    .split-body:not(.split-body--mobile-resize) .rail {
      max-height: 7.5rem;
    }
    .split-body--mobile-resize .rail {
      flex: var(--split-rail-top-grow, 0.28) 1 0;
      min-height: 3.25rem;
      max-height: none;
    }
    .split-body--mobile-resize .split-divider {
      display: flex; align-items: center; justify-content: center;
      flex: 0 0 auto; height: 1.75rem; margin: 0; padding: 0;
      border-top: 1px solid var(--_border);
      border-bottom: 1px solid var(--_border);
      background: linear-gradient(180deg, var(--_row-hover) 0%, var(--_row-active) 100%);
      cursor: ns-resize; touch-action: none;
      user-select: none; -webkit-user-select: none;
      z-index: 2;
    }
    .split-body--mobile-resize .split-divider:focus-visible {
      outline: 2px solid #2563eb; outline-offset: -2px;
    }
    .split-body--mobile-resize .split-divider.is-dragging {
      background: var(--_row-active);
    }
    .split-body--mobile-resize .canvas {
      flex: var(--split-rail-bottom-grow, 0.72) 1 0;
    }
    .product-grid.cols-5, .product-grid.cols-6 { grid-template-columns: repeat(3, 1fr); }
    .hero-row { grid-template-columns: 1fr; }
    .wrap.wide.product-title-tooltip .product-card .card-title,
    .wrap.wide.product-title-tooltip .hero-card .card-title {
      display: block; -webkit-line-clamp: unset; -webkit-box-orient: unset;
      overflow: visible; white-space: normal; word-break: break-word;
    }
    .wrap.wide.product-title-tooltip .product-grid.cols-5,
    .wrap.wide.product-title-tooltip .product-grid.cols-6 {
      grid-template-columns: repeat(2, minmax(0, 1fr));
    }
    .wrap.wide.product-title-tooltip .product-card {
      align-items: stretch;
    }
  }
`;
var SPLIT_MOBILE_MIN_FRAC = 0.15;
var SPLIT_MOBILE_MAX_FRAC = 0.85;
var SPLIT_MOBILE_DEFAULT_FRAC = 0.28;
var SPLIT_MOBILE_STORAGE_KEY = "seekmodo:split-rail-mobile-ratio-v3";
var SPLIT_MOBILE_MQ = "(max-width: 900px)";
var SPLIT_MOBILE_DEFAULT_PRODUCTS_FRAC = 1 - SPLIT_MOBILE_DEFAULT_FRAC;
function renderSplitDivider() {
  return h("div", {
    class: "split-divider",
    part: "split-divider",
    html: '<svg class="split-divider-icon" viewBox="0 0 36 12" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg"><rect x="6" y="1" width="24" height="2" rx="1" fill="currentColor"/><rect x="6" y="5" width="24" height="2" rx="1" fill="currentColor"/><rect x="6" y="9" width="24" height="2" rx="1" fill="currentColor"/></svg>'
  });
}
function bindSplitMobileResize(splitBody) {
  const divider = splitBody.querySelector(".split-divider");
  if (!divider) return () => {
  };
  const mq = window.matchMedia(SPLIT_MOBILE_MQ);
  let productsFrac = SPLIT_MOBILE_DEFAULT_PRODUCTS_FRAC;
  try {
    const stored = sessionStorage.getItem(SPLIT_MOBILE_STORAGE_KEY);
    if (stored) {
      const n = parseFloat(stored);
      if (n >= SPLIT_MOBILE_MIN_FRAC && n <= SPLIT_MOBILE_MAX_FRAC) productsFrac = n;
    }
  } catch {
  }
  const applyRatio = (nextProductsFrac) => {
    productsFrac = Math.min(
      SPLIT_MOBILE_MAX_FRAC,
      Math.max(SPLIT_MOBILE_MIN_FRAC, nextProductsFrac)
    );
    splitBody.style.setProperty("--split-rail-bottom-grow", String(productsFrac));
    splitBody.style.setProperty("--split-rail-top-grow", String(1 - productsFrac));
  };
  const clearRatio = () => {
    splitBody.style.removeProperty("--split-rail-top-grow");
    splitBody.style.removeProperty("--split-rail-bottom-grow");
  };
  const syncForViewport = () => {
    if (mq.matches) applyRatio(productsFrac);
    else clearRatio();
  };
  syncForViewport();
  let dragging = false;
  let startY = 0;
  let startProductsFrac = productsFrac;
  const onPointerDown = (e) => {
    if (!mq.matches || e.button !== 0) return;
    dragging = true;
    startY = e.clientY;
    startProductsFrac = productsFrac;
    divider.classList.add("is-dragging");
    divider.setPointerCapture(e.pointerId);
    e.preventDefault();
  };
  const onPointerMove = (e) => {
    if (!dragging) return;
    const rect = splitBody.getBoundingClientRect();
    const available = rect.height - divider.offsetHeight;
    if (available <= 0) return;
    applyRatio(startProductsFrac + (e.clientY - startY) / available);
  };
  const endDrag = (e) => {
    if (!dragging) return;
    dragging = false;
    divider.classList.remove("is-dragging");
    try {
      divider.releasePointerCapture(e.pointerId);
    } catch {
    }
    try {
      sessionStorage.setItem(SPLIT_MOBILE_STORAGE_KEY, String(productsFrac));
    } catch {
    }
  };
  divider.setAttribute("role", "separator");
  divider.setAttribute("aria-orientation", "horizontal");
  divider.setAttribute("aria-label", "Resize product and suggestion panels");
  divider.setAttribute("tabindex", "0");
  divider.addEventListener("pointerdown", onPointerDown);
  divider.addEventListener("pointermove", onPointerMove);
  divider.addEventListener("pointerup", endDrag);
  divider.addEventListener("pointercancel", endDrag);
  mq.addEventListener("change", syncForViewport);
  return () => {
    divider.removeEventListener("pointerdown", onPointerDown);
    divider.removeEventListener("pointermove", onPointerMove);
    divider.removeEventListener("pointerup", endDrag);
    divider.removeEventListener("pointercancel", endDrag);
    mq.removeEventListener("change", syncForViewport);
    divider.classList.remove("is-dragging");
    clearRatio();
  };
}
var BRAND_LOGO_FALLBACK = "https://seekmodo.com/email-assets/seekmodo-lockup.png";
function paintThumbSrc(ctx, src) {
  return ctx.resolveThumbSrc ? ctx.resolveThumbSrc(src) : src;
}
function productDisplayName(p) {
  return String(p.name ?? p.title ?? "").trim();
}
function applyProductTitleTooltip(el, ctx, name) {
  if (ctx.productTitleTooltip && name) {
    el.setAttribute("title", name);
  }
}
function productImageAlt(ctx, name) {
  return ctx.productTitleTooltip && name ? name : "";
}
function pushProductRow(ctx, p, idx) {
  const desc = {
    block: "products",
    data: p,
    value: String(p.name ?? p.title ?? ""),
    id: p.id !== void 0 ? String(p.id) : void 0
  };
  const pos = ctx.rows.length;
  ctx.rows.push(desc);
  return pos;
}
function productGridThumb(p, ctx, name) {
  const frame = h("div", { class: "thumb-frame", part: "thumb-frame" });
  const { postType } = productContentType(p);
  if (postType) {
    frame.setAttribute("data-post-type", postType);
  }
  const src = p.image_url ?? p.image;
  if (src) {
    frame.append(h("img", {
      class: "thumb",
      part: "thumb",
      attrs: {
        src: paintThumbSrc(ctx, src),
        "data-src": src,
        alt: productImageAlt(ctx, name),
        loading: "eager",
        decoding: "async"
      }
    }));
  } else {
    const empty = h("div", {
      class: "thumb-empty",
      part: "thumb thumb--empty",
      text: placeholderGlyph(postType)
    });
    if (postType) {
      empty.setAttribute("data-content-type", postType);
    }
    frame.append(empty);
  }
  return frame;
}
function placeholderGlyph(postType) {
  if (postType === "page") return "P";
  if (postType === "post") return "A";
  return "\xB7";
}
function productThumb(p, ctx, name) {
  const { postType } = productContentType(p);
  const src = p.image_url ?? p.image;
  if (src) {
    return h("img", {
      class: "thumb",
      part: "thumb",
      attrs: {
        src: paintThumbSrc(ctx, src),
        "data-src": src,
        alt: productImageAlt(ctx, name),
        loading: "eager",
        decoding: "async"
      }
    });
  }
  const empty = h("div", {
    class: "thumb-empty",
    part: "thumb thumb--empty",
    text: placeholderGlyph(postType)
  });
  if (postType) {
    empty.setAttribute("data-content-type", postType);
  }
  return empty;
}
function productPriceEl(p, ctx, cls = "card-price") {
  if (p.price === void 0 || p.price === null) return null;
  const el = h("div", { class: cls, part: "price" });
  if (p.on_sale && typeof p.sale_price === "number") {
    el.append(h("del", { text: ctx.formatPrice(p.price, p.currency) }));
    el.append(document.createTextNode(ctx.formatPrice(p.sale_price, p.currency)));
  } else {
    el.append(document.createTextNode(ctx.formatPrice(p.price, p.currency)));
  }
  return el;
}
function bindProductCard(card, ctx, pos) {
  card.classList.add("row");
  card.setAttribute("data-seekmodo-surface", "suggest");
  card.setAttribute("data-seekmodo-block", "products");
  card.setAttribute("data-seekmodo-pos", String(pos));
  const desc = ctx.rows[pos];
  if (desc?.id) card.setAttribute("data-seekmodo-id", desc.id);
  card.addEventListener("click", () => ctx.onRowClick(pos));
}
function renderProductCard(ctx, p, _idx, compact = false) {
  const pos = pushProductRow(ctx, p, _idx);
  const name = productDisplayName(p);
  const { postType, label } = productContentType(p);
  const card = h("button", {
    class: "product-card",
    part: "row",
    type: "button"
  });
  if (postType) {
    card.setAttribute("data-post-type", postType);
  }
  card.append(productGridThumb(p, ctx, name));
  const title = h("span", {
    class: "card-title",
    part: "name",
    text: name
  });
  applyProductTitleTooltip(title, ctx, name);
  card.append(title);
  if (label) {
    card.append(h("span", { class: "card-meta", part: "card-meta", text: label }));
  }
  const price = productPriceEl(p, ctx, compact ? "card-price" : "card-price");
  if (price) card.append(price);
  bindProductCard(card, ctx, pos);
  return card;
}
function renderHeroCard(ctx, p, idx, badge) {
  const pos = pushProductRow(ctx, p, idx);
  const name = productDisplayName(p);
  const card = h("button", {
    class: "hero-card",
    part: "row",
    type: "button"
  });
  if (badge) card.append(h("span", { class: "hero-badge", text: badge }));
  card.append(productThumb(p, ctx, name));
  const info = h("div", { class: "hero-info" });
  const title = h("span", {
    class: "card-title",
    part: "name",
    text: name
  });
  applyProductTitleTooltip(title, ctx, name);
  info.append(title);
  const price = productPriceEl(p, ctx);
  if (price) info.append(price);
  card.append(info);
  bindProductCard(card, ctx, pos);
  return card;
}
function renderMetaBar(ctx) {
  const total = ctx.res.meta?.total ?? 0;
  const bar = h("div", { class: "meta-bar", part: "meta-bar" });
  const left = h("div");
  left.append(h("span", { class: "count", text: `${total} results for ` }));
  left.append(h("span", { class: "query", text: `"${ctx.lastQuery}"` }));
  bar.append(left);
  const a = h("a", {
    class: "view-all view-all-cta",
    part: "view-all",
    attrs: { href: ctx.viewAllHref },
    text: ctx.label("view_all").replace("{total}", String(total))
  });
  a.addEventListener("click", (e) => {
    e.preventDefault();
    ctx.onViewAll();
  });
  bar.append(a);
  return bar;
}
function renderDidYouMean(ctx) {
  const dym = ctx.res.did_you_mean;
  if (!dym) return null;
  const bar = h("div", { class: "did-you-mean-bar", part: "did-you-mean" });
  bar.append(document.createTextNode(`Showing results for "${ctx.lastQuery}". Search instead for `));
  const btn = h("button", { class: "swap", type: "button", text: dym });
  const pos = ctx.rows.length;
  ctx.rows.push({
    block: "did_you_mean",
    data: { value: dym },
    value: dym
  });
  btn.addEventListener("click", () => ctx.onRowClick(pos));
  bar.append(btn);
  bar.append(document.createTextNode("?"));
  return bar;
}
function categoryChips(ctx, categories) {
  const row = h("div", { class: "chip-row filter-bar", part: "filter-bar" });
  row.append(h("span", { class: "filter-label", text: "Category" }));
  categories.forEach((c, i) => {
    const chip = h("button", {
      class: `chip${i === 0 ? " active" : ""}`,
      type: "button",
      text: `${c.name}${typeof c.count === "number" ? ` ${c.count}` : ""}`
    });
    const pos = ctx.rows.length;
    ctx.rows.push({
      block: "categories",
      data: c,
      value: String(c.name ?? "")
    });
    chip.addEventListener("click", () => ctx.onRowClick(pos));
    row.append(chip);
  });
  return row;
}
function keywordChips(ctx, keywords, label = "Try") {
  const row = h("div", { class: "chip-row", part: "filter-bar" });
  row.append(h("span", { class: "filter-label", text: label }));
  keywords.forEach((k) => {
    const chip = h("button", { class: "chip", type: "button", text: k.keyword });
    const pos = ctx.rows.length;
    ctx.rows.push({
      block: "keywords",
      data: k,
      value: k.keyword
    });
    chip.addEventListener("click", () => ctx.onRowClick(pos));
    row.append(chip);
  });
  return row;
}
function renderRailRow(ctx, block, item, label, badge) {
  const pos = ctx.rows.length;
  ctx.rows.push({ block, data: item, value: label });
  const row = h("button", { class: "row", part: "row", type: "button" });
  row.append(h("div", { class: "name", part: "name", text: label }));
  if (badge) row.append(h("span", { class: "badge", part: "badge", text: badge }));
  row.setAttribute("data-seekmodo-surface", "suggest");
  row.setAttribute("data-seekmodo-block", block);
  row.setAttribute("data-seekmodo-pos", String(pos));
  row.addEventListener("click", () => ctx.onRowClick(pos));
  return row;
}
function renderRailSection(title, children) {
  if (children.length === 0) return null;
  const sec = h("div", { class: "rail-section" });
  sec.append(h("div", {
    class: "group-title",
    part: "group-title",
    text: title
  }));
  children.forEach((c) => sec.append(c));
  return sec;
}
function renderBrandFooter(ctx) {
  if (!ctx.showBranding) return null;
  const a = h("a", {
    class: "brand-footer",
    part: "brand-footer",
    attrs: {
      href: ctx.brandUrl,
      target: "_blank",
      rel: "noopener noreferrer"
    }
  });
  a.append(h("span", { class: "brand-by", text: "Powered by " }));
  a.append(h("img", {
    class: "brand-logo",
    part: "brand-logo",
    attrs: {
      src: ctx.brandLogoUrl || BRAND_LOGO_FALLBACK,
      alt: "Seekmodo",
      height: "16"
    }
  }));
  return a;
}
function productGrid(ctx, products, cols) {
  const grid = h("div", {
    class: `product-grid cols-${cols}`,
    part: "product-grid"
  });
  products.forEach((p, i) => grid.append(renderProductCard(ctx, p, i, true)));
  return grid;
}
function renderWideLayout(layout, ctx) {
  const wrapClasses = ["wrap", "wide"];
  if (layout === "split-rail") wrapClasses.push("split-rail-panel");
  if (ctx.splitMobileResize) wrapClasses.push("split-rail-mobile");
  if (ctx.productTitleTooltip) wrapClasses.push("product-title-tooltip");
  const wrap = h("div", { class: wrapClasses.join(" "), part: "wrap" });
  wrap.append(h("slot", { attrs: { name: "header" } }));
  const products = (ctx.res.products ?? []).slice(
    0,
    wideLayoutProductLimit(layout, ctx.limit)
  );
  const keywords = (ctx.res.keywords ?? []).slice(0, ctx.limit);
  const categories = (ctx.res.categories ?? []).slice(0, ctx.limit);
  const redirects = (ctx.res.redirects ?? []).slice(0, ctx.limit);
  const recent = (ctx.res.recent ?? []).slice(0, 5);
  const trending = (ctx.res.trending ?? []).slice(0, 5);
  if (layout === "split-rail") {
    if (!ctx.productsPending || (ctx.res.meta?.total ?? 0) > 0) {
      wrap.append(renderMetaBar(ctx));
    }
    const bodyClass = ctx.splitMobileResize ? "split-body split-body--mobile-resize" : "split-body";
    const body = h("div", { class: bodyClass });
    const rail = h("aside", { class: "rail", part: "rail" });
    const kwRows = keywords.map(
      (k) => renderRailRow(ctx, "keywords", k, k.keyword)
    );
    const secKw = renderRailSection(ctx.label("keywords"), kwRows);
    if (secKw) rail.append(secKw);
    const redRows = redirects.map(
      (r) => renderRailRow(
        ctx,
        "redirects",
        r,
        String(r.label || r.matched_term || r.target_url)
      )
    );
    const secRed = renderRailSection(ctx.label("redirects"), redRows);
    if (secRed) rail.append(secRed);
    const catRows = categories.map(
      (c) => renderRailRow(
        ctx,
        "categories",
        c,
        String(c.name),
        typeof c.count === "number" ? String(c.count) : void 0
      )
    );
    const secCat = renderRailSection(ctx.label("categories"), catRows);
    if (secCat) rail.append(secCat);
    const recRows = recent.map(
      (r) => renderRailRow(ctx, "recent", r, r.keyword)
    );
    const secRec = renderRailSection(ctx.label("recent"), recRows);
    if (secRec) rail.append(secRec);
    const canvas = h("div", { class: "canvas" });
    if (ctx.productsPending && products.length === 0) {
      canvas.append(h("div", {
        class: "products-pending",
        part: "products-pending",
        text: "Matching products appear when you pause typing\u2026"
      }));
    } else {
      canvas.append(productGrid(ctx, products, 5));
    }
    body.append(canvas);
    if (ctx.splitMobileResize) body.append(renderSplitDivider());
    body.append(rail);
    wrap.append(body);
  } else if (layout === "cinema-grid") {
    const dym = renderDidYouMean(ctx);
    if (dym) wrap.append(dym);
    wrap.append(renderMetaBar(ctx));
    if (categories.length) wrap.append(categoryChips(ctx, categories));
    if (keywords.length) wrap.append(keywordChips(ctx, keywords));
    const gridWrap = h("div", { class: "canvas" });
    gridWrap.append(productGrid(ctx, products, 6));
    wrap.append(gridWrap);
  } else if (layout === "command-bar") {
    const total = ctx.res.meta?.total ?? 0;
    const hdr = h("div", { class: "command-header", part: "meta-bar" });
    hdr.append(h("div", { class: "query-display", text: `"${ctx.lastQuery}"` }));
    hdr.append(h("span", { class: "result-pill", text: `${total} products` }));
    const va = h("a", {
      class: "view-all-link",
      part: "view-all",
      attrs: { href: ctx.viewAllHref },
      text: "View all \u2192"
    });
    va.addEventListener("click", (e) => {
      e.preventDefault();
      ctx.onViewAll();
    });
    hdr.append(va);
    wrap.append(hdr);
    if (ctx.res.did_you_mean) {
      const pills = h("div", { class: "chip-row" });
      pills.append(h("span", { class: "filter-label", text: "Did you mean" }));
      const chip = h("button", { class: "chip", type: "button", text: ctx.res.did_you_mean });
      const pos = ctx.rows.length;
      ctx.rows.push({
        block: "did_you_mean",
        data: { value: ctx.res.did_you_mean },
        value: ctx.res.did_you_mean
      });
      chip.addEventListener("click", () => ctx.onRowClick(pos));
      pills.append(chip);
      wrap.append(pills);
    }
    if (keywords.length) wrap.append(keywordChips(ctx, keywords, "Related"));
    if (categories.length) wrap.append(categoryChips(ctx, categories));
    const gridWrap = h("div", { class: "canvas" });
    gridWrap.append(productGrid(ctx, products, 5));
    wrap.append(gridWrap);
  } else if (layout === "magazine") {
    wrap.append(renderMetaBar(ctx));
    if (categories.length) wrap.append(categoryChips(ctx, categories));
    const body = h("div", { class: "canvas" });
    const heroes = products.slice(0, 3);
    const rest = products.slice(3);
    if (heroes.length) {
      body.append(h("div", {
        class: "group-title",
        part: "group-title",
        text: "Best matches"
      }));
      const heroRow = h("div", { class: "hero-row", part: "hero-row" });
      heroes.forEach((p, i) => {
        heroRow.append(renderHeroCard(ctx, p, i, i === 0 ? "Top match" : void 0));
      });
      body.append(heroRow);
    }
    if (rest.length) {
      body.append(h("div", {
        class: "group-title",
        part: "group-title",
        text: "More results"
      }));
      body.append(productGrid(ctx, rest, 6));
    } else if (!heroes.length) {
      body.append(productGrid(ctx, products, 6));
    }
    wrap.append(body);
  }
  const brand = renderBrandFooter(ctx);
  if (brand) wrap.append(brand);
  wrap.append(h("slot", { attrs: { name: "footer" } }));
  return wrap;
}
function isWideLayout(layout) {
  return layout !== "classic" && layout !== "";
}

// src/components/suggest.ts
var DEFAULT_BLOCKS = [
  "recent",
  "did_you_mean",
  "keywords",
  "trending",
  "products",
  "categories"
];
var DEFAULT_LABELS = {
  recent: "Recently searched",
  trending: "Trending",
  keywords: "Suggestions",
  products: "Products",
  categories: "Categories",
  did_you_mean: "Did you mean",
  view_all: "View all {total} results",
  empty: "No matches yet \u2014 keep typing.",
  cors_blocked: DEFAULT_CORS_BLOCKED_MESSAGE
};
var STYLES4 = `
  :host {
    /* When anchor-mode is "auto" (default) applyAnchor() flips
       these to position:fixed via inline style so the dropdown
       overlays the input regardless of the host's DOM position.
       When anchor-mode is "none" the shadow stylesheet wins and
       the dropdown renders in-DOM at its host's position. */
    display: block;
    position: relative;
    font-family: inherit;
    --_bg: var(--seekmodo-suggest-bg, #ffffff);
    --_color: var(--seekmodo-suggest-color, #18181b);
    --_border: var(--seekmodo-suggest-border, #d4d4d8);
    --_radius: var(--seekmodo-suggest-radius, 0.5rem);
    --_shadow: var(--seekmodo-suggest-shadow, 0 10px 24px rgba(0, 0, 0, 0.08));
    --_row-padding: var(--seekmodo-suggest-row-padding, 0.5rem 0.75rem);
    --_row-hover: var(--seekmodo-suggest-row-hover, #f4f4f5);
    --_row-active: var(--seekmodo-suggest-row-active, #e4e4e7);
    --_thumb: var(--seekmodo-suggest-thumb-size, 36px);
    --_group-color: var(--seekmodo-suggest-group-color, #71717a);
    --_group-size: var(--seekmodo-suggest-group-size, 0.7rem);
    --_max-height: var(--seekmodo-suggest-max-height, 70vh);
    --_width: var(--seekmodo-suggest-width, 100%);
  }
  .wrap {
    background: var(--_bg);
    color: var(--_color);
    border: 1px solid var(--_border);
    border-radius: var(--_radius);
    box-shadow: var(--_shadow);
    padding: 0.25rem 0;
    max-height: var(--_max-height);
    width: var(--_width);
    overflow-y: auto;
    overscroll-behavior: contain;
  }
  .hidden { display: none; }
  .group-title {
    padding: 0.4rem 0.75rem 0.25rem;
    font-size: var(--_group-size);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--_group-color);
    font-weight: 600;
  }
  .row {
    width: 100%;
    text-align: left;
    background: none;
    border: none;
    padding: var(--_row-padding);
    cursor: pointer;
    font: inherit;
    color: inherit;
    display: flex;
    gap: 0.6rem;
    align-items: center;
  }
  .row:hover, .row.active {
    background: var(--_row-hover);
    outline: none;
  }
  .row:active {
    background: var(--_row-active);
  }
  .thumb {
    width: var(--_thumb);
    height: var(--_thumb);
    object-fit: contain;
    border-radius: calc(var(--_radius) - 0.125rem);
    flex-shrink: 0;
    background: var(--_row-hover);
  }
  .thumb.thumb-empty {
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 0.85rem;
    font-weight: 700;
    color: var(--_group-color);
    border: 1px solid var(--_border);
  }
  .name { flex: 1; min-width: 0; }
  .name-title {
    display: block;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .name-meta {
    display: block;
    font-size: 0.8em;
    color: var(--_group-color);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .price {
    margin-left: auto;
    font-variant-numeric: tabular-nums;
  }
  .price del { color: var(--_group-color); margin-right: 0.4rem; font-weight: normal; }
  .view-all {
    text-align: center;
    font-weight: 600;
    padding: 0.55rem 0.75rem;
  }
  .wrap:not(.wide) .view-all {
    border-top: 1px solid var(--_border);
  }
  .empty {
    padding: 0.75rem;
    color: var(--_group-color);
    text-align: center;
    font-size: 0.9em;
  }
  .cors-notice {
    padding: 0.75rem;
    font-size: 0.875rem;
    line-height: 1.45;
    color: #5c4a00;
    background: #fff8e6;
  }
  .did-you-mean {
    padding: 0.45rem 0.75rem;
    font-size: 0.9em;
  }
  .did-you-mean .swap {
    border: none;
    background: none;
    padding: 0;
    color: var(--_color);
    font: inherit;
    font-weight: 600;
    text-decoration: underline;
    cursor: pointer;
  }
  .badge {
    display: inline-block;
    font-size: 0.7em;
    padding: 0.05em 0.4em;
    border-radius: 999px;
    background: var(--_row-active);
    color: var(--_color);
    margin-left: 0.4rem;
    font-variant-numeric: tabular-nums;
  }
  /* Skeleton loader \u2014 three placeholder rows while in-flight on a
     cold cache. Masks the network latency on the first keystroke. */
  .skeleton .row {
    pointer-events: none;
  }
  .skeleton .thumb,
  .skeleton .name-title,
  .skeleton .name-meta {
    background: linear-gradient(90deg,
      var(--_row-hover) 0%,
      var(--_row-active) 50%,
      var(--_row-hover) 100%);
    background-size: 200% 100%;
    animation: seekmodo-suggest-shimmer 1.2s ease-in-out infinite;
    border-radius: 0.25rem;
    color: transparent;
  }
  .skeleton .name-title { height: 0.95em; width: 70%; }
  .skeleton .name-meta { height: 0.75em; width: 40%; margin-top: 0.3em; }
  @keyframes seekmodo-suggest-shimmer {
    0%   { background-position:  200% 0; }
    100% { background-position: -200% 0; }
  }
  @media (prefers-reduced-motion: reduce) {
    .skeleton .thumb,
    .skeleton .name-title,
    .skeleton .name-meta { animation: none; }
  }
  @media (max-width: 900px) {
    .wrap.product-title-tooltip .name-title {
      white-space: normal;
      overflow: visible;
      text-overflow: unset;
      word-break: break-word;
    }
    .wrap.product-title-tooltip .row {
      align-items: flex-start;
    }
  }
  ${WIDE_LAYOUT_STYLES}
`;
var Lru = class {
  constructor(cap) {
    this.cap = cap;
  }
  cap;
  map = /* @__PURE__ */ new Map();
  get(k) {
    const v = this.map.get(k);
    if (v === void 0) return void 0;
    this.map.delete(k);
    this.map.set(k, v);
    return v;
  }
  set(k, v) {
    if (this.map.has(k)) this.map.delete(k);
    this.map.set(k, v);
    while (this.map.size > this.cap) {
      const oldest = this.map.keys().next().value;
      if (oldest === void 0) break;
      this.map.delete(oldest);
    }
  }
  clear() {
    this.map.clear();
  }
};
var SeekmodoSuggest = class extends SeekmodoElement {
  static get observedAttributes() {
    return [
      "source",
      "input",
      "blocks",
      "min-length",
      "debounce-ms",
      "product-debounce-ms",
      "limit",
      "cache-size",
      "view-all-href",
      "lang",
      "anchor",
      "anchor-offset",
      "anchor-min-width",
      "layout",
      "split-mobile-resize",
      "product-title-tooltip",
      "typeahead-fallback-url",
      "serp-passthrough",
      "img-ver",
      "vehicle-id",
      "vehicle-filter",
      "currency",
      "show-branding",
      "brand-url",
      "brand-logo-url",
      "suppress-legacy"
    ];
  }
  current = null;
  loading = false;
  corsBlocked = false;
  lastQuery = "";
  subscribed = null;
  inputEl = null;
  debounced = null;
  debouncedAt = 0;
  debouncedPrefix = null;
  debouncedProducts = null;
  prefixDebouncedAt = 0;
  productDebouncedAt = 0;
  /** True while the query changed but the product idle fetch has not settled. */
  productsPending = false;
  /**
   * Query waiting for `seekmodo-suggest:rendered` after the next paint.
   * Set before scheduleRender so connectors hydrate against real DOM.
   */
  pendingRenderedQ = null;
  fetchToken = 0;
  prefixFetchToken = 0;
  productFetchToken = 0;
  inflight = null;
  cache = new Lru(32);
  rows = [];
  active = -1;
  bodyClickHandler = null;
  keyHandler = null;
  regionChangeHandler = null;
  /**
   * Anchor-mode listeners. We attach them at the window level so the
   * dropdown re-positions on every layout-affecting event without
   * forcing connectors to wrap the input in a positioning container.
   * Held on `this` so disconnectedCallback can detach cleanly.
   */
  anchorScrollHandler = null;
  anchorResizeHandler = null;
  anchorFocusHandler = null;
  anchorResizeRaf = null;
  lastAnchorKey = "";
  /**
   * `true` once we've ever flipped the host to `position:fixed` so
   * disconnectedCallback can restore the inline style block cleanly
   * rather than leaving stale top/left/width values behind on
   * re-attach (matters for Hydrogen / React route changes that
   * remove + re-mount the element).
   */
  anchorApplied = false;
  /**
   * Per-connector legacy-widget suppression bookkeeping. Tracks
   * jQuery-UI menus we've tagged so we can untag on
   * disconnectedCallback (idempotent — re-attach re-suppresses).
   */
  suppressedLegacyEls = /* @__PURE__ */ new WeakSet();
  /** Re-run legacy suppression after late jQuery-UI inits. */
  legacySuppressionRetryHandler = null;
  /** Teardown for split-rail mobile resize drag wiring. */
  splitResizeCleanup = null;
  connectedCallback() {
    this.resyncDebounce();
    this.resyncCache();
    this.subscribe();
    this.bindGlobalListeners();
    this.bindAnchorListeners();
    this.applyAnchor();
    this.applyLegacySuppression();
    this.scheduleLegacySuppressionRetries();
    this.scheduleRender();
  }
  disconnectedCallback() {
    this.unscheduleLegacySuppressionRetries();
    this.unbindSplitMobileResize();
    this.unsubscribe();
    this.unbindGlobalListeners();
    this.unbindAnchorListeners();
    this.restoreLegacyOnDetach();
    this.inflight?.abort();
    super.disconnectedCallback();
  }
  attributeChangedCallback(name) {
    if (name === "source" || name === "input") {
      this.unsubscribe();
      this.subscribe();
      this.applyAnchor();
      this.applyLegacySuppression();
    } else if (name === "debounce-ms" || name === "product-debounce-ms") {
      this.resyncDebounce();
    } else if (name === "cache-size") {
      this.resyncCache();
    } else if (name === "anchor" || name === "anchor-offset" || name === "anchor-min-width" || name === "layout" || name === "split-mobile-resize") {
      this.applyAnchor();
    } else if (name === "suppress-legacy") {
      this.restoreLegacyOnDetach();
      this.applyLegacySuppression();
    } else if (name === "vehicle-id" || name === "vehicle-filter" || name === "serp-passthrough" || name === "currency") {
      this.cache.clear();
      this.current = null;
      if (this.lastQuery.trim().length >= (parseInt(this.getAttribute("min-length") ?? "2", 10) || 2)) {
        void this.fetch(this.lastQuery);
      } else {
        this.scheduleRender();
      }
    } else {
      this.scheduleRender();
    }
  }
  resyncDebounce() {
    const ms = parseInt(this.getAttribute("debounce-ms") ?? "150", 10) || 150;
    if (this.debouncedAt !== ms || !this.debounced) {
      this.debouncedAt = ms;
      this.debounced = debounce((q) => void this.fetch(q), ms);
    }
    if (!this.twoPhaseEnabled()) {
      this.debouncedPrefix = null;
      this.debouncedProducts = null;
      return;
    }
    const productMs = this.productDebounceMs();
    if (this.prefixDebouncedAt !== ms || !this.debouncedPrefix) {
      this.prefixDebouncedAt = ms;
      this.debouncedPrefix = debounce(
        (q) => void this.fetchPrefix(q),
        ms
      );
    }
    if (this.productDebouncedAt !== productMs || !this.debouncedProducts) {
      this.productDebouncedAt = productMs;
      this.debouncedProducts = debounce(
        (q) => void this.fetchProducts(q),
        productMs
      );
    }
  }
  debounceMs() {
    return parseInt(this.getAttribute("debounce-ms") ?? "150", 10) || 150;
  }
  productDebounceMs() {
    return parseInt(this.getAttribute("product-debounce-ms") ?? "0", 10) || 0;
  }
  /** Two-phase typeahead when product idle debounce exceeds prefix debounce. */
  twoPhaseEnabled() {
    const productMs = this.productDebounceMs();
    return productMs > this.debounceMs();
  }
  resyncCache() {
    const cap = Math.max(
      1,
      parseInt(this.getAttribute("cache-size") ?? "32", 10) || 32
    );
    const next = new Lru(cap);
    this.cache = next;
  }
  /**
   * Subscribe to the source. Supports two modes:
   *
   *   - `source` references a sibling `<seekmodo-search-box>` whose
   *     `seekmodo:input` event we already understand.
   *   - `input`  references a plain `<input>` we attach an `input`
   *     listener to. This is the path Phase-E connector swaps use
   *     when they want one drop-in widget without also enqueueing
   *     a Seekmodo search box.
   */
  subscribe() {
    const sourceId = this.getAttribute("source");
    if (sourceId) {
      const target = document.getElementById(sourceId);
      if (target) {
        this.subscribed = target;
        target.addEventListener("seekmodo:input", this.onSeekmodoInput);
        return;
      }
    }
    const inputId = this.getAttribute("input");
    if (inputId) {
      const el = document.getElementById(inputId);
      if (el instanceof HTMLInputElement) {
        this.inputEl = el;
        el.addEventListener("input", this.onPlainInput);
        el.addEventListener("focus", this.onPlainFocus);
        el.addEventListener("blur", this.onPlainBlur);
      }
    }
  }
  unsubscribe() {
    if (this.subscribed) {
      this.subscribed.removeEventListener(
        "seekmodo:input",
        this.onSeekmodoInput
      );
      this.subscribed = null;
    }
    if (this.inputEl) {
      this.inputEl.removeEventListener("input", this.onPlainInput);
      this.inputEl.removeEventListener("focus", this.onPlainFocus);
      this.inputEl.removeEventListener("blur", this.onPlainBlur);
      this.inputEl = null;
    }
  }
  bindGlobalListeners() {
    this.bodyClickHandler = (e) => {
      const path = e.composedPath();
      if (path.includes(this)) return;
      if (this.inputEl && path.includes(this.inputEl)) return;
      if (this.subscribed && path.includes(this.subscribed)) return;
      this.dismiss();
    };
    document.addEventListener("click", this.bodyClickHandler);
    this.keyHandler = (e) => this.onKeyDown(e);
    document.addEventListener("keydown", this.keyHandler);
    this.regionChangeHandler = () => {
      this.cache.clear();
      this.current = null;
      this.scheduleRender();
    };
    document.addEventListener(
      "seekmodo:region-change",
      this.regionChangeHandler
    );
  }
  unbindGlobalListeners() {
    if (this.bodyClickHandler) {
      document.removeEventListener("click", this.bodyClickHandler);
      this.bodyClickHandler = null;
    }
    if (this.keyHandler) {
      document.removeEventListener("keydown", this.keyHandler);
      this.keyHandler = null;
    }
    if (this.regionChangeHandler) {
      document.removeEventListener(
        "seekmodo:region-change",
        this.regionChangeHandler
      );
      this.regionChangeHandler = null;
    }
  }
  /**
   * Window-level listeners that re-anchor the dropdown when the
   * page reflows. Attached unconditionally in connectedCallback
   * (cost: three captured-scope arrow fns); `applyAnchor()` is a
   * no-op when the widget is in `anchor="none"` mode, so the
   * overhead on legacy layouts is one bool check per event.
   *
   * Passive `scroll` matters here because the AKS storefront pages
   * we benchmark against fire ~50 scroll events/s while the user
   * spins through the LSU carousel; making them non-passive
   * dropped FCP by ~12 ms on a Moto G Power class device.
   */
  bindAnchorListeners() {
    this.anchorScrollHandler = () => this.scheduleApplyAnchor();
    this.anchorResizeHandler = () => this.scheduleApplyAnchor();
    this.anchorFocusHandler = (e) => {
      const t = e.target;
      if (!(t instanceof Element)) return;
      const target = this.inputEl ?? this.subscribed;
      if (!target) return;
      if (t === target || target.contains(t)) {
        this.applyAnchor();
        this.applyLegacySuppression();
      }
    };
    window.addEventListener("scroll", this.anchorScrollHandler, { passive: true });
    window.addEventListener("resize", this.anchorResizeHandler);
    window.addEventListener("orientationchange", this.anchorResizeHandler);
    document.addEventListener("focusin", this.anchorFocusHandler);
    window.visualViewport?.addEventListener("resize", this.anchorResizeHandler);
  }
  unbindAnchorListeners() {
    if (this.anchorResizeRaf !== null) {
      cancelAnimationFrame(this.anchorResizeRaf);
      this.anchorResizeRaf = null;
    }
    if (this.anchorScrollHandler) {
      window.removeEventListener("scroll", this.anchorScrollHandler);
      this.anchorScrollHandler = null;
    }
    if (this.anchorResizeHandler) {
      window.removeEventListener("resize", this.anchorResizeHandler);
      window.removeEventListener("orientationchange", this.anchorResizeHandler);
      window.visualViewport?.removeEventListener("resize", this.anchorResizeHandler);
      this.anchorResizeHandler = null;
    }
    if (this.anchorFocusHandler) {
      document.removeEventListener("focusin", this.anchorFocusHandler);
      this.anchorFocusHandler = null;
    }
  }
  /** Coalesce scroll/resize/orientation reflows to one rAF pass. */
  scheduleApplyAnchor() {
    if (typeof window === "undefined") return;
    if (this.anchorResizeRaf !== null) return;
    this.anchorResizeRaf = requestAnimationFrame(() => {
      this.anchorResizeRaf = null;
      this.applyAnchor();
    });
  }
  /**
   * Compute the anchor target and pin the host element's screen
   * rect to match. Falls into one of four code paths:
   *
   *   1. `anchor="none"`        — clear inline styles, return.
   *   2. `anchor=<selector>`    — pin to the first element matching
   *                                that CSS selector.
   *   3. `anchor="auto"`        — pin to the bound input or source
   *                                element (whichever subscribe()
   *                                attached to).
   *   4. no anchor target known — clear inline styles, return. This
   *                                catches the legacy posture where
   *                                neither `input` nor `source` is
   *                                set yet (e.g. the element renders
   *                                ahead of its input via document
   *                                order — `applyAnchor()` will fire
   *                                again on the next attribute change
   *                                or scroll event).
   */
  applyAnchor() {
    if (typeof window === "undefined") return;
    const mode = (this.getAttribute("anchor") ?? "auto").trim();
    if (mode === "none" || mode === "") {
      this.clearAnchor();
      return;
    }
    let target = null;
    if (mode === "auto") {
      target = this.inputEl ?? this.subscribed;
    } else {
      try {
        target = document.querySelector(mode);
      } catch {
        target = null;
      }
    }
    if (!target) {
      this.clearAnchor();
      return;
    }
    const rect = target.getBoundingClientRect();
    if (rect.width <= 0 && rect.height <= 0) {
      this.style.visibility = "hidden";
      return;
    }
    const offset = parseInt(this.getAttribute("anchor-offset") ?? "4", 10);
    const safeOffset = Number.isFinite(offset) ? offset : 4;
    const minWidthAttr = this.getAttribute("anchor-min-width");
    const wide = isWideLayout(this.layoutMode());
    const minWidth = minWidthAttr === null ? wide ? 960 : 480 : Math.max(0, parseInt(minWidthAttr, 10) || 0);
    const viewport = typeof window !== "undefined" && window.innerWidth > 0 ? window.innerWidth : Math.max(rect.width, minWidth);
    const maxWide = Math.min(viewport * 0.96, 1440);
    const desiredWidth = wide ? Math.max(rect.width, Math.min(Math.max(minWidth, rect.width), maxWide)) : Math.max(rect.width, minWidth);
    const viewportLimit = wide ? maxWide : Math.max(0, viewport - rect.left - 8);
    const width = wide ? Math.min(desiredWidth, viewportLimit) : Math.max(rect.width, Math.min(desiredWidth, viewportLimit));
    const left = wide ? Math.max(8, (viewport - width) / 2) : rect.left;
    const anchorKey = [
      left,
      width,
      rect.bottom,
      safeOffset,
      wide ? 1 : 0
    ].join("|");
    if (this.anchorApplied && anchorKey === this.lastAnchorKey) {
      if (this.style.visibility === "hidden") this.style.visibility = "";
      return;
    }
    this.style.position = "fixed";
    this.style.zIndex = this.style.zIndex || "10000";
    this.style.top = `${rect.bottom + safeOffset}px`;
    this.style.left = `${left}px`;
    this.style.width = `${width}px`;
    this.style.visibility = "";
    this.style.display = this.style.display || "block";
    this.anchorApplied = true;
    this.lastAnchorKey = anchorKey;
  }
  clearAnchor() {
    if (!this.anchorApplied) return;
    this.lastAnchorKey = "";
    this.style.position = "";
    this.style.top = "";
    this.style.left = "";
    this.style.width = "";
    this.style.visibility = "";
    this.style.zIndex = "";
    this.anchorApplied = false;
  }
  /**
   * Tear down competing legacy typeahead widgets on the input we
   * attached to. Comma-separated values from the `suppress-legacy`
   * attribute — each entry maps to one of a known set of behaviors.
   * Unknown entries are ignored (so future tenants can stash custom
   * tags without breaking older bundles).
   */
  applyLegacySuppression() {
    const raw = this.getAttribute("suppress-legacy");
    if (!raw) return;
    const targets = raw.split(",").map((s) => s.trim()).filter(Boolean);
    const input = this.inputEl;
    if (!input) return;
    for (const t of targets) {
      if (t === "jquery-ui") this.suppressJqueryUiAutocomplete(input);
      else if (t === "seekmodo-typeahead") this.suppressLegacyTypeahead(input);
    }
  }
  suppressJqueryUiAutocomplete(input) {
    const w = window;
    const jq = w.jQuery;
    if (!jq || !jq.ui || !jq.ui.autocomplete) return;
    const $el = jq(input);
    if ($el.data("ui-autocomplete")) {
      try {
        $el.autocomplete("close");
      } catch {
      }
      try {
        $el.autocomplete("destroy");
      } catch {
      }
    }
    const ownsAttr = $el.attr("aria-owns");
    if (ownsAttr) {
      const menu = document.getElementById(ownsAttr);
      if (menu) {
        menu.classList.add("seekmodo-suggest-legacy-suppressed");
        menu.style.display = "none";
        this.suppressedLegacyEls.add(menu);
      }
    }
    document.querySelectorAll("ul.ui-autocomplete").forEach((ul) => {
      const owner = ul.getAttribute("id");
      if (!owner) return;
      const ownedBy = document.querySelector(`[aria-owns="${CSS.escape(owner)}"]`);
      if (ownedBy === input) {
        ul.classList.add("seekmodo-suggest-legacy-suppressed");
        ul.style.display = "none";
        this.suppressedLegacyEls.add(ul);
      }
    });
  }
  /**
   * jQuery-UI autocomplete on legacy storefronts (AKS app.js) wires
   * up on `jQuery(document).ready`, which runs at DOMContentLoaded —
   * after this bundle's `<script defer>` connectedCallback. Schedule
   * a few idempotent re-suppression passes so we always win the race.
   */
  scheduleLegacySuppressionRetries() {
    this.unscheduleLegacySuppressionRetries();
    const run = () => {
      this.applyLegacySuppression();
    };
    this.legacySuppressionRetryHandler = run;
    setTimeout(run, 0);
    setTimeout(run, 50);
    if (document.readyState === "loading") {
      document.addEventListener("DOMContentLoaded", run, { once: true });
    }
    window.addEventListener("load", run, { once: true });
  }
  unscheduleLegacySuppressionRetries() {
    this.legacySuppressionRetryHandler = null;
  }
  suppressLegacyTypeahead(input) {
    const id = input.id;
    if (!id) return;
    document.querySelectorAll(
      `seekmodo-typeahead[input="${CSS.escape(id)}"]`
    ).forEach((el) => {
      el.style.display = "none";
      this.suppressedLegacyEls.add(el);
    });
  }
  restoreLegacyOnDetach() {
    const refs = [];
    document.querySelectorAll(
      ".seekmodo-suggest-legacy-suppressed"
    ).forEach((el) => {
      if (this.suppressedLegacyEls.has(el)) {
        el.classList.remove("seekmodo-suggest-legacy-suppressed");
        el.style.display = "";
        refs.push(el);
      }
    });
    document.querySelectorAll(
      "seekmodo-typeahead"
    ).forEach((el) => {
      if (this.suppressedLegacyEls.has(el)) {
        el.style.display = "";
        refs.push(el);
      }
    });
    for (const r of refs) this.suppressedLegacyEls.delete(r);
  }
  onSeekmodoInput = (e) => {
    const q = e.detail?.query ?? "";
    this.handleQuery(q);
  };
  onPlainInput = (e) => {
    const v = e.target.value ?? "";
    this.handleQuery(v);
  };
  onPlainFocus = () => {
    if (this.current && this.rows.length > 0) {
      this.scheduleRender();
    }
  };
  onPlainBlur = () => {
  };
  handleQuery(q) {
    const trimmed = q.trim();
    const min = parseInt(this.getAttribute("min-length") ?? "2", 10) || 2;
    if (trimmed.length < min) {
      this.lastQuery = trimmed;
      this.current = null;
      this.loading = false;
      this.productsPending = false;
      this.corsBlocked = false;
      this.inflight?.abort();
      this.scheduleRender();
      return;
    }
    this.lastQuery = trimmed;
    this.corsBlocked = false;
    if (this.twoPhaseEnabled()) {
      this.handleQueryTwoPhase(trimmed);
      return;
    }
    const cached = this.cache.get(this.cacheKey(trimmed));
    if (cached) {
      this.current = cached;
      this.loading = false;
      this.productsPending = false;
      this.inflight?.abort();
      this.queueRenderedEvent(trimmed, cached);
      this.emitOpen(trimmed);
      this.scheduleRender();
      return;
    }
    this.loading = true;
    this.scheduleRender();
    this.debounced?.(trimmed);
  }
  handleQueryTwoPhase(trimmed) {
    const fullCached = this.cache.get(this.cacheKey(trimmed, "full"));
    if (fullCached) {
      this.current = fullCached;
      this.loading = false;
      this.productsPending = false;
      this.inflight?.abort();
      this.queueRenderedEvent(trimmed, fullCached);
      this.emitOpen(trimmed);
      this.scheduleRender();
      return;
    }
    this.productsPending = true;
    this.inflight?.abort();
    const prefixCached = this.cache.get(this.cacheKey(trimmed, "prefix"));
    if (prefixCached) {
      this.current = this.stripProducts(prefixCached);
      this.loading = false;
      this.scheduleRender();
      this.debouncedProducts?.(trimmed);
      return;
    }
    this.loading = true;
    this.scheduleRender();
    this.debouncedPrefix?.(trimmed);
    this.debouncedProducts?.(trimmed);
  }
  /** Drop product/category rows for prefix-only paint. */
  stripProducts(res) {
    return {
      ...res,
      products: [],
      categories: [],
      meta: {
        ...res.meta ?? {},
        total: 0,
        counts: {
          ...res.meta?.counts ?? {},
          products: 0,
          categories: 0
        }
      }
    };
  }
  cacheKey(q, phase = "full") {
    const passthrough = this.getSerpPassthrough();
    const vehicleFilter = this.getVehicleFilterArgs();
    const tuneKey = passthrough ? JSON.stringify(passthrough) : "";
    const filterKey = Object.keys(vehicleFilter).length > 0 ? JSON.stringify(vehicleFilter) : "";
    const vid = this.getVehicleId();
    const vehicleKey = vid !== null ? `v${vid}` : filterKey;
    const currencyKey = this.resolvePriceCurrency();
    const phaseKey = phase === "prefix" ? "p" : "f";
    return `${phaseKey}\0${q.toLowerCase()}\0${vehicleKey}\0${tuneKey}\0${currencyKey}`;
  }
  /** Effective ISO currency for cache keys + formatting. */
  resolvePriceCurrency(productCurrency) {
    const attr = this.getAttribute("currency")?.trim();
    if (attr) return attr.toUpperCase();
    if (productCurrency) return productCurrency.toUpperCase();
    const region = this.current?.meta?.region;
    if (region && typeof region === "object" && !Array.isArray(region)) {
      const code = region.currency;
      if (typeof code === "string" && code.trim() !== "") {
        return code.trim().toUpperCase();
      }
    }
    const passthrough = this.getSerpPassthrough();
    const shopper = passthrough?.shopper_context;
    if (shopper && typeof shopper === "object" && !Array.isArray(shopper)) {
      const sc = shopper.currency;
      if (typeof sc === "string" && sc.trim() !== "") {
        return sc.trim().toUpperCase();
      }
    }
    return "USD";
  }
  resolvePriceLocale() {
    const region = this.current?.meta?.region;
    if (region && typeof region === "object" && !Array.isArray(region)) {
      const locale = region.locale;
      if (typeof locale === "string" && locale.trim() !== "") {
        return locale.trim();
      }
    }
    return void 0;
  }
  /** Positive garage vehicle id from `vehicle-id` attribute. */
  getVehicleId() {
    const raw = this.getAttribute("vehicle-id");
    if (!raw) return null;
    const id = parseInt(raw, 10);
    return Number.isFinite(id) && id > 0 ? id : null;
  }
  /**
   * Optional JSON vehicle filter envelope (`vehicle-filter` attribute)
   * or fields merged from `serp-passthrough`. Connectors stamp
   * `filter_by` / `vehicle_filter_mode` here when YMM title tokens
   * apply (vehicle_id=0) or as belt-and-suspenders for fitment.
   */
  getVehicleFilterArgs() {
    const out = {};
    const rawFilter = this.getAttribute("vehicle-filter");
    if (rawFilter) {
      try {
        const parsed = JSON.parse(rawFilter);
        if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
          Object.assign(out, parsed);
        }
      } catch {
      }
    }
    const passthrough = this.getSerpPassthrough();
    if (passthrough) {
      for (const key of [
        "filter_by",
        "vehicle_filter_mode",
        "vehicle_hard_filter",
        "vehicle_id",
        "shopper_context"
      ]) {
        if (passthrough[key] !== void 0 && passthrough[key] !== null && out[key] === void 0) {
          out[key] = passthrough[key];
        }
      }
    }
    const vehicleId = this.getVehicleId();
    if (vehicleId !== null && out.vehicle_id === void 0) {
      out.vehicle_id = vehicleId;
    }
    return out;
  }
  /** Connector Typesense tuning forwarded into /v1/suggest SerpPreview. */
  getSerpPassthrough() {
    const raw = this.getAttribute("serp-passthrough");
    if (!raw) return null;
    try {
      const parsed = JSON.parse(raw);
      if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
        return parsed;
      }
    } catch {
    }
    return null;
  }
  /** Whether a settled query should show a loading skeleton before products land. */
  showProductLoadingSkeleton(q) {
    return q.trim() !== "";
  }
  /**
   * Skip the connector typeahead q-shim when img-ver hydration is wired —
   * that shim returns 60px thumbs that fight the 240px upgrade path.
   * Gateway image_url is still painted immediately; the observer upgrades.
   */
  deferStorefrontThumbs() {
    return this.thumbVer() !== "";
  }
  /** Connector-supplied asset generation — busts stale CDN/browser image caches. */
  thumbVer() {
    return (this.getAttribute("img-ver") ?? "").trim();
  }
  /** Append `_smv` when the connector ships a new img-ver (KIP cached thumbs). */
  thumbSrc(raw) {
    const ver = this.thumbVer();
    if (!ver || !raw) return raw;
    try {
      const base = typeof window !== "undefined" ? window.location.origin : "http://localhost";
      const u = new URL(raw, base);
      u.searchParams.set("_smv", ver);
      return /^https?:\/\//i.test(raw) ? u.toString() : `${u.pathname}${u.search}${u.hash}`;
    } catch {
      const join = raw.includes("?") ? "&" : "?";
      return `${raw}${join}_smv=${encodeURIComponent(ver)}`;
    }
  }
  async fetchPrefix(q) {
    this.inflight?.abort();
    const ctl = new AbortController();
    this.inflight = ctl;
    const token = ++this.prefixFetchToken;
    try {
      const client = await this.getClient();
      const attrLimit = parseInt(this.getAttribute("limit") ?? "5", 10) || 5;
      const baseArgs = this.buildSuggestArgs(q, attrLimit);
      let res = await client.suggest({
        ...baseArgs,
        complete: false,
        include_products: false
      });
      if (token !== this.prefixFetchToken || ctl.signal.aborted) return;
      if (q.trim() !== this.lastQuery) return;
      res = this.stripProducts(res);
      this.cache.set(this.cacheKey(q, "prefix"), res);
      await this.applySuggestResponse(q, attrLimit, res, ctl, token, false, "prefix");
    } catch (err) {
      if (token !== this.prefixFetchToken || ctl.signal.aborted) return;
      if (q.trim() !== this.lastQuery) return;
      this.corsBlocked = isLikelyCorsBlock(err);
      if (this.corsBlocked) {
        this.current = null;
        this.loading = false;
        emit(this, "seekmodo-suggest:cors-blocked", { q });
        console.warn("[seekmodo-suggest] blocked by CORS or network policy", err);
      } else {
        console.warn("[seekmodo-suggest] prefix fetch failed", err);
      }
      this.scheduleRender();
    }
  }
  async fetchProducts(q) {
    this.inflight?.abort();
    const ctl = new AbortController();
    this.inflight = ctl;
    const token = ++this.productFetchToken;
    try {
      const client = await this.getClient();
      const attrLimit = parseInt(this.getAttribute("limit") ?? "5", 10) || 5;
      const layout = this.layoutMode();
      const limit = isWideLayout(layout) ? wideLayoutProductLimit(layout, attrLimit) : attrLimit;
      const baseArgs = this.buildSuggestArgs(q, limit);
      if (this.showProductLoadingSkeleton(q)) {
        this.loading = true;
        this.scheduleRender();
        if (token !== this.productFetchToken || ctl.signal.aborted) return;
      }
      let res = await client.suggest({
        ...baseArgs,
        include_products: true
      });
      if (token !== this.productFetchToken || ctl.signal.aborted) return;
      if (q.trim() !== this.lastQuery) return;
      await this.applySuggestResponse(q, limit, res, ctl, token, true, "full");
    } catch (err) {
      if (token !== this.productFetchToken || ctl.signal.aborted) return;
      if (q.trim() !== this.lastQuery) return;
      this.corsBlocked = isLikelyCorsBlock(err);
      this.current = null;
      this.loading = false;
      this.productsPending = false;
      if (this.corsBlocked) {
        emit(this, "seekmodo-suggest:cors-blocked", { q });
        console.warn("[seekmodo-suggest] blocked by CORS or network policy", err);
      } else {
        console.warn("[seekmodo-suggest] product fetch failed", err);
      }
      this.scheduleRender();
    }
  }
  async fetch(q) {
    this.inflight?.abort();
    const ctl = new AbortController();
    this.inflight = ctl;
    const token = ++this.fetchToken;
    try {
      const client = await this.getClient();
      const attrLimit = parseInt(this.getAttribute("limit") ?? "5", 10) || 5;
      const layout = this.layoutMode();
      const limit = isWideLayout(layout) ? wideLayoutProductLimit(layout, attrLimit) : attrLimit;
      const baseArgs = this.buildSuggestArgs(q, limit);
      if (this.showProductLoadingSkeleton(q)) {
        this.loading = true;
        this.scheduleRender();
        if (token !== this.fetchToken || ctl.signal.aborted) return;
      }
      let res = await client.suggest({
        ...baseArgs,
        include_products: true
      });
      if (token !== this.fetchToken || ctl.signal.aborted) return;
      await this.applySuggestResponse(q, limit, res, ctl, token, true);
    } catch (err) {
      if (token !== this.fetchToken || ctl.signal.aborted) return;
      this.corsBlocked = isLikelyCorsBlock(err);
      this.current = null;
      this.loading = false;
      if (this.corsBlocked) {
        emit(this, "seekmodo-suggest:cors-blocked", { q });
        console.warn("[seekmodo-suggest] blocked by CORS or network policy", err);
      } else {
        console.warn("[seekmodo-suggest] fetch failed", err);
      }
      this.scheduleRender();
    }
  }
  buildSuggestArgs(q, limit) {
    const session = this.getSessionId();
    const args = { q, limit };
    if (session) args.session_id = session;
    const vehicleFilter = this.getVehicleFilterArgs();
    for (const [key, value] of Object.entries(vehicleFilter)) {
      if (value === void 0 || value === null) continue;
      if (key === "vehicle_id") {
        const id = typeof value === "number" ? value : parseInt(String(value), 10);
        if (Number.isFinite(id)) args.vehicle_id = id;
        continue;
      }
      if (key === "filter_by" && typeof value === "string") {
        args.filter_by = value;
        continue;
      }
      if (key === "vehicle_filter_mode" && typeof value === "string") {
        args.vehicle_filter_mode = value;
        continue;
      }
      if (key === "vehicle_hard_filter") {
        args.vehicle_hard_filter = value !== false && value !== "false" && value !== 0 && value !== "0";
        continue;
      }
      if (key === "shopper_context" && value && typeof value === "object" && !Array.isArray(value)) {
        args.shopper_context = value;
      }
    }
    const serpPassthrough = this.getSerpPassthrough();
    if (serpPassthrough) args.serp_passthrough = serpPassthrough;
    return args;
  }
  async applySuggestResponse(q, limit, res, ctl, token, final, phase = "full") {
    const merged = phase === "full" ? await this.mergeTypeaheadFallback(q, limit, res, ctl) : null;
    if (merged) res = merged;
    if (phase === "prefix") {
      res = this.stripProducts(res);
    }
    this.current = res;
    this.loading = !final;
    if (phase === "full") {
      this.productsPending = !final;
    }
    if (final) {
      this.cache.set(this.cacheKey(q, phase), res);
      if (phase === "full") {
        this.productsPending = false;
      }
    }
    if (final && res.redirect?.target_url) {
      window.location.assign(res.redirect.target_url);
      return;
    }
    if (final) {
      this.emitOpen(q);
      if (this.isEmpty(res)) {
        emit(this, "seekmodo-suggest:empty", { q, input: this.inputEl });
      }
    }
    if (final && phase === "full" && (res.products?.length ?? 0) > 0) {
      this.queueRenderedEvent(q, res);
      emit(this, "seekmodo-suggest:render", { q, products: res.products?.length ?? 0 });
    }
    this.scheduleRender();
  }
  /** Remember to emit seekmodo-suggest:rendered after the next paint. */
  queueRenderedEvent(q, res) {
    if ((res.products?.length ?? 0) > 0) {
      this.pendingRenderedQ = q;
    }
  }
  afterRender() {
    const q = this.pendingRenderedQ;
    if (!q) return;
    this.pendingRenderedQ = null;
    const products = this.current?.products?.length ?? 0;
    if (products > 0) {
      emit(this, "seekmodo-suggest:rendered", { q, products });
    }
  }
  getSessionId() {
    if (typeof document === "undefined") return null;
    const m = document.cookie.match(/(?:^|; )seekmodo_session=([^;]+)/);
    return m ? decodeURIComponent(m[1]) : null;
  }
  currentSearchEventId() {
    const raw = this.current?.meta?.search_event_id;
    if (typeof raw === "number" && Number.isFinite(raw) && raw > 0) {
      return Math.trunc(raw);
    }
    if (typeof raw === "string" && raw !== "") {
      const n = parseInt(raw, 10);
      if (Number.isFinite(n) && n > 0) {
        return n;
      }
    }
    return void 0;
  }
  isEmpty(r) {
    return (r.keywords?.length ?? 0) === 0 && (r.products?.length ?? 0) === 0 && (r.categories?.length ?? 0) === 0 && (r.recent?.length ?? 0) === 0 && (r.trending?.length ?? 0) === 0 && !r.did_you_mean;
  }
  typeaheadFallbackUrl() {
    const raw = (this.getAttribute("typeahead-fallback-url") ?? "").trim();
    return raw.length > 0 ? raw : null;
  }
  /**
   * Content-vertical storefronts (e.g. trek.ca) may have an empty
   * Typesense suggest index while the connector's same-origin
   * `/typeahead` REST route still returns native WP rows. Merge
   * those rows into the products block so split-rail stays useful.
   */
  async mergeTypeaheadFallback(q, limit, base, ctl) {
    const url = this.typeaheadFallbackUrl();
    if (!url || typeof fetch !== "function") return null;
    if ((base.products?.length ?? 0) > 0) return null;
    if (this.deferStorefrontThumbs()) return null;
    try {
      const sep = url.includes("?") ? "&" : "?";
      const r = await fetch(
        `${url}${sep}q=${encodeURIComponent(q)}&max=${encodeURIComponent(String(limit))}`,
        { credentials: "same-origin", signal: ctl.signal }
      );
      if (!r.ok) return null;
      const body = await r.json();
      const rows = Array.isArray(body?.rows) ? body.rows : [];
      if (rows.length === 0) return null;
      const products = rows.slice(0, limit).map((row, idx) => {
        const rec = row;
        const id = rec.id !== void 0 && rec.id !== null ? String(rec.id) : String(idx);
        const name = typeof rec.name === "string" && rec.name !== "" ? rec.name : typeof rec.title === "string" ? rec.title : "";
        const urlVal = typeof rec.url === "string" && rec.url !== "" ? rec.url : typeof rec.permalink === "string" ? rec.permalink : void 0;
        const image = typeof rec.image_url === "string" && rec.image_url !== "" ? rec.image_url : typeof rec.thumbnail_url === "string" ? rec.thumbnail_url : void 0;
        return {
          id,
          name,
          url: urlVal,
          image_url: image,
          post_type: typeof rec.post_type === "string" ? rec.post_type : void 0,
          excerpt: typeof rec.excerpt === "string" ? rec.excerpt : void 0
        };
      });
      const meta = { ...base.meta ?? {}, typeahead_fallback: true };
      meta.total = Math.max(meta.total ?? 0, products.length);
      meta.counts = {
        ...meta.counts ?? {},
        products: products.length
      };
      return { ...base, products, meta };
    } catch {
      return null;
    }
  }
  blocks() {
    const raw = this.getAttribute("blocks");
    if (!raw) return DEFAULT_BLOCKS;
    const parsed = raw.split(",").map((s) => s.trim()).filter(
      (s) => [
        "recent",
        "trending",
        "did_you_mean",
        "keywords",
        "products",
        "categories"
      ].includes(s)
    );
    return parsed.length > 0 ? parsed : DEFAULT_BLOCKS;
  }
  label(key) {
    return DEFAULT_LABELS[key] ?? key;
  }
  layoutMode() {
    const raw = (this.getAttribute("layout") ?? DEFAULT_SUGGEST_LAYOUT).trim();
    if (raw === "classic" || raw === "cinema-grid" || raw === "command-bar" || raw === "magazine" || raw === "split-rail") {
      return raw;
    }
    return DEFAULT_SUGGEST_LAYOUT;
  }
  showBrandingFlag() {
    const v = (this.getAttribute("show-branding") ?? "true").trim().toLowerCase();
    return v !== "false" && v !== "0" && v !== "no";
  }
  splitMobileResizeEnabled() {
    const v = (this.getAttribute("split-mobile-resize") ?? "").trim().toLowerCase();
    return v === "true" || v === "1" || v === "yes" || v === "on";
  }
  productTitleTooltipEnabled() {
    const v = (this.getAttribute("product-title-tooltip") ?? "").trim().toLowerCase();
    return v === "true" || v === "1" || v === "yes" || v === "on";
  }
  unbindSplitMobileResize() {
    this.splitResizeCleanup?.();
    this.splitResizeCleanup = null;
  }
  bindSplitMobileResizeIfNeeded(layout, wideWrap) {
    this.unbindSplitMobileResize();
    if (layout !== "split-rail" || !this.splitMobileResizeEnabled()) return;
    const body = wideWrap.querySelector(".split-body");
    if (body instanceof HTMLElement) {
      this.splitResizeCleanup = bindSplitMobileResize(body);
    }
  }
  dismiss() {
    if (this.current === null && !this.loading) return;
    emit(this, "seekmodo-suggest:dismiss", { q: this.lastQuery });
    this.current = null;
    this.loading = false;
    this.productsPending = false;
    this.scheduleRender();
  }
  emitOpen(q) {
    emit(this, "seekmodo-suggest:open", { q });
  }
  /** Navigate to the configured SERP; emits view-all for connector hooks. */
  buildViewAllHref(q) {
    const tmpl = this.getAttribute("view-all-href") ?? "/search?q={q}";
    const href = tmpl.replace("{q}", encodeURIComponent(q));
    return appendQueryParam(href, "seekmodo_skip_category_redirect", "1");
  }
  navigateViewAll() {
    const total = this.current?.meta?.total ?? 0;
    emit(this, "seekmodo-suggest:view-all", { q: this.lastQuery, total });
    window.location.assign(this.buildViewAllHref(this.lastQuery));
  }
  onKeyDown(e) {
    const focused = this.shadowRoot?.activeElement ?? document.activeElement;
    const isOurInput = this.inputEl && focused === this.inputEl || this.subscribed && focused === this.subscribed || this.subscribed && this.subscribed.contains(focused);
    if (!isOurInput && !this.contains(focused)) {
      return;
    }
    if (this.rows.length === 0 && e.key !== "Escape") return;
    if (e.key === "ArrowDown") {
      e.preventDefault();
      this.active = (this.active + 1) % this.rows.length;
      this.applyActive();
    } else if (e.key === "ArrowUp") {
      e.preventDefault();
      this.active = (this.active - 1 + this.rows.length) % this.rows.length;
      this.applyActive();
    } else if (e.key === "Enter" && this.active >= 0) {
      e.preventDefault();
      this.activateRow(this.active);
    } else if (e.key === "Escape") {
      e.preventDefault();
      this.dismiss();
    }
  }
  applyActive() {
    const els = this.root.querySelectorAll(".row");
    els.forEach((el, i) => {
      if (i === this.active) {
        el.classList.add("active");
        el.setAttribute("part", "row row-active");
        el.scrollIntoView({ block: "nearest" });
      } else {
        el.classList.remove("active");
        el.setAttribute("part", "row");
      }
    });
  }
  activateRow(i) {
    const desc = this.rows[i];
    if (!desc) return;
    const searchEventId = this.currentSearchEventId();
    emit(this, "seekmodo-suggest:row-click", {
      block: desc.block,
      row: desc.data,
      q: this.lastQuery,
      value: desc.value,
      id: desc.id,
      position: i + 1,
      ...searchEventId !== void 0 ? { search_event_id: searchEventId } : {}
    });
  }
  render() {
    this.unbindSplitMobileResize();
    const style = document.createElement("style");
    style.textContent = STYLES4;
    if (this.loading && this.current === null && !this.corsBlocked) {
      this.root.replaceChildren(style, this.renderSkeleton());
      this.rows = [];
      this.active = -1;
      return;
    }
    if (this.corsBlocked) {
      renderCorsBlockedPanel(this.root, STYLES4, {
        message: this.label("cors_blocked")
      });
      this.rows = [];
      this.active = -1;
      this.applyAnchor();
      return;
    }
    if (this.current === null) {
      this.root.replaceChildren(style);
      this.rows = [];
      this.active = -1;
      return;
    }
    const displayRes = this.displayResponse();
    if (!displayRes) {
      this.root.replaceChildren(style);
      this.rows = [];
      this.active = -1;
      return;
    }
    if (this.isEmpty(displayRes)) {
      const emptySlot = h("slot", { attrs: { name: "empty" } });
      const fallback = h("div", { class: "empty", text: this.label("empty") });
      emptySlot.append(fallback);
      const wrap2 = h("div", { class: "wrap", part: "wrap" });
      wrap2.append(emptySlot);
      this.root.replaceChildren(style, wrap2);
      this.rows = [];
      this.active = -1;
      return;
    }
    const wrapClass = this.productTitleTooltipEnabled() ? "wrap product-title-tooltip" : "wrap";
    const wrap = h("div", { class: wrapClass, part: "wrap" });
    wrap.append(h("slot", { attrs: { name: "header" } }));
    const rows = [];
    const attrLimit = parseInt(this.getAttribute("limit") ?? "5", 10) || 5;
    const layout = this.layoutMode();
    if (isWideLayout(layout)) {
      const viewAllHref = this.buildViewAllHref(this.lastQuery);
      const priceFmt = (value, currency) => formatSuggestPrice(
        value,
        this.resolvePriceCurrency(currency),
        this.resolvePriceLocale()
      );
      const wideWrap = renderWideLayout(layout, {
        res: displayRes,
        lastQuery: this.lastQuery,
        limit: wideLayoutProductLimit(layout, attrLimit),
        label: (k) => this.label(k),
        rows,
        onRowClick: (pos) => this.activateRow(pos),
        onViewAll: () => this.navigateViewAll(),
        viewAllHref,
        showBranding: this.showBrandingFlag(),
        brandUrl: this.getAttribute("brand-url") ?? "https://seekmodo.com",
        brandLogoUrl: this.getAttribute("brand-logo-url") ?? "https://seekmodo.com/email-assets/seekmodo-lockup.png",
        formatPrice: priceFmt,
        splitMobileResize: this.splitMobileResizeEnabled(),
        productTitleTooltip: this.productTitleTooltipEnabled(),
        resolveThumbSrc: (src) => this.thumbSrc(src),
        productsPending: this.productsPending && this.twoPhaseEnabled()
      });
      this.rows = rows;
      this.active = -1;
      this.root.replaceChildren(style, wideWrap);
      this.bindSplitMobileResizeIfNeeded(layout, wideWrap);
      this.applyAnchor();
      return;
    }
    const blocks = this.blocks();
    for (const blk of blocks) {
      const block = this.renderBlock(blk, displayRes, attrLimit, rows);
      if (block) wrap.append(block);
    }
    this.rows = rows;
    this.active = -1;
    const total = displayRes.meta?.total ?? 0;
    if (total > 0 && this.lastQuery.length > 0) {
      const href = this.buildViewAllHref(this.lastQuery);
      const a = h("a", {
        class: "view-all",
        part: "view-all",
        attrs: { href },
        text: this.label("view_all").replace("{total}", String(total))
      });
      a.addEventListener("click", (e) => {
        e.preventDefault();
        this.navigateViewAll();
      });
      wrap.append(a);
    }
    wrap.append(h("slot", { attrs: { name: "footer" } }));
    if (this.showBrandingFlag()) {
      const brand = h("a", {
        class: "brand-footer",
        part: "brand-footer",
        attrs: {
          href: this.getAttribute("brand-url") ?? "https://seekmodo.com",
          target: "_blank",
          rel: "noopener noreferrer"
        }
      });
      brand.append(h("span", { class: "brand-by", text: "Powered by " }));
      brand.append(h("img", {
        class: "brand-logo",
        part: "brand-logo",
        attrs: {
          src: this.getAttribute("brand-logo-url") ?? "https://seekmodo.com/email-assets/seekmodo-lockup.png",
          alt: "Seekmodo",
          height: "16"
        }
      }));
      wrap.append(brand);
    }
    this.root.replaceChildren(style, wrap);
    this.applyAnchor();
  }
  displayResponse() {
    if (!this.current) return null;
    if (this.productsPending && this.twoPhaseEnabled()) {
      return this.stripProducts(this.current);
    }
    return this.current;
  }
  renderSkeleton() {
    const wrap = h("div", { class: "wrap skeleton", part: "wrap skeleton" });
    for (let i = 0; i < 3; i++) {
      const row = h("div", { class: "row", part: "row skeleton" });
      row.append(h("div", { class: "thumb", part: "thumb" }));
      const name = h("div", { class: "name" });
      name.append(h("span", { class: "name-title" }));
      name.append(h("span", { class: "name-meta" }));
      row.append(name);
      wrap.append(row);
    }
    return wrap;
  }
  /**
   * Render one named block. Returns null when the block has no
   * data. Caller-side slot named the same as the block lets the
   * connector swap any block's rendering wholesale.
   */
  renderBlock(kind, res, limit, rows) {
    if (kind === "did_you_mean") {
      const dym = res.did_you_mean;
      if (!dym) return null;
      const wrap2 = h("div", { class: "group", part: "group did-you-mean" });
      wrap2.append(h("slot", { attrs: { name: "did_you_mean" } }));
      const line = h("div", { class: "did-you-mean" });
      line.append(document.createTextNode(this.label("did_you_mean") + " "));
      const btn = h("button", {
        class: "swap",
        type: "button",
        attrs: { "data-seekmodo-surface": "suggest", "data-seekmodo-block": "did_you_mean" },
        text: dym
      });
      btn.addEventListener("click", () => {
        const searchEventId = this.currentSearchEventId();
        emit(this, "seekmodo-suggest:row-click", {
          block: "did_you_mean",
          row: { value: dym },
          q: this.lastQuery,
          value: dym,
          ...searchEventId !== void 0 ? { search_event_id: searchEventId } : {}
        });
      });
      line.append(btn);
      wrap2.append(line);
      return wrap2;
    }
    const arr = this.blockData(kind, res, limit);
    if (arr.length === 0) return null;
    const wrap = h("div", { class: "group", part: "group", attrs: { "data-block": kind } });
    wrap.append(h("slot", { attrs: { name: kind } }));
    wrap.append(h("div", {
      class: "group-title",
      part: "group-title",
      text: this.label(kind)
    }));
    arr.forEach((item, idx) => {
      const desc = {
        block: kind,
        data: item,
        value: this.rowValue(kind, item),
        id: this.rowId(kind, item)
      };
      rows.push(desc);
      const pos = rows.length - 1;
      const customRow = window.seekmodoSuggest?.renderRow?.(desc.data, kind);
      let rowEl;
      if (customRow instanceof HTMLElement) {
        rowEl = customRow;
        rowEl.classList.add("row");
      } else if (typeof customRow === "string" && customRow.length > 0) {
        rowEl = h("button", { class: "row", part: "row", type: "button" });
        rowEl.innerHTML = customRow;
      } else {
        rowEl = this.renderRowDefault(kind, item, idx);
      }
      rowEl.setAttribute("data-seekmodo-surface", "suggest");
      rowEl.setAttribute("data-seekmodo-block", kind);
      rowEl.setAttribute("data-seekmodo-pos", String(pos));
      if (desc.id) rowEl.setAttribute("data-seekmodo-id", desc.id);
      rowEl.addEventListener("click", () => this.activateRow(pos));
      wrap.append(rowEl);
    });
    return wrap;
  }
  blockData(kind, res, limit) {
    switch (kind) {
      case "recent":
        return (res.recent ?? []).slice(0, limit);
      case "trending":
        return (res.trending ?? []).slice(0, limit);
      case "keywords":
        return (res.keywords ?? []).slice(0, limit);
      case "products":
        return (res.products ?? []).slice(0, limit);
      case "categories":
        return (res.categories ?? []).slice(0, limit);
      default:
        return [];
    }
  }
  rowValue(kind, item) {
    const it = item;
    if (kind === "recent" || kind === "trending" || kind === "keywords") {
      return String(it.keyword ?? "");
    }
    if (kind === "products") {
      return String(it.name ?? it.title ?? "");
    }
    if (kind === "categories") {
      return String(it.name ?? "");
    }
    return "";
  }
  rowId(kind, item) {
    if (kind !== "products") return void 0;
    const id = item.id;
    return id !== void 0 ? String(id) : void 0;
  }
  renderRowDefault(kind, item, _idx) {
    const btn = h("button", {
      class: "row",
      part: "row",
      type: "button"
    });
    if (kind === "products") {
      const p = item;
      const { postType, label } = productContentType(p);
      const imgSrc = p.image_url ?? p.image;
      const titleText = String(p.name ?? p.title ?? "").trim();
      const imgAlt = this.productTitleTooltipEnabled() && titleText ? titleText : "";
      if (postType) {
        btn.setAttribute("data-post-type", postType);
      }
      if (imgSrc) {
        const busted = this.thumbSrc(imgSrc);
        btn.append(h("img", {
          class: "thumb",
          part: "thumb",
          attrs: {
            src: busted,
            "data-src": imgSrc,
            alt: imgAlt,
            loading: "eager",
            decoding: "async"
          }
        }));
      } else {
        const glyph = postType === "page" ? "P" : postType === "post" ? "A" : "\xB7";
        const empty = h("div", {
          class: "thumb thumb-empty",
          part: "thumb thumb--empty",
          text: glyph
        });
        if (postType) {
          empty.setAttribute("data-content-type", postType);
        }
        btn.append(empty);
      }
      const name = h("div", { class: "name", part: "name" });
      const titleSpan = h("span", { class: "name-title", text: titleText });
      if (this.productTitleTooltipEnabled() && titleText) {
        titleSpan.setAttribute("title", titleText);
      }
      name.append(titleSpan);
      const metaBits = [
        label,
        p.brand ? String(p.brand) : "",
        p.sku ?? p.model ?? p.ez_number ?? ""
      ].filter(Boolean);
      if (metaBits.length > 0) {
        name.append(h("span", { class: "name-meta", text: metaBits.join(" \xB7 ") }));
      }
      btn.append(name);
      const priceEl = this.renderPrice(p);
      if (priceEl) btn.append(priceEl);
      return btn;
    }
    if (kind === "categories") {
      const c = item;
      const name = h("div", { class: "name", part: "name", text: c.name });
      btn.append(name);
      if (typeof c.count === "number" && c.count > 0) {
        btn.append(h("span", {
          class: "badge",
          part: "badge",
          text: String(c.count)
        }));
      }
      return btn;
    }
    if (kind === "recent" || kind === "trending" || kind === "keywords") {
      const k = item;
      const name = h("div", {
        class: "name",
        part: "name",
        text: String(k.keyword)
      });
      btn.append(name);
      if (kind === "trending" && typeof k.search_count === "number") {
        btn.append(h("span", {
          class: "badge",
          part: "badge",
          text: String(k.search_count)
        }));
      }
      return btn;
    }
    return btn;
  }
  renderPrice(p) {
    if (p.price === void 0 || p.price === null) return null;
    const currency = this.resolvePriceCurrency(
      typeof p.currency === "string" ? p.currency : void 0
    );
    const locale = this.resolvePriceLocale();
    const wrap = h("div", { class: "price", part: "price" });
    if (p.on_sale && typeof p.sale_price === "number") {
      wrap.append(h("del", { text: formatSuggestPrice(p.price, currency, locale) }));
      wrap.append(document.createTextNode(formatSuggestPrice(p.sale_price, currency, locale)));
    } else {
      wrap.append(document.createTextNode(formatSuggestPrice(p.price, currency, locale)));
    }
    return wrap;
  }
};
function formatSuggestPrice(value, currency, locale) {
  try {
    const fmt = new Intl.NumberFormat(locale, {
      style: "currency",
      currency,
      maximumFractionDigits: 2
    });
    return fmt.format(value);
  } catch {
    return `${value.toFixed(2)} ${currency}`;
  }
}

// src/components/recommendations.ts
var STYLES5 = `
  :host { display: block; font-family: inherit; margin: var(--seekmodo-strip-margin, 1.5rem 0); }
  header { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 0.75rem; }
  h3 { margin: 0; font-size: var(--seekmodo-strip-title-size, 1.1rem); font-weight: 600; }
  .strip {
    display: flex; gap: 0.75rem; overflow-x: auto; padding-bottom: 0.5rem;
    scroll-snap-type: x mandatory;
  }
  .card {
    flex: 0 0 var(--seekmodo-card-width, 180px);
    scroll-snap-align: start;
    border: 1px solid #e4e4e7;
    border-radius: 0.5rem;
    overflow: hidden;
    text-decoration: none;
    color: inherit;
    background: white;
  }
  img { width: 100%; aspect-ratio: 1 / 1; object-fit: cover; }
  .body { padding: 0.4rem 0.6rem; }
  .title { font-size: 0.9rem; }
  .price { font-weight: 600; color: var(--seekmodo-price-color, #16a34a); margin-top: 0.25rem; }
`;
var SeekmodoRecommendations = class extends SeekmodoElement {
  static get observedAttributes() {
    return ["algorithm", "placement", "source-doc-id", "limit"];
  }
  hits = [];
  docs = /* @__PURE__ */ new Map();
  /**
   * Sprint 9 PR 6 — region info from the hydrate-search response,
   * so the strip's price labels render in the shopper's currency
   * + locale without an extra network call.
   */
  hydrateRegion = null;
  loadToken = 0;
  connectedCallback() {
    this.scheduleRender();
    void this.fetchAndRender();
    document.addEventListener(SEEKMODO_REGION_EVENT, this.onRegionChange);
  }
  disconnectedCallback() {
    super.disconnectedCallback();
    document.removeEventListener(SEEKMODO_REGION_EVENT, this.onRegionChange);
  }
  onRegionChange = () => {
    void this.fetchAndRender();
  };
  attributeChangedCallback() {
    if (!this.isConnected) return;
    void this.fetchAndRender();
  }
  async fetchAndRender() {
    const token = ++this.loadToken;
    try {
      const client = await this.getClient();
      const algorithm = this.getAttribute("algorithm") ?? "related";
      const limit = parseInt(this.getAttribute("limit") ?? "8", 10) || 8;
      const placement = this.getAttribute("placement") ?? void 0;
      const sourceDocId = this.getAttribute("source-doc-id") ?? void 0;
      let res;
      switch (algorithm) {
        case "trending":
          res = await client.recommend.trending({ placement, limit });
          break;
        case "also_bought":
          res = await client.recommend.alsoBought({ source_doc_id: sourceDocId, placement, limit });
          break;
        case "also_viewed":
          res = await client.recommend.alsoViewed({ source_doc_id: sourceDocId, placement, limit });
          break;
        case "related":
        default:
          res = await client.recommend.related({ source_doc_id: sourceDocId, placement, limit });
      }
      if (token !== this.loadToken) return;
      this.hits = res.hits;
      if (this.hits.length > 0) {
        const ids = this.hits.map((h2) => h2.doc_id);
        try {
          const hyd = await client.search({
            filter_by: `id:=[${ids.map((i) => JSON.stringify(i)).join(",")}]`,
            per_page: ids.length
          });
          if (token !== this.loadToken) return;
          this.docs = new Map(hyd.hits.map((h2) => [
            String(h2.document.id ?? ""),
            h2.document
          ]));
          const regionMeta = hyd.meta?.region;
          this.hydrateRegion = regionMeta && typeof regionMeta === "object" ? {
            currency: typeof regionMeta.currency === "string" ? regionMeta.currency : void 0,
            locale: typeof regionMeta.locale === "string" ? regionMeta.locale : void 0
          } : null;
        } catch {
          this.docs = /* @__PURE__ */ new Map();
          this.hydrateRegion = null;
        }
      } else {
        this.docs = /* @__PURE__ */ new Map();
        this.hydrateRegion = null;
      }
      emit(this, "seekmodo:recommendations", { hits: this.hits });
      this.scheduleRender();
    } catch (err) {
      if (token !== this.loadToken) return;
      console.warn("[seekmodo-recommendations] fetch failed", err);
      this.hits = [];
      this.docs = /* @__PURE__ */ new Map();
      this.scheduleRender();
    }
  }
  render() {
    const style = document.createElement("style");
    style.textContent = STYLES5;
    if (this.hits.length === 0) {
      this.root.replaceChildren(style);
      return;
    }
    const titleField = this.getAttribute("title-field") ?? "title";
    const imageField = this.getAttribute("image-field") ?? "image_url";
    const priceField = this.getAttribute("price-field") ?? "price_cents";
    const urlField = this.getAttribute("url-field") ?? "url";
    const currency = this.getAttribute("currency") ?? this.hydrateRegion?.currency ?? "USD";
    const locale = this.hydrateRegion?.locale;
    const title = this.getAttribute("title") ?? defaultTitleFor(this.getAttribute("algorithm"));
    const headerSlot = document.createElement("slot");
    headerSlot.name = "header";
    headerSlot.append(h("h3", { text: title }));
    const header = h("header");
    header.append(headerSlot);
    const strip = h("div", { class: "strip", part: "strip" });
    for (const hit of this.hits) {
      const doc = this.docs.get(hit.doc_id);
      if (!doc) continue;
      const t = String(doc[titleField] ?? "");
      const img = String(doc[imageField] ?? "");
      const url = String(doc[urlField] ?? "#");
      const priceCents = Number(doc[priceField] ?? 0);
      const a = h("a", { class: "card", part: "card", href: url, attrs: { "data-doc-id": hit.doc_id } });
      if (img) a.append(h("img", { attrs: { src: img, alt: t, loading: "lazy" } }));
      const body = h("div", { class: "body" });
      body.append(h("div", { class: "title", part: "title", text: t }));
      if (priceCents > 0) {
        body.append(h("div", { class: "price", part: "price", text: formatPrice2(priceCents, currency, locale) }));
      }
      a.append(body);
      a.addEventListener("click", () => this.fireClick(hit.doc_id));
      strip.append(a);
    }
    this.root.replaceChildren(style, header, strip);
  }
  async fireClick(docId) {
    try {
      const client = await this.getClient();
      void client.event({
        event_type: "click",
        doc_id: docId,
        extra: { surface: "recommendations", placement: this.getAttribute("placement") ?? void 0 }
      }).catch(() => {
      });
    } catch {
    }
  }
};
function defaultTitleFor(algo) {
  switch (algo) {
    case "trending":
      return "Trending now";
    case "also_bought":
      return "Customers also bought";
    case "also_viewed":
      return "Customers also viewed";
    default:
      return "Related products";
  }
}
function formatPrice2(cents, currency, locale) {
  try {
    return new Intl.NumberFormat(locale ?? void 0, {
      style: "currency",
      currency
    }).format(cents / 100);
  } catch {
    return `${(cents / 100).toFixed(2)} ${currency}`;
  }
}

// src/components/banner-slot.ts
var STYLES6 = `
  :host { display: block; }
  .banner {
    margin: var(--seekmodo-banner-margin, 0 0 1rem);
  }
`;
var SeekmodoBannerSlot = class extends SeekmodoElement {
  static get observedAttributes() {
    return ["position", "source"];
  }
  banners = [];
  subscribed = null;
  connectedCallback() {
    this.subscribe();
    this.scheduleRender();
  }
  disconnectedCallback() {
    this.unsubscribe();
    super.disconnectedCallback();
  }
  attributeChangedCallback(name) {
    if (name === "source") {
      this.unsubscribe();
      this.subscribe();
    }
    if (name === "position") {
      this.scheduleRender();
    }
  }
  /** Imperative API for storefronts that fetch search themselves. */
  setHits(res) {
    this.banners = res.banners ?? [];
    this.scheduleRender();
  }
  subscribe() {
    const id = this.getAttribute("source");
    if (!id) return;
    const target = document.getElementById(id);
    if (!target) return;
    this.subscribed = target;
    target.addEventListener("seekmodo:hits", this.listener);
  }
  unsubscribe() {
    if (this.subscribed) {
      this.subscribed.removeEventListener("seekmodo:hits", this.listener);
      this.subscribed = null;
    }
  }
  listener = (e) => {
    const detail = e.detail;
    if (detail && typeof detail === "object" && "banners" in detail) {
      this.banners = detail.banners ?? [];
      this.scheduleRender();
    }
  };
  render() {
    const style = document.createElement("style");
    style.textContent = STYLES6;
    const wantedPosition = this.getAttribute("position") ?? "top";
    const matched = this.banners.filter((b) => {
      if (wantedPosition === "inline") {
        return b.position.startsWith("inline");
      }
      return b.position === wantedPosition;
    });
    if (matched.length === 0) {
      const slot = document.createElement("slot");
      this.root.replaceChildren(style, slot);
      return;
    }
    const fragments = [style];
    for (const b of matched) {
      const wrap = h("div", { class: "banner", part: "banner" });
      wrap.innerHTML = b.html;
      fragments.push(wrap);
    }
    this.root.replaceChildren(...fragments);
  }
};

// src/components/chat.ts
var STYLES7 = `
  :host {
    display: block;
    font-family: var(--seekmodo-chat-font, inherit);
    color: inherit;
    border: var(--seekmodo-chat-border, 1px solid #e4e4e7);
    border-radius: var(--seekmodo-chat-radius, 0.5rem);
    background: var(--seekmodo-chat-bg, white);
    max-width: var(--seekmodo-chat-max-width, 480px);
  }
  [part="transcript"] {
    display: flex;
    flex-direction: column;
    gap: var(--seekmodo-chat-gap, 0.75rem);
    padding: var(--seekmodo-chat-padding, 1rem);
    max-height: var(--seekmodo-chat-transcript-height, 360px);
    overflow-y: auto;
  }
  [part="turn"] {
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
    line-height: 1.4;
  }
  [part="turn-user"] {
    align-self: flex-end;
    background: var(--seekmodo-chat-user-bg, #2563eb);
    color: var(--seekmodo-chat-user-fg, white);
    padding: 0.5rem 0.75rem;
    border-radius: 0.5rem 0.5rem 0 0.5rem;
    max-width: 80%;
  }
  [part="turn-assistant"] {
    align-self: flex-start;
    background: var(--seekmodo-chat-assistant-bg, #f4f4f5);
    color: var(--seekmodo-chat-assistant-fg, #18181b);
    padding: 0.5rem 0.75rem;
    border-radius: 0.5rem 0.5rem 0.5rem 0;
    max-width: 90%;
    white-space: pre-wrap;
  }
  [part="thinking"] {
    align-self: flex-start;
    font-style: italic;
    color: #71717a;
    padding: 0.5rem 0.75rem;
  }
  [part="citations"] {
    display: flex;
    flex-wrap: wrap;
    gap: 0.25rem;
    margin-top: 0.25rem;
  }
  [part="citation"] {
    font-size: 0.75rem;
    background: white;
    border: 1px solid #d4d4d8;
    border-radius: 999px;
    padding: 0.125rem 0.5rem;
    color: inherit;
    text-decoration: none;
  }
  [part="error"] {
    font-size: 0.875rem;
    color: #b91c1c;
    padding: 0 1rem;
  }
  [part="upgrade"] {
    align-self: flex-start;
    background: var(--seekmodo-chat-upgrade-bg, #fef3c7);
    color: var(--seekmodo-chat-upgrade-fg, #78350f);
    padding: 0.5rem 0.75rem;
    border-radius: 0.5rem;
    max-width: 90%;
  }
  [part="upgrade"] a {
    color: inherit;
    font-weight: 600;
  }
  [part="composer"] {
    display: flex;
    gap: 0.5rem;
    padding: 0.75rem 1rem;
    border-top: 1px solid #e4e4e7;
  }
  [part="input"] {
    flex: 1;
    padding: 0.5rem 0.75rem;
    border: 1px solid #d4d4d8;
    border-radius: 0.375rem;
    font: inherit;
    color: inherit;
    background: white;
  }
  [part="submit"] {
    padding: 0.5rem 1rem;
    border: none;
    border-radius: 0.375rem;
    background: var(--seekmodo-submit-bg, #2563eb);
    color: var(--seekmodo-submit-fg, white);
    cursor: pointer;
    font: inherit;
  }
  [part="submit"][disabled] { opacity: 0.5; cursor: not-allowed; }
`;
var SeekmodoChat = class extends SeekmodoElement {
  static get observedAttributes() {
    return ["placeholder", "intro"];
  }
  transcriptEl = null;
  inputEl = null;
  submitEl = null;
  turns = [];
  thinking = false;
  maxTurns = 12;
  autoCitations = false;
  connectedCallback() {
    this.maxTurns = Number(this.getAttribute("max-turns") ?? "12") || 12;
    this.autoCitations = this.hasAttribute("auto-citations");
    const intro = (this.getAttribute("intro") ?? "").trim();
    if (intro !== "" && this.turns.length === 0) {
      this.turns.push({ role: "assistant", text: intro });
    }
    this.scheduleRender();
  }
  render() {
    if (!this.transcriptEl) {
      this.firstRender();
    }
    this.repaintTranscript();
    this.updateComposerState();
  }
  firstRender() {
    const style = document.createElement("style");
    style.textContent = STYLES7;
    this.transcriptEl = h("div", {
      part: "transcript",
      attrs: { role: "log", "aria-live": "polite" }
    });
    this.inputEl = h("input", {
      part: "input",
      type: "text",
      placeholder: this.getAttribute("placeholder") ?? "Ask a question\u2026",
      attrs: { "aria-label": "Chat message", autocomplete: "off" }
    });
    this.submitEl = h("button", {
      part: "submit",
      type: "submit",
      text: "Send"
    });
    const composer = h(
      "form",
      { part: "composer", attrs: { role: "search" } },
      [this.inputEl, this.submitEl]
    );
    composer.addEventListener("submit", (e) => {
      e.preventDefault();
      void this.handleSubmit();
    });
    this.root.replaceChildren(style, this.transcriptEl, composer);
  }
  repaintTranscript() {
    if (!this.transcriptEl) return;
    const next = document.createDocumentFragment();
    const slotHost = h("slot", null, []);
    next.appendChild(slotHost);
    for (const t of this.turns) {
      next.appendChild(this.renderTurn(t));
    }
    if (this.thinking) {
      next.appendChild(
        h("div", { part: "thinking", text: "Thinking\u2026" })
      );
    }
    this.transcriptEl.replaceChildren(next);
    this.transcriptEl.scrollTop = this.transcriptEl.scrollHeight;
  }
  renderTurn(turn) {
    if (turn.role === "user") {
      const wrap2 = h("div", { part: "turn" });
      wrap2.appendChild(h("div", { part: "turn-user", text: turn.text }));
      return wrap2;
    }
    if (turn.role === "error") {
      return h("div", { part: "error", text: turn.text });
    }
    if (turn.role === "suppressed") {
      const wrap2 = h("div", { part: "turn" });
      const upgrade = h("div", {
        part: "upgrade",
        attrs: {
          "data-reason": turn.suppressionReason ?? "quota_exceeded_no_credits"
        }
      });
      const slot = h("slot", { attrs: { name: "upgrade" } });
      const fallback = h("span");
      fallback.innerHTML = `${escapeHtml(turn.text)} <a href="/account/billing?upgrade=chat">Upgrade to keep chat</a>`;
      slot.appendChild(fallback);
      upgrade.appendChild(slot);
      wrap2.appendChild(upgrade);
      return wrap2;
    }
    const wrap = h("div", { part: "turn" });
    wrap.appendChild(h("div", { part: "turn-assistant", text: turn.text }));
    if (this.autoCitations && turn.citations && turn.citations.length > 0) {
      const list = h("div", { part: "citations" });
      for (const c of turn.citations) {
        const chip = h("a", {
          part: "citation",
          href: `/products/${encodeURIComponent(c.doc_id)}`,
          text: c.title ?? c.doc_id
        });
        list.appendChild(chip);
      }
      wrap.appendChild(list);
    }
    return wrap;
  }
  updateComposerState() {
    if (!this.inputEl || !this.submitEl) return;
    const userTurns = this.turns.filter((t) => t.role === "user").length;
    const disabled = this.thinking || userTurns >= this.maxTurns || this.turns.some((t) => t.role === "suppressed");
    this.submitEl.disabled = disabled;
    this.inputEl.disabled = disabled;
    this.inputEl.placeholder = disabled ? this.turns.some((t) => t.role === "suppressed") ? "Chat paused \u2014 upgrade to keep using." : userTurns >= this.maxTurns ? "Conversation limit reached. Reload to start a new chat." : "Thinking\u2026" : this.getAttribute("placeholder") ?? "Ask a question\u2026";
  }
  async handleSubmit() {
    if (!this.inputEl) return;
    const text = this.inputEl.value.trim();
    if (text === "" || this.thinking) return;
    this.inputEl.value = "";
    this.turns.push({ role: "user", text });
    this.thinking = true;
    this.scheduleRender();
    let client;
    try {
      client = await this.getClient();
    } catch (err) {
      this.thinking = false;
      this.turns.push({
        role: "error",
        text: "Chat is unavailable \u2014 failed to authenticate."
      });
      this.scheduleRender();
      return;
    }
    const conversation = this.turns.filter((t) => t.role === "user" || t.role === "assistant").map((t) => ({
      role: t.role,
      content: t.text
    }));
    try {
      const res = await client.chat({ messages: conversation });
      this.handleChatResponse(res);
    } catch (err) {
      this.handleChatError(err);
    } finally {
      this.thinking = false;
      this.scheduleRender();
    }
  }
  handleChatResponse(res) {
    const suppressed = Boolean(res.meta?.suppressed);
    if (suppressed) {
      this.turns.push({
        role: "suppressed",
        text: res.message !== "" ? res.message : "Chat is paused for this storefront.",
        suppressionReason: res.meta?.suppression_reason ?? "quota_exceeded_no_credits"
      });
      emit(this, "seekmodo:chat-suppressed", {
        reason: res.meta?.suppression_reason ?? "quota_exceeded_no_credits"
      });
    } else {
      this.turns.push({
        role: "assistant",
        text: res.message,
        citations: res.citations
      });
    }
    emit(this, "seekmodo:chat-turn", {
      message: res.message,
      citations: res.citations,
      meta: res.meta ?? {}
    });
  }
  handleChatError(err) {
    if (err instanceof SeekmodoQuotaError) {
      this.turns.push({
        role: "suppressed",
        text: "Chat is paused for this storefront.",
        suppressionReason: "quota_exceeded_no_credits"
      });
      emit(this, "seekmodo:chat-suppressed", {
        reason: "quota_exceeded_no_credits"
      });
      return;
    }
    const reason = err instanceof SeekmodoError ? err.name.replace(/^Seekmodo/, "").toLowerCase() : "network_error";
    this.turns.push({
      role: "error",
      text: `Sorry \u2014 chat hit an error (${reason}). Try again in a moment.`
    });
  }
};
function escapeHtml(s) {
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
}

// src/components/image-search.ts
var MAX_B64_BYTES = 2 * 1024 * 1024;
var MIN_MAX_EDGE = 128;
var STYLES8 = `
  :host {
    display: inline-block;
    font-family: inherit;
    color: inherit;
  }
  [part="button"] {
    display: inline-flex;
    align-items: center;
    gap: var(--seekmodo-image-button-gap, 0.5rem);
    padding: var(--seekmodo-image-button-padding, 0.5rem 0.75rem);
    border: var(--seekmodo-image-button-border, 1px solid #d4d4d8);
    border-radius: var(--seekmodo-image-button-radius, 0.375rem);
    background: var(--seekmodo-image-button-bg, white);
    color: inherit;
    cursor: pointer;
    font: inherit;
  }
  [part="button"]:hover { filter: brightness(0.97); }
  [part="button"][disabled] { opacity: 0.6; cursor: progress; }
  [part="icon"] {
    width: var(--seekmodo-image-icon-size, 1.25rem);
    height: var(--seekmodo-image-icon-size, 1.25rem);
    flex-shrink: 0;
  }
  [part="preview"] {
    margin-top: var(--seekmodo-image-preview-margin, 0.5rem);
    max-width: var(--seekmodo-image-preview-width, 6rem);
    max-height: var(--seekmodo-image-preview-width, 6rem);
    border: 1px solid #e4e4e7;
    border-radius: 0.25rem;
    object-fit: contain;
  }
  [part="error"] {
    color: var(--seekmodo-image-error-color, #b91c1c);
    font-size: var(--seekmodo-image-error-size, 0.875rem);
    margin-top: var(--seekmodo-image-error-margin, 0.25rem);
  }
  input[type="file"] {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
  }
`;
var CAMERA_SVG = `
  <svg part="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
       fill="none" stroke="currentColor" stroke-width="2"
       stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
    <path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"></path>
    <circle cx="12" cy="13" r="4"></circle>
  </svg>
`;
var SeekmodoImageSearch = class extends SeekmodoElement {
  static get observedAttributes() {
    return ["button-label"];
  }
  buttonEl = null;
  fileInputEl = null;
  previewEl = null;
  errorEl = null;
  busy = false;
  suppressed = false;
  suppressionReason = null;
  connectedCallback() {
    this.scheduleRender();
  }
  attributeChangedCallback() {
    this.scheduleRender();
  }
  render() {
    if (!this.buttonEl) {
      this.firstRender();
    }
    this.updateState();
  }
  firstRender() {
    const style = document.createElement("style");
    style.textContent = STYLES8;
    const buttonLabel = this.getAttribute("button-label") ?? "Search by image";
    this.buttonEl = h("button", {
      part: "button",
      type: "button",
      attrs: { "aria-label": buttonLabel }
    });
    this.buttonEl.innerHTML = CAMERA_SVG.trim();
    const labelNode = h("span", { text: buttonLabel });
    this.buttonEl.appendChild(labelNode);
    const slot = h("slot", null, []);
    this.buttonEl.appendChild(slot);
    this.buttonEl.addEventListener("click", () => this.openPicker());
    this.fileInputEl = h("input", {
      type: "file",
      attrs: {
        accept: "image/*",
        // capture="environment" on mobile cues the OS to open the
        // back camera directly. iOS Safari respects it; Android
        // Chrome treats it as a hint. Desktop browsers fall back
        // to the regular file picker. Setting it unconditionally
        // is fine — storefronts that want desktop-first behaviour
        // can remove it via the `attrs` override on host pages
        // (they'd need to subclass; that's the escape hatch).
        capture: "environment"
      }
    });
    this.fileInputEl.addEventListener("change", () => {
      const f = this.fileInputEl?.files?.[0] ?? null;
      if (f) {
        void this.handleFilePick(f);
      }
    });
    this.previewEl = null;
    this.errorEl = null;
    this.root.replaceChildren(style, this.buttonEl, this.fileInputEl);
  }
  updateState() {
    if (!this.buttonEl) return;
    if (this.suppressed) {
      const wrap = h("div", { part: "upgrade" });
      const upgradeSlot = h("slot", null, []);
      upgradeSlot.setAttribute("name", "upgrade");
      wrap.appendChild(upgradeSlot);
      const fallback = h("a", {
        attrs: { href: "/account/billing?upgrade=image_search" },
        text: "Image search paused \u2014 upgrade to keep"
      });
      wrap.appendChild(fallback);
      this.root.replaceChildren(wrap);
      return;
    }
    if (this.busy) {
      this.buttonEl.setAttribute("disabled", "");
    } else {
      this.buttonEl.removeAttribute("disabled");
    }
  }
  openPicker() {
    if (this.busy || this.suppressed) return;
    this.fileInputEl?.click();
  }
  async handleFilePick(file) {
    this.busy = true;
    this.clearError();
    this.scheduleRender();
    try {
      const maxEdge = this.resolveMaxEdge();
      const quality = this.resolveQuality();
      const b64 = await this.resizeAndEncode(file, maxEdge, quality);
      if (b64.length > MAX_B64_BYTES) {
        const retry = await this.resizeAndEncode(file, Math.floor(maxEdge / 2), quality);
        if (retry.length > MAX_B64_BYTES) {
          this.renderErrorMessage(
            "image_too_large",
            "That image is too large to search with. Try a smaller photo."
          );
          return;
        }
      }
      if (this.hasAttribute("with-preview")) {
        this.renderPreview(b64);
      }
      const client = await this.getClient();
      const results = await this.callSearchByImage(client, b64);
      const suppressed = results.meta?.suppressed === true;
      if (suppressed) {
        this.suppressed = true;
        this.suppressionReason = results.meta?.suppression_reason ?? "quota_exceeded";
        emit(this, "seekmodo:image-search-suppressed", {
          reason: this.suppressionReason
        });
        return;
      }
      emit(this, "seekmodo:image-search", {
        query: "<image>",
        results
      });
    } catch (err) {
      if (err instanceof SeekmodoQuotaError) {
        this.suppressed = true;
        this.suppressionReason = "quota_exceeded";
        emit(this, "seekmodo:image-search-suppressed", {
          reason: this.suppressionReason
        });
        return;
      }
      if (err instanceof SeekmodoError) {
        const errBody = err.body ?? {};
        const errCode = errBody.code ?? errBody.error ?? "error";
        emit(this, "seekmodo:image-search-error", {
          reason: errCode,
          message: err.message
        });
        if (errCode === "feature_not_in_plan" || errCode === "feature_disabled") {
          this.suppressed = true;
          this.suppressionReason = errCode;
          return;
        }
        this.renderErrorMessage(
          errCode,
          "Image search is unavailable right now. Please try again."
        );
        return;
      }
      this.renderErrorMessage(
        "unknown_error",
        "Image search is unavailable right now. Please try again."
      );
    } finally {
      this.busy = false;
      if (this.fileInputEl) {
        this.fileInputEl.value = "";
      }
      this.scheduleRender();
    }
  }
  /**
   * Read the file via FileReader + canvas + toBlob to produce a
   * downsized JPEG, then return the base64 substring (no
   * `data:image/jpeg;base64,` prefix).
   */
  async resizeAndEncode(file, maxEdge, quality) {
    const url = URL.createObjectURL(file);
    try {
      const img = await this.loadImage(url);
      const { width, height } = this.computeDimensions(img, maxEdge);
      const canvas = document.createElement("canvas");
      canvas.width = width;
      canvas.height = height;
      const ctx = canvas.getContext("2d");
      if (!ctx) {
        throw new Error("canvas_unavailable");
      }
      ctx.drawImage(img, 0, 0, width, height);
      const blob = await new Promise((resolve, reject) => {
        canvas.toBlob(
          (b) => {
            if (b) resolve(b);
            else reject(new Error("canvas_toblob_failed"));
          },
          "image/jpeg",
          quality
        );
      });
      const dataUrl = await this.blobToDataUrl(blob);
      const comma = dataUrl.indexOf(",");
      if (comma === -1) {
        throw new Error("dataurl_malformed");
      }
      return dataUrl.slice(comma + 1);
    } finally {
      URL.revokeObjectURL(url);
    }
  }
  loadImage(url) {
    return new Promise((resolve, reject) => {
      const img = new Image();
      img.onload = () => resolve(img);
      img.onerror = () => reject(new Error("image_decode_failed"));
      img.src = url;
    });
  }
  blobToDataUrl(blob) {
    return new Promise((resolve, reject) => {
      const r = new FileReader();
      r.onload = () => resolve(String(r.result ?? ""));
      r.onerror = () => reject(new Error("filereader_failed"));
      r.readAsDataURL(blob);
    });
  }
  computeDimensions(img, maxEdge) {
    const w = img.naturalWidth || img.width;
    const h2 = img.naturalHeight || img.height;
    if (w <= 0 || h2 <= 0) {
      return { width: maxEdge, height: maxEdge };
    }
    if (w <= maxEdge && h2 <= maxEdge) {
      return { width: w, height: h2 };
    }
    const scale = w >= h2 ? maxEdge / w : maxEdge / h2;
    return {
      width: Math.max(1, Math.round(w * scale)),
      height: Math.max(1, Math.round(h2 * scale))
    };
  }
  resolveMaxEdge() {
    const raw = this.getAttribute("max-edge");
    const parsed = raw ? Number(raw) : 1024;
    if (!Number.isFinite(parsed) || parsed < MIN_MAX_EDGE) return 1024;
    return Math.floor(parsed);
  }
  resolveQuality() {
    const raw = this.getAttribute("quality");
    const parsed = raw ? Number(raw) : 0.85;
    if (!Number.isFinite(parsed)) return 0.85;
    if (parsed < 0.1) return 0.1;
    if (parsed > 1) return 1;
    return parsed;
  }
  renderPreview(b64) {
    if (!this.previewEl) {
      this.previewEl = h("img", { part: "preview", attrs: { alt: "Search image preview" } });
      this.root.appendChild(this.previewEl);
    }
    this.previewEl.src = `data:image/jpeg;base64,${b64}`;
  }
  renderErrorMessage(code, message) {
    if (!this.errorEl) {
      this.errorEl = h("div", { part: "error" });
      this.root.appendChild(this.errorEl);
    }
    this.errorEl.textContent = message;
    emit(this, "seekmodo:image-search-error", {
      reason: code,
      message
    });
  }
  clearError() {
    if (this.errorEl) {
      this.errorEl.textContent = "";
    }
  }
  async callSearchByImage(client, imageB64) {
    const sdk = client;
    if (typeof sdk.searchByImage === "function") {
      return sdk.searchByImage({ image_b64: imageB64 });
    }
    return client.transport.call(
      "search.byImage",
      { image_b64: imageB64 },
      {}
    );
  }
  /**
   * Imperative trigger — opens the file picker programmatically.
   * Useful for storefronts that want to bind the camera icon to a
   * custom button in their own DOM.
   */
  open() {
    this.openPicker();
  }
};

// src/components/quiz.ts
var STYLES9 = `
  :host {
    display: block;
    font-family: var(--seekmodo-quiz-font, inherit);
    color: inherit;
    border: var(--seekmodo-quiz-border, 1px solid #e4e4e7);
    border-radius: var(--seekmodo-quiz-radius, 0.5rem);
    background: var(--seekmodo-quiz-bg, white);
    max-width: var(--seekmodo-quiz-max-width, 560px);
    padding: var(--seekmodo-quiz-padding, 1.25rem);
  }
  [part="intro"] {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
  }
  [part="intro"] h2 {
    margin: 0;
    font-size: var(--seekmodo-quiz-title-size, 1.25rem);
  }
  [part="intro"] p {
    margin: 0;
    color: var(--seekmodo-quiz-muted, #71717a);
  }
  [part="progress"] {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    color: var(--seekmodo-quiz-muted, #71717a);
    font-size: 0.875rem;
    margin-bottom: 0.75rem;
  }
  [part="progress"]::after {
    content: "";
    flex: 1;
    height: 4px;
    background: #e4e4e7;
    border-radius: 999px;
    background-image: linear-gradient(to right,
      var(--seekmodo-quiz-progress-fg, #2563eb) 0%,
      var(--seekmodo-quiz-progress-fg, #2563eb) var(--seekmodo-quiz-progress-pct, 0%),
      transparent var(--seekmodo-quiz-progress-pct, 0%));
  }
  [part="question"] {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
  }
  [part="question"] h3 {
    margin: 0;
    font-size: var(--seekmodo-quiz-question-size, 1.05rem);
  }
  [part="option"] {
    appearance: none;
    text-align: left;
    padding: 0.65rem 0.85rem;
    border: 1px solid #d4d4d8;
    border-radius: 0.375rem;
    background: white;
    color: inherit;
    cursor: pointer;
    font: inherit;
  }
  [part="option"]:hover {
    border-color: var(--seekmodo-quiz-option-hover, #2563eb);
  }
  [part="option"][aria-pressed="true"] {
    border-color: var(--seekmodo-quiz-option-active, #2563eb);
    background: var(--seekmodo-quiz-option-active-bg, #eff6ff);
  }
  [part="option"] small {
    display: block;
    color: var(--seekmodo-quiz-muted, #71717a);
    font-size: 0.8rem;
    margin-top: 0.15rem;
  }
  [part="submit"], [part="restart"] {
    appearance: none;
    padding: 0.6rem 1rem;
    border: none;
    border-radius: 0.375rem;
    background: var(--seekmodo-quiz-cta-bg, #2563eb);
    color: var(--seekmodo-quiz-cta-fg, white);
    cursor: pointer;
    font: inherit;
    align-self: flex-start;
  }
  [part="submit"][disabled] { opacity: 0.5; cursor: not-allowed; }
  [part="results"] {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
    gap: 0.75rem;
    margin-top: 0.75rem;
  }
  [part="result-item"] {
    border: 1px solid #e4e4e7;
    border-radius: 0.375rem;
    padding: 0.5rem 0.75rem;
    text-decoration: none;
    color: inherit;
    background: var(--seekmodo-quiz-result-bg, #fafafa);
  }
  [part="result-item"]:hover {
    border-color: var(--seekmodo-quiz-option-hover, #2563eb);
  }
  [part="error"] {
    color: #b91c1c;
    font-size: 0.875rem;
    margin-top: 0.5rem;
  }
`;
var SeekmodoQuiz = class extends SeekmodoElement {
  static get observedAttributes() {
    return ["quiz-id", "quiz-slug", "definition-src", "inline-definition", "auto-start"];
  }
  definition = null;
  phase = { kind: "loading_def" };
  /** Map of question_id -> option_id chosen so far. */
  answers = /* @__PURE__ */ new Map();
  connectedCallback() {
    this.tryLoadDefinition();
    this.scheduleRender();
  }
  attributeChangedCallback(name) {
    if (name === "definition-src" || name === "inline-definition") {
      this.tryLoadDefinition();
    }
    this.scheduleRender();
  }
  /**
   * Resolve the {@link QuizDefinition} from either `inline-definition`
   * (preferred — explicit) or `definition-src` (a CSS selector for a
   * sibling `<script type="application/json">` block). On any error,
   * the phase moves to `error` with a stable reason code.
   */
  tryLoadDefinition() {
    if (this.definition !== null) {
      return;
    }
    const inline = this.getAttribute("inline-definition");
    if (inline !== null && inline.trim() !== "") {
      try {
        const parsed = JSON.parse(inline);
        this.definition = this.validateDefinition(parsed);
        this.phase = this.hasAttribute("auto-start") && this.definition.questions.length > 0 ? { kind: "question", index: 0 } : { kind: "intro" };
        return;
      } catch (err) {
        this.phase = { kind: "error", reason: "definition_parse_failed" };
        return;
      }
    }
    const sel = this.getAttribute("definition-src");
    if (sel !== null && sel.trim() !== "") {
      const node = this.ownerDocument?.querySelector(sel);
      if (node === null || node === void 0) {
        this.phase = { kind: "error", reason: "definition_src_not_found" };
        return;
      }
      try {
        const text = (node.textContent ?? "").trim();
        if (text === "") {
          this.phase = { kind: "error", reason: "definition_src_empty" };
          return;
        }
        const parsed = JSON.parse(text);
        this.definition = this.validateDefinition(parsed);
        this.phase = this.hasAttribute("auto-start") && this.definition.questions.length > 0 ? { kind: "question", index: 0 } : { kind: "intro" };
        return;
      } catch {
        this.phase = { kind: "error", reason: "definition_parse_failed" };
        return;
      }
    }
  }
  /**
   * Defensively validate the shape so an operator-side typo lands as
   * a clean error UI rather than a `Cannot read properties of
   * undefined` runtime explosion mid-question.
   */
  validateDefinition(raw) {
    if (raw === null || typeof raw !== "object") {
      throw new Error("definition_not_object");
    }
    const obj = raw;
    const questionsRaw = obj.questions;
    if (!Array.isArray(questionsRaw) || questionsRaw.length === 0) {
      throw new Error("definition_questions_empty");
    }
    const questions = questionsRaw.map((q, i) => {
      if (q === null || typeof q !== "object") {
        throw new Error(`question_${i}_not_object`);
      }
      const qq = q;
      const id = typeof qq.id === "string" ? qq.id : "";
      const label = typeof qq.label === "string" ? qq.label : "";
      if (id === "" || label === "") {
        throw new Error(`question_${i}_missing_id_or_label`);
      }
      const optsRaw = qq.options;
      if (!Array.isArray(optsRaw) || optsRaw.length === 0) {
        throw new Error(`question_${i}_options_empty`);
      }
      const options = optsRaw.map((o, j) => {
        if (o === null || typeof o !== "object") {
          throw new Error(`question_${i}_option_${j}_not_object`);
        }
        const oo = o;
        const oid = typeof oo.id === "string" ? oo.id : "";
        const olabel = typeof oo.label === "string" ? oo.label : "";
        if (oid === "" || olabel === "") {
          throw new Error(`question_${i}_option_${j}_missing_fields`);
        }
        const desc = typeof oo.description === "string" ? oo.description : void 0;
        return desc !== void 0 ? { id: oid, label: olabel, description: desc } : { id: oid, label: olabel };
      });
      return {
        id,
        label,
        description: typeof qq.description === "string" ? qq.description : void 0,
        options
      };
    });
    const title = typeof obj.title === "string" ? obj.title : void 0;
    const description = typeof obj.description === "string" ? obj.description : void 0;
    return { title, description, questions };
  }
  render() {
    const style = document.createElement("style");
    style.textContent = STYLES9;
    const wrap = h("div", { part: "quiz" });
    if (this.phase.kind === "loading_def") {
      if (this.getAttribute("definition-src") === null && this.getAttribute("inline-definition") === null) {
        wrap.appendChild(
          h("div", { part: "error", text: "Quiz definition not provided." })
        );
      } else {
        wrap.appendChild(
          h("div", { part: "intro" }, [
            h("p", { text: "Loading quiz\u2026" })
          ])
        );
      }
      this.root.replaceChildren(style, wrap);
      return;
    }
    if (this.phase.kind === "error") {
      wrap.appendChild(
        h("div", {
          part: "error",
          text: `Quiz unavailable (${this.phase.reason}).`
        })
      );
      this.root.replaceChildren(style, wrap);
      return;
    }
    if (this.phase.kind === "intro") {
      this.renderIntro(wrap);
    } else if (this.phase.kind === "question") {
      this.renderQuestion(wrap, this.phase.index);
    } else if (this.phase.kind === "submitting") {
      this.renderSubmitting(wrap);
    } else {
      this.renderResults(wrap, this.phase.response);
    }
    this.root.replaceChildren(style, wrap);
  }
  renderIntro(parent) {
    const def = this.definition;
    const intro = h("div", { part: "intro" });
    const headlineSlot = h("slot", { attrs: { name: "intro" } });
    const fallback = h("div");
    if (def.title) {
      fallback.appendChild(h("h2", { text: def.title }));
    }
    if (def.description) {
      fallback.appendChild(h("p", { text: def.description }));
    }
    headlineSlot.appendChild(fallback);
    intro.appendChild(headlineSlot);
    const cta = h("button", {
      part: "submit",
      type: "button",
      text: this.getAttribute("cta-label") ?? "Start"
    });
    cta.addEventListener("click", () => {
      this.phase = { kind: "question", index: 0 };
      this.scheduleRender();
      const q = def.questions[0];
      if (q !== void 0) {
        emit(this, "seekmodo:quiz-question", { index: 0, question: q });
      }
    });
    intro.appendChild(cta);
    parent.appendChild(intro);
  }
  renderQuestion(parent, index) {
    const def = this.definition;
    const q = def.questions[index];
    if (q === void 0) {
      this.phase = { kind: "error", reason: "question_index_out_of_range" };
      this.scheduleRender();
      return;
    }
    const total = def.questions.length;
    const progress = h("div", {
      part: "progress",
      text: `Question ${index + 1} of ${total}`
    });
    const pct = Math.round((index + 1) / total * 100);
    progress.style.setProperty("--seekmodo-quiz-progress-pct", `${pct}%`);
    parent.appendChild(progress);
    const wrap = h("div", { part: "question" });
    wrap.appendChild(h("h3", { text: q.label }));
    if (q.description !== void 0) {
      wrap.appendChild(h("p", { text: q.description }));
    }
    const selected = this.answers.get(q.id);
    for (const opt of q.options) {
      const btn = h("button", {
        part: "option",
        type: "button",
        attrs: {
          "aria-pressed": String(opt.id === selected),
          "data-question-id": q.id,
          "data-option-id": opt.id
        }
      });
      btn.appendChild(h("span", { text: opt.label }));
      if (opt.description !== void 0) {
        btn.appendChild(h("small", { text: opt.description }));
      }
      btn.addEventListener("click", () => {
        this.answers.set(q.id, opt.id);
        emit(this, "seekmodo:quiz-answered", {
          index,
          question_id: q.id,
          option_id: opt.id
        });
        if (index + 1 < total) {
          this.phase = { kind: "question", index: index + 1 };
          this.scheduleRender();
          const nextQ = def.questions[index + 1];
          if (nextQ !== void 0) {
            emit(this, "seekmodo:quiz-question", {
              index: index + 1,
              question: nextQ
            });
          }
        } else {
          this.scheduleRender();
        }
      });
      wrap.appendChild(btn);
    }
    if (index === total - 1 && this.answers.has(q.id)) {
      const submit = h("button", {
        part: "submit",
        type: "button",
        text: this.getAttribute("submit-label") ?? "Show my recommendations"
      });
      submit.addEventListener("click", () => {
        void this.submit();
      });
      wrap.appendChild(submit);
    }
    parent.appendChild(wrap);
  }
  renderSubmitting(parent) {
    parent.appendChild(
      h("div", { part: "intro" }, [
        h("p", { text: "Finding your matches\u2026" })
      ])
    );
  }
  renderResults(parent, response) {
    const cites = response.citations ?? [];
    const wrap = h("div", { part: "results" });
    if (cites.length === 0) {
      const empty = h("div", { part: "intro" });
      const slot = h("slot", { attrs: { name: "empty" } });
      const fb = h("p", {
        text: response.meta?.fallback_used === true ? "No exact matches \u2014 here's what's similar." : "No products match your answers. Try different selections."
      });
      slot.appendChild(fb);
      empty.appendChild(slot);
      parent.appendChild(empty);
    } else {
      for (const c of cites) {
        const card = h("a", {
          part: "result-item",
          href: `/products/${encodeURIComponent(c.doc_id)}`,
          text: c.title ?? c.doc_id
        });
        wrap.appendChild(card);
      }
      parent.appendChild(wrap);
    }
    const restart = h("button", {
      part: "restart",
      type: "button",
      text: "Start over"
    });
    restart.addEventListener("click", () => {
      this.answers.clear();
      this.phase = { kind: "intro" };
      this.scheduleRender();
    });
    parent.appendChild(restart);
  }
  async submit() {
    const quizId = this.getAttribute("quiz-id");
    const quizSlug = this.getAttribute("quiz-slug");
    if ((quizId === null || quizId === "") && (quizSlug === null || quizSlug === "")) {
      this.phase = { kind: "error", reason: "quiz_id_or_slug_required" };
      this.scheduleRender();
      return;
    }
    emit(this, "seekmodo:quiz-complete", {
      answers: Object.fromEntries(this.answers.entries())
    });
    this.phase = { kind: "submitting" };
    this.scheduleRender();
    let client;
    try {
      client = await this.getClient();
    } catch {
      this.phase = { kind: "error", reason: "auth_failed" };
      this.scheduleRender();
      emit(this, "seekmodo:quiz-error", { reason: "auth_failed" });
      return;
    }
    const args = {
      messages: [],
      mode: "quiz",
      answers: Object.fromEntries(this.answers.entries())
    };
    if (quizSlug !== null && quizSlug !== "") {
      args.quiz_slug = quizSlug;
    }
    if (quizId !== null && quizId !== "") {
      args.quiz_id = quizId;
    }
    try {
      const res = await client.chat(args);
      this.phase = { kind: "results", response: res };
      this.scheduleRender();
      emit(this, "seekmodo:quiz-results", {
        citations: res.citations,
        meta: res.meta ?? {}
      });
    } catch (err) {
      const reason = this.normaliseError(err);
      this.phase = { kind: "error", reason };
      this.scheduleRender();
      emit(this, "seekmodo:quiz-error", { reason });
    }
  }
  normaliseError(err) {
    if (err instanceof SeekmodoQuotaError) {
      return "quota_exceeded_no_credits";
    }
    if (err instanceof SeekmodoError) {
      return err.name.replace(/^Seekmodo/, "").toLowerCase();
    }
    return "network_error";
  }
};

// src/components/region-switcher.ts
var STYLES10 = `
  :host { display: inline-block; font-family: inherit; }
  label { display: inline-flex; align-items: center; gap: 0.5rem; }
  span[part="label"] { color: var(--seekmodo-region-label-color, #525252); font-size: 0.85rem; }
  select {
    padding: 0.3rem 0.5rem; font: inherit;
    border: 1px solid var(--seekmodo-region-border, #d4d4d8);
    border-radius: 0.25rem; background: white; color: inherit;
    cursor: pointer;
  }
  select:focus { outline: 2px solid var(--seekmodo-region-focus, #2563eb); outline-offset: 1px; }
`;
var SeekmodoRegionSwitcher = class extends SeekmodoElement {
  static get observedAttributes() {
    return ["regions", "label", "placeholder"];
  }
  /**
   * Public property mirror of the `regions` attribute. Setting
   * this triggers a re-render. Reading returns the parsed list.
   */
  regionsList = [];
  connectedCallback() {
    this.parseRegionsAttr();
    this.scheduleRender();
    document.addEventListener(SEEKMODO_REGION_EVENT, this.onExternalChange);
  }
  disconnectedCallback() {
    super.disconnectedCallback();
    document.removeEventListener(SEEKMODO_REGION_EVENT, this.onExternalChange);
  }
  attributeChangedCallback(name) {
    if (!this.isConnected) return;
    if (name === "regions") this.parseRegionsAttr();
    this.scheduleRender();
  }
  set regions(next) {
    this.regionsList = sanitiseRegions(next);
    this.scheduleRender();
  }
  get regions() {
    return this.regionsList;
  }
  parseRegionsAttr() {
    const raw = this.getAttribute("regions") ?? "";
    if (raw === "") {
      this.regionsList = [];
      return;
    }
    try {
      const parsed = JSON.parse(raw);
      if (Array.isArray(parsed)) {
        this.regionsList = sanitiseRegions(parsed);
      } else {
        this.regionsList = [];
      }
    } catch {
      this.regionsList = [];
    }
  }
  onExternalChange = () => {
    this.scheduleRender();
  };
  render() {
    const style = document.createElement("style");
    style.textContent = STYLES10;
    if (this.regionsList.length === 0) {
      this.root.replaceChildren(style);
      return;
    }
    const labelText = this.getAttribute("label") ?? "Region";
    const placeholder = this.getAttribute("placeholder") ?? "Select your region";
    const current = getSeekmodoRegion();
    const select = h("select", { part: "select" });
    if (current === null) {
      const opt = h("option", { text: placeholder });
      opt.value = "";
      opt.disabled = true;
      opt.selected = true;
      select.append(opt);
    }
    for (const r of this.regionsList) {
      const opt = h("option", {
        text: r.display_name ?? r.id
      });
      opt.value = r.id;
      if (r.id === current) opt.selected = true;
      select.append(opt);
    }
    select.addEventListener("change", () => {
      const slug = select.value || null;
      const written = setSeekmodoRegion(slug);
      emit(this, SEEKMODO_REGION_EVENT, { region: written });
    });
    const label = h("label");
    label.append(
      h("span", { part: "label", text: labelText }),
      select
    );
    this.root.replaceChildren(style, label);
  }
};
function sanitiseRegions(input) {
  if (!Array.isArray(input)) return [];
  const out = [];
  for (const item of input) {
    if (!item || typeof item !== "object") continue;
    const obj = item;
    if (typeof obj.id !== "string" || obj.id === "") continue;
    out.push({
      id: obj.id,
      display_name: typeof obj.display_name === "string" ? obj.display_name : typeof obj.displayName === "string" ? obj.displayName : void 0,
      currency: typeof obj.currency === "string" ? obj.currency : void 0,
      locale: typeof obj.locale === "string" ? obj.locale : void 0
    });
  }
  return out;
}

// src/index.ts
var REGISTRATIONS = [
  ["seekmodo-search-box", SeekmodoSearchBox],
  ["seekmodo-results", SeekmodoResults],
  ["seekmodo-facets", SeekmodoFacets],
  ["seekmodo-typeahead", SeekmodoTypeahead],
  // SM-606 — universal suggest widget. Drop-in replacement for
  // every connector's bespoke typeahead JS (WP vanilla, Zen Cart
  // vanilla, BigCommerce snippet, AKS jQuery UI) backed by the
  // gateway's rich /v1/suggest envelope (recent + trending +
  // did_you_mean + "View all N results").
  ["seekmodo-suggest", SeekmodoSuggest],
  ["seekmodo-recommendations", SeekmodoRecommendations],
  ["seekmodo-banner-slot", SeekmodoBannerSlot],
  ["seekmodo-chat", SeekmodoChat],
  ["seekmodo-image-search", SeekmodoImageSearch],
  // Sprint 10 PR 6 — guided shopping-quiz player. Wraps the
  // gateway's `mode='quiz'` ChatTool branch (PR 2) with a
  // storefront-side question-walk + answer collection UI.
  ["seekmodo-quiz", SeekmodoQuiz],
  // Sprint 9 PR 6 — multi-region.
  ["seekmodo-region-switcher", SeekmodoRegionSwitcher]
];
function defineAll() {
  if (typeof customElements === "undefined") return;
  for (const [name, ctor] of REGISTRATIONS) {
    if (!customElements.get(name)) {
      customElements.define(name, ctor);
    }
  }
}
if (typeof customElements !== "undefined") {
  defineAll();
}
export {
  SEEKMODO_REGION_COOKIE,
  SEEKMODO_REGION_EVENT,
  SeekmodoBannerSlot,
  SeekmodoChat,
  SeekmodoFacets,
  SeekmodoImageSearch,
  SeekmodoQuiz,
  SeekmodoRecommendations,
  SeekmodoRegionSwitcher,
  SeekmodoResults,
  SeekmodoSearchBox,
  SeekmodoSuggest,
  SeekmodoTypeahead,
  defineAll,
  getSeekmodoClient,
  getSeekmodoRegion,
  resetSeekmodoClient,
  setSeekmodoRegion
};
//# sourceMappingURL=index.js.map
