{
  "version": 3,
  "sources": ["/cdn/shop/t/8/assets/trh-fabric-browser.js?cb=20260501a"],
  "sourcesContent": ["/**\n * TRH Fabric Browser - Shared Fabric & Trim Selection Module\n * ===========================================================\n * A self-contained module for browsing, searching, and selecting fabrics and\n * trims from the Shopify Storefront API. Designed to be reused across all\n * made-to-order product calculators (drapery, pillows, furniture, cushions).\n *\n * @module TRHFabricBrowser\n * @version 2.0.0\n *\n * v2.0.0 - Search now queries the FULL product inventory server-side via the\n *   Storefront API `products(query:)` search (scoped by product type), instead\n *   of filtering a 250-product client-side preload. The store has 10,000+\n *   products (5,400+ fabrics), so a bounded preload silently hid most fabrics\n *   from search results. See searchProductsRemote().\n *\n * Usage:\n *   TRHFabricBrowser.init({ ...config });\n *   TRHFabricBrowser.getSelectedFabric();\n *   TRHFabricBrowser.selectFabric(fabricObj);\n *\n * Custom Events (dispatched on document):\n *   - trh:fabric-selected   detail: { fabric }\n *   - trh:trim-selected     detail: { trim }\n *   - trh:fabric-cleared\n *   - trh:trim-cleared\n *   - trh:products-loaded   detail: { fabricCount, trimCount }\n */\n/* TRH fabric min fix 2026-10-26 -------------------------------------------\n * TRHFabricMin: one source of truth for a store fabric's order minimum and\n * unit, shared by every made-to-order calculator. Mirrors the product-page\n * banner (templates/product.json, block custom_liquid_aNEBpa):\n *   - minimum = highest min-N tag, else 1 (no minimum)\n *   - unit    = meter for a per-meter tag or Woodchip & Magnolia fabrics,\n *               else yard\n * Calculators compute yardage in yards; qty() converts to the fabric's sold\n * unit, rounds up, and never returns less than the fabric's minimum.\n * ------------------------------------------------------------------------- */\n(function () {\n  'use strict';\n  if (window.TRHFabricMin) return;\n  var DEFAULT_MIN = 1; /* Rachel t354u 2026-10-26: no min-N tag = no minimum (from 1 whole yard) */\n  var YD_TO_M = 0.9144;\n  var reg = {};\n  function fromTags(tags, vendor, productType) {\n    var min = 0, meter = false;\n    (tags || []).forEach(function (t) {\n      t = String(t).toLowerCase().trim();\n      if (t.slice(0, 4) === 'min-') {\n        var n = parseInt(t.slice(4), 10);\n        if (n > min) min = n;\n      }\n      if (t === 'per-meter') meter = true;\n    });\n    if (vendor === 'Woodchip & Magnolia' && (!productType || productType === 'Fabric')) meter = true;\n    return { min: min > 0 ? min : DEFAULT_MIN, unit: meter ? 'meter' : 'yard' };\n  }\n  function info(f) {\n    if (!f) return { min: DEFAULT_MIN, unit: 'yard' };\n    if (f.minQty) return { min: f.minQty, unit: f.unit || 'yard' };\n    var r = reg[String(f.id || f.variantId || '')];\n    if (r) return r;\n    if (f.tags || f.vendor) return fromTags(f.tags, f.vendor, f.productType);\n    return { min: DEFAULT_MIN, unit: 'yard' };\n  }\n  function toUnits(yards, f) {\n    var u = info(f).unit === 'meter' ? yards * YD_TO_M : yards;\n    return Math.round(u * 1000) / 1000;\n  }\n  function qty(yards, f) {\n    return Math.max(Math.ceil(toUnits(yards, f)), info(f).min);\n  }\n  function unitWord(n, f) {\n    var m = info(f).unit === 'meter';\n    return Number(n) === 1 ? (m ? 'meter' : 'yard') : (m ? 'meters' : 'yards');\n  }\n  function label(n, f) { return n + ' ' + unitWord(n, f); }\n  function abbr(f) { return info(f).unit === 'meter' ? 'm' : 'yd'; }\n  function per(f) { return '/' + abbr(f); }\n  function register(item) {\n    if (item && item.id && item.minQty) reg[String(item.id)] = { min: item.minQty, unit: item.unit || 'yard' };\n  }\n  window.TRHFabricMin = {\n    DEFAULT_MIN: DEFAULT_MIN, fromTags: fromTags, info: info, toUnits: toUnits,\n    qty: qty, unitWord: unitWord, label: label, abbr: abbr, per: per, register: register\n  };\n})();\n\n(function () {\n  'use strict';\n\n  /* =========================================================================\n   * CONSTANTS\n   * ========================================================================= */\n\n  /** @const {string} CSS class prefix for BEM naming */\n  const PREFIX = 'trh-fb';\n\n  /** @const {string} Shopify Storefront API version */\n  const API_VERSION = '2024-01';\n\n  /** @const {number} Default fabric width in inches when no metafield is found */\n  const DEFAULT_FABRIC_WIDTH = 54;\n\n  /** @const {number} Maximum number of products to preload per type (legacy cache) */\n  const MAX_PRODUCTS = 250;\n\n  /** @const {number} Maximum search suggestions to display */\n  const MAX_SUGGESTIONS = 10;\n\n  /** @const {number} Product nodes to request per remote search (over-fetched\n   *  slightly because unavailable variants are filtered out after mapping) */\n  const SEARCH_FETCH_COUNT = 20;\n\n  /** @const {number} Maximum entries kept in the remote search result cache */\n  const SEARCH_CACHE_MAX = 50;\n\n  /** @const {number} Debounce delay in milliseconds for search input */\n  const DEBOUNCE_MS = 200;\n\n  /**\n   * @const {Object<string,string>} Storefront API search scope per item type.\n   * Products are authoritatively categorized in Shopify Admin by product type\n   * (\"Fabric\" / \"Trim\"), so scoping by product_type searches the entire\n   * inventory of that type \u2014 no title/tag guessing needed.\n   */\n  const SEARCH_SCOPES = {\n    /* 2026-10-26: include Vinyl so vinyls / faux leathers are searchable */\n    fabric: '(product_type:Fabric OR product_type:Vinyl)',\n    trim: 'product_type:Trim'\n  };\n\n  /* 2026-10-28 outdoor curtain: OPT-IN per-calculator scope. A calculator may\n   * pass config.fabricScope / config.trimScope (Storefront search syntax),\n   * config.fabricCollection / config.trimCollection (popup collection handle)\n   * and config.fabricRequireAnyTag / config.trimRequireAnyTag (array of tags;\n   * a selection without one of them is refused). When none are passed, every\n   * existing calculator behaves exactly as before. */\n  function scopeFor(type) {\n    if (_config) {\n      if (type === 'fabric' && _config.fabricScope) return _config.fabricScope;\n      if (type === 'trim' && _config.trimScope) return _config.trimScope;\n    }\n    return SEARCH_SCOPES[type];\n  }\n  function blockedByScope(type, item) {\n    if (!_config) return false;\n    var req = type === 'fabric' ? _config.fabricRequireAnyTag : _config.trimRequireAnyTag;\n    if (!req || !req.length) return false;\n    var tags = ((item && item.tags) || []).map(function (t) { return String(t).toLowerCase().trim(); });\n    var ok = req.some(function (r) { return tags.indexOf(String(r).toLowerCase()) !== -1; });\n    if (!ok) {\n      var name = (item && item.title) || (type === 'trim' ? 'This trim' : 'This fabric');\n      window.alert(name + ' ' + (_config.scopeBlockedMessage || 'can\\u2019t be used with this calculator.'));\n    }\n    return !ok;\n  }\n\n  /* =========================================================================\n   * MODULE STATE\n   * ========================================================================= */\n\n  /** @type {Object|null} Current configuration object */\n  let _config = null;\n\n  /** @type {Array<Object>} All loaded fabric products */\n  let _allFabrics = [];\n\n  /** @type {Array<Object>} All loaded trim products */\n  let _allTrims = [];\n\n  /** @type {Object|null} Currently selected fabric */\n  let _selectedFabric = null;\n\n  /** @type {Object|null} Currently selected trim */\n  let _selectedTrim = null;\n\n  /** @type {boolean} Whether products have been loaded from the API */\n  let _productsLoaded = false;\n\n  /** @type {boolean} Whether a product load is currently in progress */\n  let _loadingProducts = false;\n\n  /** @type {Promise|null} Tracks the in-flight loadProducts() promise */\n  let _loadingPromise = null;\n\n  /** @type {Array<Object>} Current search results for zoom navigation */\n  let _currentSearchResults = [];\n\n  /** @type {Map<string, Array<Object>>} Cache of remote search results keyed by type+term */\n  let _searchCache = new Map();\n\n  /**\n   * @type {Object<string, number>} Monotonic sequence counters per item type.\n   * Guards against out-of-order async search responses overwriting newer ones.\n   */\n  let _searchSeq = { fabric: 0, trim: 0 };\n\n  /** @type {string|null} Current zoom modal type ('fabric' or 'trim') */\n  let _zoomType = null;\n\n  /** @type {boolean} Flag to prevent dropdown close during \"choose another\" */\n  let _isChoosingAnother = false;\n\n  /** @type {Object} References to dynamically created DOM elements */\n  let _els = {};\n\n  /* =========================================================================\n   * UTILITIES\n   * ========================================================================= */\n\n  /**\n   * Creates a simple debounce wrapper. Uses lodash if available, otherwise\n   * falls back to a minimal inline implementation.\n   * @param {Function} fn - The function to debounce\n   * @param {number} wait - Delay in milliseconds\n   * @returns {Function} Debounced function\n   */\n  function debounce(fn, wait) {\n    if (typeof _ !== 'undefined' && typeof _.debounce === 'function') {\n      return _.debounce(fn, wait);\n    }\n    let timer = null;\n    return function () {\n      var context = this;\n      var args = arguments;\n      clearTimeout(timer);\n      timer = setTimeout(function () {\n        fn.apply(context, args);\n      }, wait);\n    };\n  }\n\n  /**\n   * Normalizes a string for accent-insensitive search comparison.\n   * Strips diacritical marks via Unicode NFD decomposition.\n   * @param {string} str - The string to normalize\n   * @returns {string} Lowercased, accent-stripped string\n   */\n  function normalizeForSearch(str) {\n    return str\n      .normalize('NFD')\n      .replace(/[\\u0300-\\u036f]/g, '')\n      .toLowerCase();\n  }\n\n  /**\n   * Dispatches a custom event on the document.\n   * @param {string} eventName - Event name (e.g., 'trh:fabric-selected')\n   * @param {Object} [detail={}] - Event detail payload\n   */\n  function dispatchEvent(eventName, detail) {\n    document.dispatchEvent(\n      new CustomEvent(eventName, { detail: detail || {} })\n    );\n  }\n\n  /**\n   * Reports a completed catalogue search.\n   *\n   * What people search for here -- and especially what they search for and do\n   * NOT find -- is demand signal nobody else has: it is unfiltered intent from\n   * customers already on the site, and a direct sourcing input (a term with\n   * repeated zero-result searches is a fabric worth stocking).\n   *\n   * There is no analytics tag on this store yet, so this deliberately writes\n   * to the two places that cost nothing and need no backend: a DOM\n   * CustomEvent, and window.dataLayer. Nothing consumes them today. The moment\n   * GA4 or Tag Manager is installed, every search from that point is already\n   * being emitted in the shape a tag expects, with no further code change --\n   * which matters because search history, like Search Console data, cannot be\n   * backfilled.\n   *\n   * Deliberately records the term, the result count and the type only. No\n   * identifiers, nothing about the visitor.\n   *\n   * @param {string} term Raw search term as typed\n   * @param {string} type 'fabric' or 'trim'\n   * @param {number} count Number of results shown\n   */\n  function reportSearch(term, type, count) {\n    var payload = {\n      search_term: String(term || '').trim().toLowerCase(),\n      search_type: type,\n      result_count: count,\n      zero_results: count === 0\n    };\n    dispatchEvent('trh:catalogue-search', payload);\n    try {\n      window.dataLayer = window.dataLayer || [];\n      window.dataLayer.push(Object.assign({ event: 'trh_catalogue_search' }, payload));\n    } catch (e) {\n      /* dataLayer is a convenience, never a dependency of the search itself */\n    }\n  }\n\n  /**\n   * Creates a DOM element with optional class name, attributes, and text.\n   * @param {string} tag - HTML tag name\n   * @param {Object} [opts] - Options\n   * @param {string} [opts.className] - CSS class(es)\n   * @param {Object} [opts.attrs] - Key/value attribute pairs\n   * @param {string} [opts.text] - Text content\n   * @param {string} [opts.html] - Inner HTML (use with caution)\n   * @returns {HTMLElement}\n   */\n  function createElement(tag, opts) {\n    var el = document.createElement(tag);\n    if (!opts) return el;\n    if (opts.className) el.className = opts.className;\n    if (opts.attrs) {\n      Object.keys(opts.attrs).forEach(function (key) {\n        el.setAttribute(key, opts.attrs[key]);\n      });\n    }\n    if (opts.text) el.textContent = opts.text;\n    if (opts.html) el.innerHTML = opts.html;\n    return el;\n  }\n\n  /**\n   * Escapes HTML entities to prevent XSS in dynamically generated content.\n   * @param {string} str - Raw string\n   * @returns {string} Escaped string safe for innerHTML\n   */\n  function escapeHtml(str) {\n    if (!str) return '';\n    var div = document.createElement('div');\n    div.appendChild(document.createTextNode(str));\n    return div.innerHTML;\n  }\n\n  /* =========================================================================\n   * STYLES\n   * ========================================================================= */\n\n  /**\n   * Injects the module's scoped CSS into the document head. Uses BEM naming\n   * with the `trh-fb` prefix to avoid collisions with existing styles.\n   * Idempotent - will not inject a second time.\n   */\n  function injectStyles() {\n    if (document.getElementById(PREFIX + '-styles')) return;\n\n    var style = document.createElement('style');\n    style.id = PREFIX + '-styles';\n    style.textContent = [\n      /* Search suggestions dropdown */\n      '.' + PREFIX + '__suggestions {',\n      '  position: absolute;',\n      '  top: 100%;',\n      '  left: 0;',\n      '  right: 0;',\n      '  background: #fff;',\n      '  border: 1px solid #ddd;',\n      '  border-top: none;',\n      '  border-radius: 0 0 6px 6px;',\n      '  max-height: 350px;',\n      '  overflow-y: auto;',\n      '  z-index: 1000;',\n      '  display: none;',\n      '  box-shadow: 0 4px 12px rgba(0,0,0,.15);',\n      '}',\n      '.' + PREFIX + '__suggestions--active {',\n      '  display: block;',\n      '  z-index: 1001;',\n      '}',\n\n      /* Individual suggestion row */\n      '.' + PREFIX + '__suggestion {',\n      '  padding: 12px;',\n      '  cursor: pointer;',\n      '  border-bottom: 1px solid #eee;',\n      '  font-size: 14px;',\n      '  display: flex;',\n      '  align-items: center;',\n      '  gap: 10px;',\n      '  transition: background .2s;',\n      '  min-height: 50px;',\n      '  position: relative;',\n      '}',\n      '.' + PREFIX + '__suggestion:hover,',\n      '.' + PREFIX + '__suggestion--selected {',\n      '  background: #f0f8ff;',\n      '  color: #007cba;',\n      '}',\n      '.' + PREFIX + '__suggestion:last-child {',\n      '  border-bottom: none;',\n      '}',\n\n      /* Suggestion image thumbnail */\n      '.' + PREFIX + '__suggestion-image {',\n      '  width: 40px;',\n      '  height: 40px;',\n      '  object-fit: cover;',\n      '  border-radius: 4px;',\n      '  border: 1px solid #ddd;',\n      '  flex-shrink: 0;',\n      '  background: #f3f4f6;',\n      '  cursor: zoom-in;',\n      '}',\n\n      /* Suggestion text details */\n      '.' + PREFIX + '__suggestion-details {',\n      '  flex: 1;',\n      '  min-width: 0;',\n      '}',\n      '.' + PREFIX + '__suggestion-name {',\n      '  font-weight: 500;',\n      '  margin: 0 0 2px 0;',\n      '  white-space: nowrap;',\n      '  overflow: hidden;',\n      '  text-overflow: ellipsis;',\n      '}',\n      '.' + PREFIX + '__suggestion-price {',\n      '  font-size: 12px;',\n      '  color: #666;',\n      '  margin: 0;',\n      '}',\n\n      /* No results message */\n      '.' + PREFIX + '__no-results {',\n      '  padding: 12px;',\n      '  color: #666;',\n      '  font-style: italic;',\n      '  text-align: center;',\n      '}',\n\n      /* Swatch preview */\n      '.' + PREFIX + '__swatch-preview {',\n      '  display: none;',\n      '  margin-top: 5px;',\n      '  align-items: center;',\n      '  gap: 6px;',\n      '  position: relative;',\n      '}',\n      '.' + PREFIX + '__swatch-preview--visible {',\n      '  display: flex;',\n      '}',\n      '.' + PREFIX + '__swatch-image {',\n      '  width: 40px;',\n      '  height: 40px;',\n      '  object-fit: cover;',\n      '  border-radius: 4px;',\n      '  border: 1px solid #ddd;',\n      '  background: #f3f4f6;',\n      '}',\n      '.' + PREFIX + '__swatch-image--clickable {',\n      '  cursor: zoom-in;',\n      '}',\n      '.' + PREFIX + '__swatch-price {',\n      '  font-size: 12px;',\n      '  color: #666;',\n      '  margin: 0;',\n      '}',\n      '.' + PREFIX + '__swatch-clear {',\n      '  background: transparent;',\n      '  border: none;',\n      '  outline: none;',\n      '  box-shadow: none;',\n      '  cursor: pointer;',\n      '  font-size: 18px;',\n      '  color: #999;',\n      '  display: none;',\n      '  transition: color .2s;',\n      '  line-height: 1;',\n      '  padding: 0;',\n      '  margin: 0 0 0 8px;',\n      '  font-weight: normal;',\n      '  width: auto;',\n      '  height: auto;',\n      '  min-width: unset;',\n      '  min-height: unset;',\n      '}',\n      '.' + PREFIX + '__swatch-clear--visible {',\n      '  display: inline-block;',\n      '}',\n      '.' + PREFIX + '__swatch-clear:hover {',\n      '  color: #666;',\n      '}',\n\n      /* Zoom modal */\n      '.' + PREFIX + '__zoom-overlay {',\n      '  display: none;',\n      '  position: fixed;',\n      '  top: 0;',\n      '  left: 0;',\n      '  right: 0;',\n      '  bottom: 0;',\n      '  background: rgba(0,0,0,.9);',\n      '  z-index: 10000;',\n      '  justify-content: center;',\n      '  align-items: center;',\n      '  cursor: pointer;',\n      '}',\n      '.' + PREFIX + '__zoom-overlay--visible {',\n      '  display: flex;',\n      '}',\n      '.' + PREFIX + '__zoom-content {',\n      '  background: #fff;',\n      '  border-radius: 12px;',\n      '  padding: 20px;',\n      '  max-width: 80vw;',\n      '  max-height: 80vh;',\n      '  text-align: center;',\n      '  position: relative;',\n      '  overflow-y: auto;',\n      '  cursor: default;',\n      '}',\n      '.' + PREFIX + '__zoom-close {',\n      '  position: absolute;',\n      '  top: 10px;',\n      '  right: 15px;',\n      '  font-size: 32px;',\n      '  font-weight: bold;',\n      '  cursor: pointer;',\n      '  color: #fff;',\n      '  background: rgba(0,0,0,0.6);',\n      '  width: 40px;',\n      '  height: 40px;',\n      '  border-radius: 50%;',\n      '  display: flex;',\n      '  align-items: center;',\n      '  justify-content: center;',\n      '  z-index: 100;',\n      '  transition: background .2s, transform .2s;',\n      '  border: none;',\n      '  line-height: 1;',\n      '}',\n      '.' + PREFIX + '__zoom-close:hover {',\n      '  background: rgba(0,0,0,0.8);',\n      '  transform: scale(1.1);',\n      '}',\n      '.' + PREFIX + '__zoom-image {',\n      '  max-width: 400px;',\n      '  max-height: 400px;',\n      '  object-fit: contain;',\n      '  border-radius: 8px;',\n      '}',\n      '.' + PREFIX + '__zoom-title {',\n      '  font-size: 18px;',\n      '  font-weight: bold;',\n      '  margin: 10px 0;',\n      '}',\n      '.' + PREFIX + '__zoom-price {',\n      '  font-size: 14px;',\n      '  color: #666;',\n      '  margin-bottom: 10px;',\n      '}',\n      '.' + PREFIX + '__zoom-actions {',\n      '  display: flex;',\n      '  justify-content: center;',\n      '  gap: 8px;',\n      '  flex-wrap: wrap;',\n      '}',\n      '.' + PREFIX + '__zoom-btn {',\n      '  background: #28a745;',\n      '  color: #fff;',\n      '  border: none;',\n      '  padding: 8px 16px;',\n      '  border-radius: 6px;',\n      '  font-size: 14px;',\n      '  cursor: pointer;',\n      '  margin: 5px;',\n      '  transition: background .2s, transform .2s, box-shadow .2s;',\n      '}',\n      '.' + PREFIX + '__zoom-btn:hover {',\n      '  background: #218838;',\n      '  transform: scale(1.05);',\n      '  box-shadow: 0 4px 8px rgba(0,0,0,.15);',\n      '}',\n      '.' + PREFIX + '__zoom-btn:active {',\n      '  transform: scale(1);',\n      '  box-shadow: 0 2px 4px rgba(0,0,0,.1);',\n      '}',\n      '.' + PREFIX + '__zoom-btn--secondary {',\n      '  background: #6c757d;',\n      '}',\n      '.' + PREFIX + '__zoom-btn--secondary:hover {',\n      '  background: #5a6268;',\n      '}',\n      '.' + PREFIX + '__zoom-nav {',\n      '  position: absolute;',\n      '  top: 50%;',\n      '  transform: translateY(-50%);',\n      '  background: #6c757d;',\n      '  color: #fff;',\n      '  border: none;',\n      '  padding: 8px 12px;',\n      '  border-radius: 6px;',\n      '  font-size: 14px;',\n      '  cursor: pointer;',\n      '  transition: background .2s, transform .2s, box-shadow .2s;',\n      '}',\n      '.' + PREFIX + '__zoom-nav:hover:not(:disabled) {',\n      '  background: #5a6268;',\n      '  transform: translateY(-50%) scale(1.05);',\n      '  box-shadow: 0 4px 8px rgba(0,0,0,.15);',\n      '}',\n      '.' + PREFIX + '__zoom-nav:active:not(:disabled) {',\n      '  transform: translateY(-50%) scale(1);',\n      '  box-shadow: 0 2px 4px rgba(0,0,0,.1);',\n      '}',\n      '.' + PREFIX + '__zoom-nav:disabled {',\n      '  opacity: 0.5;',\n      '  cursor: not-allowed;',\n      '  background: #6c757d;',\n      '}',\n      '.' + PREFIX + '__zoom-nav--prev {',\n      '  left: 10px;',\n      '}',\n      '.' + PREFIX + '__zoom-nav--next {',\n      '  right: 10px;',\n      '}',\n\n      /* Catalog styles are inside blob iframe \u2014 see buildCatalogHTML() */\n\n      /* Search input selected state */\n      '.' + PREFIX + '__search-input--selected {',\n      '  background: #f8f9fa;',\n      '  border-color: #28a745;',\n      '}',\n\n      /* Responsive adjustments */\n      '@media (max-width: 400px) {',\n      '  .' + PREFIX + '__suggestion { padding: 8px; min-height: 45px; }',\n      '  .' + PREFIX + '__suggestion-image,',\n      '  .' + PREFIX + '__swatch-image { width: 35px; height: 35px; }',\n      '  .' + PREFIX + '__zoom-image { max-width: 250px; max-height: 250px; }',\n      '  .' + PREFIX + '__zoom-nav { padding: 6px 10px; font-size: 12px; }',\n      '}'\n    ].join('\\n');\n\n    document.head.appendChild(style);\n  }\n\n  /* =========================================================================\n   * DOM CREATION\n   * ========================================================================= */\n\n  /**\n   * Creates and injects the zoom modal into the DOM. The zoom modal displays\n   * a large preview of a fabric/trim with navigation and selection controls.\n   * @returns {HTMLElement} The zoom modal overlay element\n   */\n  function createZoomModal() {\n    var overlay = createElement('div', {\n      className: PREFIX + '__zoom-overlay',\n      attrs: { id: PREFIX + '-zoom-overlay', role: 'dialog', 'aria-modal': 'true', 'aria-label': 'Product zoom view' }\n    });\n\n    var content = createElement('div', { className: PREFIX + '__zoom-content' });\n\n    var closeBtn = createElement('button', {\n      className: PREFIX + '__zoom-close',\n      attrs: { type: 'button', 'aria-label': 'Close zoom view' },\n      html: '&times;'\n    });\n\n    var img = createElement('img', {\n      className: PREFIX + '__zoom-image',\n      attrs: { id: PREFIX + '-zoom-image', alt: '' }\n    });\n\n    var title = createElement('div', {\n      className: PREFIX + '__zoom-title',\n      attrs: { id: PREFIX + '-zoom-title' }\n    });\n\n    var price = createElement('div', {\n      className: PREFIX + '__zoom-price',\n      attrs: { id: PREFIX + '-zoom-price' }\n    });\n\n    var actions = createElement('div', { className: PREFIX + '__zoom-actions' });\n\n    var selectBtn = createElement('button', {\n      className: PREFIX + '__zoom-btn',\n      attrs: { type: 'button', id: PREFIX + '-zoom-select' },\n      text: 'Select This'\n    });\n\n    var chooseBtn = createElement('button', {\n      className: PREFIX + '__zoom-btn ' + PREFIX + '__zoom-btn--secondary',\n      attrs: { type: 'button', id: PREFIX + '-zoom-choose-another' },\n      text: 'Choose Another'\n    });\n\n    actions.appendChild(selectBtn);\n    actions.appendChild(chooseBtn);\n\n    var prevBtn = createElement('button', {\n      className: PREFIX + '__zoom-nav ' + PREFIX + '__zoom-nav--prev',\n      attrs: { type: 'button', id: PREFIX + '-zoom-prev' },\n      text: 'Previous'\n    });\n\n    var nextBtn = createElement('button', {\n      className: PREFIX + '__zoom-nav ' + PREFIX + '__zoom-nav--next',\n      attrs: { type: 'button', id: PREFIX + '-zoom-next' },\n      text: 'Next'\n    });\n\n    content.appendChild(closeBtn);\n    content.appendChild(img);\n    content.appendChild(title);\n    content.appendChild(price);\n    content.appendChild(actions);\n    content.appendChild(prevBtn);\n    content.appendChild(nextBtn);\n    overlay.appendChild(content);\n\n    _els.zoomOverlay = overlay;\n    _els.zoomImage = img;\n    _els.zoomTitle = title;\n    _els.zoomPrice = price;\n    _els.zoomSelectBtn = selectBtn;\n    _els.zoomChooseBtn = chooseBtn;\n    _els.zoomPrevBtn = prevBtn;\n    _els.zoomNextBtn = nextBtn;\n    _els.zoomCloseBtn = closeBtn;\n\n    return overlay;\n  }\n\n  /**\n   * Creates the catalog lightbox overlay with an iframe. The catalog content\n   * is loaded via a blob URL (see buildCatalogHTML), giving perfect CSS\n   * isolation from the Craft theme.\n   * @param {string} type - 'fabric' or 'trim'\n   * @returns {HTMLElement} The overlay element (appended to document.body)\n   */\n  function createCatalogModal(type) {\n    /* Overlay \u2014 simple fixed backdrop with centered iframe */\n    var overlay = document.createElement('div');\n    overlay.id = PREFIX + '-catalog-overlay-' + type;\n    overlay.style.cssText = 'display:none;position:fixed;top:0;left:0;right:0;bottom:0;' +\n      'background:rgba(0,0,0,.85);z-index:10000;justify-content:center;align-items:center;';\n    overlay.setAttribute('role', 'dialog');\n    overlay.setAttribute('aria-modal', 'true');\n\n    /* Iframe \u2014 blob URL gives us perfect CSS isolation from Craft theme */\n    var iframe = document.createElement('iframe');\n    iframe.id = PREFIX + '-catalog-iframe-' + type;\n    iframe.style.cssText = 'width:94vw;max-width:1200px;height:90vh;border:none;' +\n      'border-radius:12px;background:#fff;';\n    iframe.setAttribute('sandbox', 'allow-scripts');\n\n    overlay.appendChild(iframe);\n\n    if (type === 'fabric') {\n      _els.catalogFabricOverlay = overlay;\n      _els.catalogFabricIframe = iframe;\n    } else {\n      _els.catalogTrimOverlay = overlay;\n      _els.catalogTrimIframe = iframe;\n    }\n\n    return overlay;\n  }\n\n  /**\n   * Builds a self-contained HTML document for the catalog iframe.\n   * Includes search, product type filter, sort, and a card grid.\n   * Communication with the parent page is via postMessage.\n   * @param {string} type - 'fabric' or 'trim'\n   * @param {Array<Object>} products - Product items to display\n   * @returns {string} Complete HTML document string\n   */\n  function buildCatalogHTML(type, products) {\n    var label = type === 'fabric' ? 'Fabric' : 'Trim';\n\n    /* Serialize product data for embedding in the iframe script */\n    var itemsJSON = JSON.stringify(products.map(function (p) {\n      return {\n        id: p.id, title: p.title, price: p.price,\n        imageUrl: p.imageUrl, altText: p.altText || p.title,\n        fabricWidth: p.fabricWidth || null,\n        tags: p.tags || [], productType: p.productType || '',\n        vendor: p.vendor || '', minQty: p.minQty || null, unit: p.unit || 'yard'\n      };\n    }));\n\n    /* Build product type filter options */\n    var typeSet = {};\n    products.forEach(function (p) { if (p.productType) typeSet[p.productType] = true; });\n    var typeOpts = Object.keys(typeSet).sort().map(function (t) {\n      return '<option value=\"' + t.replace(/\"/g, '&quot;') + '\">' + t + '</option>';\n    }).join('');\n\n    return [\n      '<!DOCTYPE html><html><head><meta charset=\"utf-8\">',\n      '<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">',\n      '<style>',\n      '*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}',\n      'body{font-family:-apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,sans-serif;background:#fff;color:#333}',\n      '.main{display:flex;flex-direction:column;height:100vh}',\n      '.header{display:flex;align-items:center;justify-content:space-between;padding:18px 24px;border-bottom:1px solid #e0e0e0}',\n      '.title{font-family:\"Times New Roman\",Garamond,serif;font-size:22px;font-weight:600;letter-spacing:1px;text-transform:uppercase}',\n      '.close-btn{font-size:28px;cursor:pointer;color:#333;background:none;width:36px;height:36px;',\n      '  border-radius:50%;display:flex;align-items:center;justify-content:center;border:none;line-height:1}',\n      '.close-btn:hover{background:#f0f0f0}',\n      '.toolbar{display:flex;gap:12px;padding:14px 24px;border-bottom:1px solid #e0e0e0;flex-wrap:wrap;align-items:center}',\n      '.search-input{flex:1;min-width:200px;padding:10px 14px;border:1px solid #ccc;border-radius:6px;font-size:15px;outline:none}',\n      '.search-input:focus{border-color:#888}',\n      '.filter-select{padding:8px 12px;border:1px solid #ccc;border-radius:6px;font-size:14px;background:#fff;cursor:pointer}',\n      '.count{padding:8px 24px;font-size:13px;color:#888}',\n      '.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(175px,1fr));gap:16px;padding:16px 24px 24px;overflow-y:auto;flex:1}',\n      '.card{border:1px solid #e0e0e0;border-radius:8px;overflow:hidden;cursor:pointer;',\n      '  transition:border-color .2s,box-shadow .2s;background:#fff}',\n      '.card:hover{border-color:#888;box-shadow:0 3px 10px rgba(0,0,0,.12)}',\n      '.card-img{width:100%;aspect-ratio:1/1;object-fit:cover;display:block;background:#f0f0f0}',\n      '.card-info{padding:10px}',\n      '.card-name{font-size:13px;font-weight:600;line-height:1.3;margin-bottom:3px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',\n      '.card-price{font-size:13px;color:#666}',\n      '.empty{grid-column:1/-1;text-align:center;padding:60px 20px;color:#888;font-size:16px}',\n      '@media(max-width:600px){.grid{grid-template-columns:repeat(2,1fr);gap:10px;padding:10px}.toolbar{padding:10px 14px}}',\n      '</style></head><body><div class=\"main\">',\n      '<div class=\"header\">',\n      '  <div class=\"title\">' + label + ' Collection</div>',\n      '  <button class=\"close-btn\" id=\"closeBtn\">&times;</button>',\n      '</div>',\n      '<div class=\"toolbar\">',\n      '  <input type=\"text\" class=\"search-input\" id=\"searchInput\" placeholder=\"Search ' + type + 's by name...\">',\n      '  <select class=\"filter-select\" id=\"typeFilter\"><option value=\"\">All Types</option>' + typeOpts + '</select>',\n      '  <select class=\"filter-select\" id=\"sortSelect\">',\n      '    <option value=\"name-asc\">Name A\\u2013Z</option>',\n      '    <option value=\"name-desc\">Name Z\\u2013A</option>',\n      '    <option value=\"price-asc\">Price: Low\\u2013High</option>',\n      '    <option value=\"price-desc\">Price: High\\u2013Low</option>',\n      '  </select>',\n      '</div>',\n      '<div class=\"count\" id=\"count\"></div>',\n      '<div class=\"grid\" id=\"grid\"></div>',\n      '</div>',\n      '<script>',\n      'var items=' + itemsJSON + ';',\n      'var itemType=\"' + type + '\";',\n      'function esc(s){var d=document.createElement(\"div\");d.textContent=s;return d.innerHTML}',\n      'function render(){',\n      '  var term=document.getElementById(\"searchInput\").value.toLowerCase().trim();',\n      '  var typeVal=document.getElementById(\"typeFilter\").value;',\n      '  var sortVal=document.getElementById(\"sortSelect\").value;',\n      '  var filtered=items.filter(function(p){',\n      '    if(term&&p.title.toLowerCase().indexOf(term)===-1)return false;',\n      '    if(typeVal&&p.productType!==typeVal)return false;',\n      '    return true;',\n      '  });',\n      '  filtered.sort(function(a,b){',\n      '    switch(sortVal){',\n      '      case\"name-desc\":return b.title.localeCompare(a.title);',\n      '      case\"price-asc\":return a.price-b.price;',\n      '      case\"price-desc\":return b.price-a.price;',\n      '      default:return a.title.localeCompare(b.title);',\n      '    }',\n      '  });',\n      '  document.getElementById(\"count\").textContent=\"Showing \"+filtered.length+\" of \"+items.length+\" \"+itemType+(items.length!==1?\"s\":\"\");',\n      '  var grid=document.getElementById(\"grid\");',\n      '  grid.innerHTML=\"\";',\n      '  if(filtered.length===0){grid.innerHTML=\\'<div class=\"empty\">No \\'+itemType+\\'s found.</div>\\';return}',\n      '  var frag=document.createDocumentFragment();',\n      '  filtered.forEach(function(item){',\n      '    var card=document.createElement(\"div\");card.className=\"card\";',\n      '    card.innerHTML=\\'<img class=\"card-img\" src=\"\\'+esc(item.imageUrl)+\\'\" alt=\"\\'+esc(item.altText)+\\'\" loading=\"lazy\">\\'',\n      '      +\\'<div class=\"card-info\"><div class=\"card-name\">\\'+esc(item.title)+\\'</div>\\'',\n      '      +\\'<div class=\"card-price\">$\\'+item.price.toFixed(2)+(item.unit===\"meter\"?\"/m\":\"/yd\")+\\'</div></div>\\';',\n      '    card.addEventListener(\"click\",function(){',\n      '      parent.postMessage({type:\"trhCatalogSelect\",itemType:itemType,item:item},\"*\");',\n      '    });',\n      '    frag.appendChild(card);',\n      '  });',\n      '  grid.appendChild(frag);',\n      '}',\n      'var _t;document.getElementById(\"searchInput\").addEventListener(\"input\",function(){clearTimeout(_t);_t=setTimeout(render,200)});',\n      'document.getElementById(\"typeFilter\").addEventListener(\"change\",render);',\n      'document.getElementById(\"sortSelect\").addEventListener(\"change\",render);',\n      'document.getElementById(\"closeBtn\").addEventListener(\"click\",function(){parent.postMessage({type:\"trhCatalogClose\"},\"*\")});',\n      'document.addEventListener(\"keydown\",function(e){if(e.key===\"Escape\")parent.postMessage({type:\"trhCatalogClose\"},\"*\")});',\n      'render();',\n      'setTimeout(function(){document.getElementById(\"searchInput\").focus()},100);',\n      '<\\/script></body></html>'\n    ].join('\\n');\n  }\n\n  /**\n   * Creates a swatch preview element for displaying the selected item\n   * inline below the search field.\n   * @param {string} type - 'fabric' or 'trim'\n   * @returns {HTMLElement} The swatch preview container\n   */\n  function createSwatchPreview(type) {\n    var container = createElement('div', {\n      className: PREFIX + '__swatch-preview',\n      attrs: { id: PREFIX + '-swatch-' + type }\n    });\n\n    var img = createElement('img', {\n      className: PREFIX + '__swatch-image',\n      attrs: { id: PREFIX + '-swatch-image-' + type, alt: '' }\n    });\n\n    var infoWrap = createElement('div', {\n      attrs: { style: 'display: flex; align-items: center; gap: 0; flex: 1;' }\n    });\n\n    var price = createElement('span', {\n      className: PREFIX + '__swatch-price',\n      attrs: { id: PREFIX + '-swatch-price-' + type }\n    });\n\n    var clearBtn = createElement('button', {\n      className: PREFIX + '__swatch-clear',\n      attrs: {\n        type: 'button',\n        id: PREFIX + '-swatch-clear-' + type,\n        title: 'Clear selection',\n        'aria-label': 'Clear ' + type + ' selection'\n      },\n      html: '&times;'\n    });\n\n    infoWrap.appendChild(price);\n    infoWrap.appendChild(clearBtn);\n    container.appendChild(img);\n    container.appendChild(infoWrap);\n\n    if (type === 'fabric') {\n      _els.fabricSwatch = container;\n      _els.fabricSwatchImage = img;\n      _els.fabricSwatchPrice = price;\n      _els.fabricSwatchClear = clearBtn;\n    } else {\n      _els.trimSwatch = container;\n      _els.trimSwatchImage = img;\n      _els.trimSwatchPrice = price;\n      _els.trimSwatchClear = clearBtn;\n    }\n\n    return container;\n  }\n\n  /* =========================================================================\n   * GRAPHQL - PRODUCT LOADING\n   * ========================================================================= */\n\n  /**\n   * Builds the GraphQL query for fetching products matching a Storefront API\n   * search string. Used both for live search-as-you-type (with a term) and for\n   * the bounded legacy preload (scope only). Queries all six known metafield\n   * namespace/key combinations for fabric width.\n   * @returns {string} The GraphQL query string\n   */\n  function buildProductsSearchQuery() {\n    var productFields = [\n      '        id title productType vendor availableForSale tags',\n      '        widthCustom: metafield(namespace: \"custom\", key: \"width\") { value }',\n      '        variants(first: 100) {',\n      '          edges {',\n      '            node {',\n      '              id title price { amount } availableForSale',\n      '              image {',\n      '                url(transform: { maxWidth: 800, maxHeight: 800 })',\n      '                altText',\n      '              }',\n      '            }',\n      '          }',\n      '        }',\n      '        images(first: 1) {',\n      '          edges { node { url(transform: { maxWidth: 800, maxHeight: 800 }) altText } }',\n      '        }'\n    ].join('\\n');\n\n    return [\n      'query searchProducts($first: Int!, $query: String!) {',\n      '  products(first: $first, query: $query) {',\n      '    edges {',\n      '      node {',\n      productFields,\n      '      }',\n      '    }',\n      '  }',\n      '}'\n    ].join('\\n');\n  }\n\n  /**\n   * Builds the GraphQL query for fetching a single product's width metafield\n   * and variant IDs. Used when a fabric is selected from the catalog popup.\n   * @returns {string} The GraphQL query string\n   */\n  function buildSingleProductQuery() {\n    return [\n      'query getProduct($id: ID!) {',\n      '  product(id: $id) {',\n      '    tags vendor productType',\n      '    widthCustom: metafield(namespace: \"custom\", key: \"width\") { value }',\n      '    variants(first: 1) {',\n      '      edges { node { id } }',\n      '    }',\n      '  }',\n      '}'\n    ].join('\\n');\n  }\n\n  /**\n   * Resolves fabric width from an array of metafield values. Returns the first\n   * non-null numeric value, or the default width.\n   * @param {Array<string|null>} sources - Array of metafield value strings\n   * @returns {number} Resolved fabric width in inches\n   */\n  function resolveWidth(sources) {\n    for (var i = 0; i < sources.length; i++) {\n      if (sources[i] != null) {\n        var parsed = parseFloat(sources[i]);\n        if (!isNaN(parsed) && parsed > 0) return parsed;\n      }\n    }\n    return DEFAULT_FABRIC_WIDTH;\n  }\n\n  /**\n   * Extracts width metafield sources from a product node.\n   * @param {Object} node - GraphQL product node\n   * @returns {Array<string|null>} Array of metafield values\n   */\n  function extractWidthSources(node) {\n    /* Only custom.width survives. The theme previously queried width from five\n     * further namespaces (fabric, textiles, category, and two unnamespaced\n     * keys). None of them has a metafield definition on this store, and the\n     * Storefront API returns null for any metafield without one, so all five\n     * were guaranteed dead on every request -- verified 2026-08-31 against a\n     * sample of fabrics: custom.width 10/10 populated, the others 0/10. They\n     * are removed rather than kept \"just in case\" because every one of them\n     * added a field to a query that runs on each keystroke of the fabric\n     * search. The array shape is retained so callers are unaffected. */\n    return [\n      node.widthCustom ? node.widthCustom.value : null\n    ];\n  }\n\n  /**\n   * Extracts a variant ID number from a full Shopify GID string.\n   * @param {string} fullId - e.g., \"gid://shopify/ProductVariant/123456\"\n   * @returns {string} Numeric variant ID, e.g., \"123456\"\n   */\n  function extractVariantId(fullId) {\n    var str = String(fullId);\n    return str.indexOf('/') !== -1 ? str.split('/').pop() : str;\n  }\n\n  /**\n   * Maps a product node and its variants into an array of selectable items.\n   * @param {Object} node - GraphQL product node\n   * @param {boolean} includeFabricWidth - Whether to include fabricWidth property\n   * @returns {Array<Object>} Array of product items\n   */\n  /** @type {Object<string, boolean>} Zero-priced variants already reported this session */\n  var _zeroPricedReported = {};\n\n  /**\n   * Records a catalogue item that was excluded for having no price, once per\n   * variant per session. These are data errors in Shopify (a fabric with a\n   * $0.00 price), not free products; they are dropped from every result set.\n   * @param {string} title - Display title of the offending item\n   * @param {string} variantId - Numeric Shopify variant ID\n   */\n  function reportZeroPriced(title, variantId) {\n    if (_zeroPricedReported[variantId]) return;\n    _zeroPricedReported[variantId] = true;\n    console.warn(\n      '[TRHFabricBrowser] Excluded zero-priced catalogue item (data error, ' +\n      'not a free product): \"' + title + '\" variant ' + variantId\n    );\n    dispatchEvent('trh:zero-priced-item-excluded', { title: title, variantId: variantId });\n  }\n\n  function mapVariantsToItems(node, includeFabricWidth) {\n    /* 2026-10-26: price-upon-request items (hide-price tag) are quoted, never\n     * sold through a calculator, even if a placeholder price is stored. */\n    if ((node.tags || []).some(function (t) { return String(t).toLowerCase().trim() === 'hide-price'; })) {\n      reportZeroPriced(node.title, 'por-' + node.id);\n      return [];\n    }\n    var mainImage = node.images.edges[0] ? node.images.edges[0].node : null;\n    var fabricWidth = includeFabricWidth ? resolveWidth(extractWidthSources(node)) : null;\n\n    return node.variants.edges\n      .filter(function (v) { return v.node.availableForSale; })\n      .map(function (v) {\n        var variantTitle = v.node.title && v.node.title !== node.title ? v.node.title : '';\n        var displayTitle = variantTitle ? node.title + ' - ' + variantTitle : node.title;\n\n        var variantImage = v.node.image;\n        var imageUrl = (variantImage ? variantImage.url : null) || (mainImage ? mainImage.url : null);\n        var altText = (variantImage ? variantImage.altText : null) ||\n          (mainImage ? mainImage.altText : null) || node.title;\n\n        if (!imageUrl) return null;\n\n        var price = parseFloat(v.node.price.amount);\n\n        /* Data-quality guard: a fabric or trim priced 0 is a catalogue error,\n         * not a free product. Selecting one would add its yardage to the cart\n         * at $0, so it must never be offered. Reported once per session so the\n         * bad data surfaces instead of failing silently. */\n        if (!(price > 0)) {\n          reportZeroPriced(displayTitle, extractVariantId(v.node.id));\n          return null;\n        }\n\n        var item = {\n          id: extractVariantId(v.node.id),\n          title: displayTitle,\n          price: price,\n          imageUrl: imageUrl,\n          altText: altText,\n          tags: node.tags || [],\n          productType: node.productType || '',\n          vendor: node.vendor || ''\n        };\n        /* TRH fabric min fix 2026-10-26 */\n        var _fm = window.TRHFabricMin.fromTags(item.tags, item.vendor, item.productType);\n        item.minQty = _fm.min;\n        item.unit = _fm.unit;\n        window.TRHFabricMin.register(item);\n\n        if (includeFabricWidth) {\n          item.fabricWidth = fabricWidth;\n        }\n\n        return item;\n      })\n      .filter(Boolean);\n  }\n\n  /**\n   * Executes a Storefront API product search and maps the results into\n   * selectable items.\n   * @param {string} searchString - Storefront API search query string\n   * @param {number} first - Maximum product nodes to fetch (Shopify cap: 250)\n   * @param {boolean} includeFabricWidth - Whether items carry fabricWidth\n   * @returns {Promise<Array<Object>>} Mapped items (empty array on no results)\n   * @throws {Error} On network failure or unexpected API response\n   */\n  async function executeProductSearch(searchString, first, includeFabricWidth) {\n    var response = await fetch(\n      'https://' + _config.storeDomain + '/api/' + API_VERSION + '/graphql.json',\n      {\n        method: 'POST',\n        headers: {\n          'Content-Type': 'application/json',\n          'X-Shopify-Storefront-Access-Token': _config.storefrontToken\n        },\n        body: JSON.stringify({\n          query: buildProductsSearchQuery(),\n          variables: { first: first, query: searchString }\n        })\n      }\n    );\n\n    if (!response.ok) {\n      throw new Error('Storefront API HTTP ' + response.status);\n    }\n\n    var data = await response.json();\n\n    if (!data.data || !data.data.products) {\n      throw new Error('Unexpected Storefront API response: ' + JSON.stringify(data.errors || data).slice(0, 200));\n    }\n\n    var items = [];\n    data.data.products.edges.forEach(function (edge) {\n      items = items.concat(mapVariantsToItems(edge.node, includeFabricWidth));\n    });\n    return items;\n  }\n\n  /**\n   * Escapes a user-supplied search term for safe embedding in a Storefront\n   * API search query string. Strips characters that carry query syntax\n   * meaning (quotes, backslashes, colons, parentheses, wildcards, dashes).\n   * @param {string} term - Raw user input\n   * @returns {string[]} Array of cleaned search words (may be empty)\n   */\n  function sanitizeSearchWords(term) {\n    return String(term)\n      .replace(/[\"'\\\\:()*-]/g, ' ')\n      .split(/\\s+/)\n      .filter(function (w) { return w.length > 0; })\n      .slice(0, 8); /* Cap word count to keep the query string bounded */\n  }\n\n  /**\n   * Searches the FULL product inventory of the given type server-side via the\n   * Storefront API. Matches each word as a title prefix (e.g. \"stri\" matches\n   * \"Rustic Stripe Fabric\") and falls back to Shopify relevance search so\n   * tag/description matches also surface. Results with the term in the title\n   * are ranked first. Results are cached per (type, term).\n   *\n   * @param {string} term - Raw user search input\n   * @param {string} type - 'fabric' or 'trim'\n   * @param {number} [limit=MAX_SUGGESTIONS] - Maximum items to return\n   * @returns {Promise<Array<Object>>} Matching items, best matches first\n   * @throws {Error} On network/API failure (callers show an error state)\n   */\n  async function searchProductsRemote(term, type, limit) {\n    limit = limit || MAX_SUGGESTIONS;\n    var words = sanitizeSearchWords(term);\n    if (words.length === 0) return [];\n\n    var normalizedTerm = normalizeForSearch(words.join(' '));\n    var cacheKey = type + '|' + limit + '|' + normalizedTerm;\n    if (_searchCache.has(cacheKey)) {\n      return _searchCache.get(cacheKey);\n    }\n\n    var scope = scopeFor(type);\n\n    /* Three query shapes, precision first. Colour and pattern now exist as\n     * color-* / pattern-* product tags, which Shopify matches in a bare-word\n     * search -- so a single blended query lets thousands of tag matches crowd\n     * out the handful of genuine title matches before ranking ever runs. The\n     * fix is to ask for them separately and merge.\n     *\n     *   1. TITLE  title:rustic* AND title:stri*\n     *      Guarantees named products (\"Rustic Stripe Fabric\") are in the pool.\n     *   2. ATTRIBUTE  rustic AND stri\n     *      Words AND-ed, so \"blue velvet\" means blue AND velvet rather than\n     *      anything velvet-ish in any colour.\n     *   3. LOOSE  rustic stri   (only if 1 and 2 came back thin)\n     *      Shopify's own relevance match, preserving recall for odd phrasings.\n     */\n    var titleQuery = '(' + words.map(function (w) { return 'title:' + w + '*'; }).join(' AND ') +\n      ') AND (' + scope + ')';\n    var attrQuery = '(' + words.join(' AND ') + ') AND (' + scope + ')';\n\n    /* Run the two precise queries together: same wall-clock as one. A failure\n     * in either must not lose the other's results. */\n    var pair = await Promise.all([\n      executeProductSearch(titleQuery, SEARCH_FETCH_COUNT, type === 'fabric')\n        .catch(function () { return []; }),\n      executeProductSearch(attrQuery, SEARCH_FETCH_COUNT, type === 'fabric')\n        .catch(function () { return []; })\n    ]);\n\n    var merged = [];\n    var seen = Object.create(null);\n    function absorb(list) {\n      list.forEach(function (item) {\n        if (seen[item.id]) return;\n        seen[item.id] = true;\n        merged.push(item);\n      });\n    }\n    absorb(pair[0]);\n    absorb(pair[1]);\n\n    if (merged.length < limit) {\n      var loose = await executeProductSearch(\n        '(' + words.join(' ') + ') AND (' + scope + ')',\n        SEARCH_FETCH_COUNT, type === 'fabric'\n      ).catch(function () { return []; });\n      absorb(loose);\n    }\n\n    /* Owner's ranking decision (2026-08-31): best match first, regardless of\n     * whose brand it is. A product whose *title* contains every typed word is\n     * a stronger match than one matching only through a colour or pattern tag,\n     * so title hits lead; brand and price play no part in the ordering. */\n    var titleMatches = [];\n    var otherMatches = [];\n    merged.forEach(function (item) {\n      var haystack = normalizeForSearch(item.title);\n      var isTitleHit = words.every(function (w) {\n        return haystack.indexOf(normalizeForSearch(w)) !== -1;\n      });\n      (isTitleHit ? titleMatches : otherMatches).push(item);\n    });\n\n    var ranked = titleMatches.concat(otherMatches).slice(0, limit);\n\n    /* Bounded cache: reset when full (simple + sufficient for a session) */\n    if (_searchCache.size >= SEARCH_CACHE_MAX) _searchCache.clear();\n    _searchCache.set(cacheKey, ranked);\n\n    return ranked;\n  }\n\n\n  /**\n   * Preloads a bounded convenience cache of fabrics and trims (first\n   * MAX_PRODUCTS of each type). This feeds the legacy getAllFabrics() /\n   * getAllTrims() API and the trh:products-loaded event. It is NOT the search\n   * data source \u2014 search always queries the full inventory server-side via\n   * searchProductsRemote().\n   * @returns {Promise<void>}\n   */\n  async function loadProducts() {\n    if (_loadingProducts) return _loadingPromise;\n    if (!_config) {\n      console.error('[TRHFabricBrowser] Cannot load products: not initialized.');\n      return Promise.resolve();\n    }\n\n    _loadingProducts = true;\n    _loadingPromise = _doLoadProducts();\n    return _loadingPromise;\n  }\n\n  async function _doLoadProducts() {\n    try {\n      var results = await Promise.all([\n        executeProductSearch(scopeFor('fabric'), MAX_PRODUCTS, true),\n        executeProductSearch(scopeFor('trim'), MAX_PRODUCTS, false)\n      ]);\n\n      _allFabrics = results[0];\n      _allTrims = results[1];\n      _productsLoaded = true;\n\n      console.log(\n        '[TRHFabricBrowser] Preloaded ' + _allFabrics.length + ' fabrics, ' +\n        _allTrims.length + ' trims (bounded cache; search queries full inventory)'\n      );\n\n      dispatchEvent('trh:products-loaded', {\n        fabricCount: _allFabrics.length,\n        trimCount: _allTrims.length\n      });\n    } catch (err) {\n      console.error('[TRHFabricBrowser] loadProducts error:', err);\n    } finally {\n      _loadingProducts = false;\n    }\n  }\n\n  /**\n   * Fetches the width metafield and variant ID for a single product.\n   * Used when a fabric is selected from the catalog popup and we need\n   * metafield data that wasn't included in the postMessage payload.\n   * @param {string} productId - Numeric Shopify product ID\n   * @returns {Promise<Object>} Object with { fabricWidth, variantId }\n   */\n  async function fetchProductMetafields(productId) {\n    var result = { fabricWidth: DEFAULT_FABRIC_WIDTH, variantId: null, tags: null, vendor: '', productType: '', notOffered: false };\n\n    try {\n      var response = await fetch(\n        'https://' + _config.storeDomain + '/api/' + API_VERSION + '/graphql.json',\n        {\n          method: 'POST',\n          headers: {\n            'Content-Type': 'application/json',\n            'X-Shopify-Storefront-Access-Token': _config.storefrontToken\n          },\n          body: JSON.stringify({\n            query: buildSingleProductQuery(),\n            variables: { id: 'gid://shopify/Product/' + productId }\n          })\n        }\n      );\n\n      if (!response.ok) return result;\n\n      var data = await response.json();\n      var product = data.data ? data.data.product : null;\n      /* 2026-10-26: product not on the Custom Calculator channel = not offered */\n      if (!product) { if (data.data) result.notOffered = true; return result; }\n\n      var widthSources = extractWidthSources(product);\n      result.fabricWidth = resolveWidth(widthSources);\n      result.tags = product.tags || [];\n      result.vendor = product.vendor || '';\n      result.productType = product.productType || '';\n\n      var variants = product.variants ? product.variants.edges : [];\n      if (variants.length > 0) {\n        result.variantId = extractVariantId(variants[0].node.id);\n      }\n    } catch (err) {\n      console.error('[TRHFabricBrowser] fetchProductMetafields error:', err);\n    }\n\n    return result;\n  }\n\n  /* =========================================================================\n   * SEARCH / SUGGESTIONS\n   * ========================================================================= */\n\n  /**\n   * Searches the full product inventory server-side for the typed term and\n   * renders matching suggestions into the specified dropdown container.\n   * A per-type sequence counter discards stale responses so an earlier,\n   * slower request can never overwrite the results of a newer keystroke.\n   * @param {string} term - Raw search input value\n   * @param {string} type - 'fabric' or 'trim'\n   * @returns {Promise<void>}\n   */\n  async function showSuggestions(term, type) {\n    var suggestionsEl = type === 'fabric'\n      ? document.getElementById(_config.fabricSuggestionsId)\n      : document.getElementById(_config.trimSuggestionsId);\n\n    if (!suggestionsEl) return;\n\n    suggestionsEl.innerHTML = '';\n\n    if (!term || term.trim() === '') {\n      suggestionsEl.style.display = 'none';\n      suggestionsEl.classList.remove(PREFIX + '__suggestions--active');\n      return;\n    }\n\n    var seq = ++_searchSeq[type];\n\n    /* Loading indicator while the remote search is in flight */\n    suggestionsEl.innerHTML = '<div class=\"' + PREFIX + '__no-results\">Searching\u2026</div>';\n    suggestionsEl.style.display = 'block';\n    suggestionsEl.classList.add(PREFIX + '__suggestions--active');\n\n    var matches;\n    try {\n      matches = await searchProductsRemote(term, type, MAX_SUGGESTIONS);\n    } catch (err) {\n      console.error('[TRHFabricBrowser] Search failed:', err);\n      if (seq !== _searchSeq[type]) return; /* A newer search superseded this one */\n      suggestionsEl.innerHTML = '<div class=\"' + PREFIX + '__no-results\">' +\n        'Search is temporarily unavailable. Please try again.</div>';\n      return;\n    }\n\n    /* Discard stale responses (user kept typing while this request ran) */\n    if (seq !== _searchSeq[type]) return;\n\n    /* Only report searches the user actually saw a result for -- superseded\n     * keystrokes returned above, so this fires once per settled search. */\n    reportSearch(term, type, matches.length);\n\n    suggestionsEl.innerHTML = '';\n\n    if (matches.length === 0) {\n      suggestionsEl.innerHTML = '<div class=\"' + PREFIX + '__no-results\">No ' +\n        type + 's found</div>';\n    } else {\n      matches.forEach(function (item) {\n        var row = createElement('div', { className: PREFIX + '__suggestion' });\n\n        var img = createElement('img', {\n          className: PREFIX + '__suggestion-image',\n          attrs: {\n            src: item.imageUrl,\n            alt: escapeHtml(item.altText),\n            loading: 'lazy'\n          }\n        });\n\n        var details = createElement('div', { className: PREFIX + '__suggestion-details' });\n        var name = createElement('div', {\n          className: PREFIX + '__suggestion-name',\n          text: item.title\n        });\n        var price = createElement('div', {\n          className: PREFIX + '__suggestion-price',\n          text: '$' + item.price.toFixed(2) + window.TRHFabricMin.per(item)\n        });\n\n        details.appendChild(name);\n        details.appendChild(price);\n        row.appendChild(img);\n        row.appendChild(details);\n\n        row.addEventListener('click', function (e) {\n          if (e.target.classList.contains(PREFIX + '__suggestion-image')) {\n            showZoomModal(item, type, matches);\n          } else {\n            type === 'fabric' ? selectFabric(item) : selectTrim(item);\n          }\n        });\n\n        suggestionsEl.appendChild(row);\n      });\n    }\n\n    suggestionsEl.style.display = 'block';\n    suggestionsEl.classList.add(PREFIX + '__suggestions--active');\n  }\n\n  /* =========================================================================\n   * ZOOM MODAL\n   * ========================================================================= */\n\n  /**\n   * Opens the zoom modal to display a full-size image of the given item.\n   * When searchList is provided, enables previous/next navigation and\n   * select/choose-another buttons.\n   * @param {Object} item - Product item with imageUrl, title, price\n   * @param {string} type - 'fabric' or 'trim'\n   * @param {Array<Object>|null} searchList - Navigable list, or null for swatch-only view\n   */\n  function showZoomModal(item, type, searchList) {\n    _zoomType = type;\n    var items = searchList || (type === 'fabric' ? _allFabrics : _allTrims);\n    _currentSearchResults = items;\n    var idx = -1;\n    for (var i = 0; i < items.length; i++) {\n      if (items[i].id === item.id) { idx = i; break; }\n    }\n\n    _els.zoomImage.src = item.imageUrl;\n    _els.zoomImage.alt = item.altText || item.title;\n    _els.zoomTitle.textContent = item.title;\n    _els.zoomPrice.textContent = '$' + item.price.toFixed(2) + window.TRHFabricMin.per(item);\n\n    _els.zoomOverlay.classList.add(PREFIX + '__zoom-overlay--visible');\n\n    // Store current item data for selection\n    _els.zoomSelectBtn._currentItem = item;\n    _els.zoomPrevBtn._currentIndex = idx;\n    _els.zoomNextBtn._currentIndex = idx;\n\n    if (searchList === null) {\n      _els.zoomPrevBtn.style.display = 'none';\n      _els.zoomNextBtn.style.display = 'none';\n      _els.zoomSelectBtn.style.display = 'none';\n      _els.zoomChooseBtn.style.display = 'none';\n    } else {\n      _els.zoomPrevBtn.style.display = 'block';\n      _els.zoomNextBtn.style.display = 'block';\n      _els.zoomSelectBtn.style.display = 'inline-block';\n      _els.zoomChooseBtn.style.display = 'inline-block';\n\n      _els.zoomPrevBtn.disabled = (idx <= 0);\n      _els.zoomNextBtn.disabled = (idx >= items.length - 1);\n    }\n  }\n\n  /**\n   * Closes the zoom modal. Optionally flags a \"choose another\" state to\n   * prevent the suggestions dropdown from closing on the same click.\n   * @param {boolean} [chooseAnother=false] - Whether user wants to choose another item\n   */\n  function closeZoomModal(chooseAnother) {\n    _els.zoomOverlay.classList.remove(PREFIX + '__zoom-overlay--visible');\n    _isChoosingAnother = !!chooseAnother;\n    if (chooseAnother) {\n      setTimeout(function () { _isChoosingAnother = false; }, 100);\n    }\n    _zoomType = null;\n    _currentSearchResults = [];\n  }\n\n  /**\n   * Handles the \"Select This\" button in the zoom modal - selects the current\n   * item and closes both the zoom and catalog modals.\n   */\n  function selectFromZoom() {\n    var item = _els.zoomSelectBtn._currentItem;\n    if (!item) return;\n\n    var typeToClose = _zoomType;\n    if (_zoomType === 'fabric') {\n      selectFabric(item);\n    } else {\n      selectTrim(item);\n    }\n    closeZoomModal();\n    closeCatalogModal(typeToClose);\n  }\n\n  /**\n   * Navigates the zoom modal to a different item in the search results.\n   * @param {number} direction - -1 for previous, +1 for next\n   */\n  function navigateZoom(direction) {\n    var currentIdx = direction === -1\n      ? _els.zoomPrevBtn._currentIndex\n      : _els.zoomNextBtn._currentIndex;\n\n    var items = _currentSearchResults;\n    var newIdx = currentIdx + direction;\n\n    if (newIdx < 0 || newIdx >= items.length) return;\n\n    showZoomModal(items[newIdx], _zoomType, _currentSearchResults);\n  }\n\n  /* =========================================================================\n   * CATALOG BROWSER (POPUP)\n   * ========================================================================= */\n\n  /**\n   * Opens the Shopify collection page in a popup window. The collection page\n   * already has selection code (main-collection-product-grid.liquid) that adds\n   * \"Select\" buttons and sends postMessage back when a product is chosen.\n   * @param {string} type - 'fabric' or 'trim'\n   */\n  function showCatalog(type) {\n    var domain = _config.storefrontDomain || _config.storeDomain;\n    /* NOTE: the fabric collection handle changed from 'fabric-1' to 'fabric'\n     * when the catalog was reorganized (verified against the live store \u2014\n     * collection(handle:\"fabric-1\") now returns null). */\n    var collection = type === 'fabric' ? (_config.fabricCollection || 'fabric') : (_config.trimCollection || 'trim');\n    var productLabel = _config.productLabel || 'Calculator';\n    var url = 'https://' + domain + '/collections/' + collection +\n      '?calculator=true&calculatorType=' + type + '&productLabel=' + encodeURIComponent(productLabel);\n\n    var popup = window.open(url, 'trh_' + type + '_catalog',\n      'width=1200,height=900,resizable=yes,scrollbars=yes');\n\n    if (!popup || popup.closed) {\n      alert('Please allow popups for this site to browse the ' + type + ' collection.');\n    }\n  }\n\n  /**\n   * Closes the catalog modal (no-op for popup window approach; the popup\n   * closes itself after selection via window.close()).\n   * @param {string} type - 'fabric' or 'trim'\n   */\n  function closeCatalogModal(type) {\n    /* Popup windows close themselves after postMessage. Nothing to do here. */\n  }\n\n  /* =========================================================================\n   * SELECTION MANAGEMENT\n   * ========================================================================= */\n\n  /**\n   * Updates the visibility of the clear button on a swatch preview.\n   * @param {string} type - 'fabric' or 'trim'\n   */\n  function updateSwatchClearVisibility(type) {\n    var clearBtn = type === 'fabric' ? _els.fabricSwatchClear : _els.trimSwatchClear;\n    var selected = type === 'fabric' ? _selectedFabric : _selectedTrim;\n\n    if (!clearBtn) return;\n\n    if (selected) {\n      clearBtn.classList.add(PREFIX + '__swatch-clear--visible');\n    } else {\n      clearBtn.classList.remove(PREFIX + '__swatch-clear--visible');\n    }\n  }\n\n  /**\n   * Selects a fabric item. Updates the search input field, shows the swatch\n   * preview, hides suggestions, and fires the selection callback and custom event.\n   * @param {Object} fabric - Fabric item object\n   * @param {string} fabric.id - Variant ID\n   * @param {string} fabric.title - Display title\n   * @param {number} fabric.price - Price per yard\n   * @param {string} fabric.imageUrl - Thumbnail image URL\n   * @param {string} fabric.altText - Image alt text\n   * @param {number} [fabric.fabricWidth] - Fabric width in inches\n   */\n  function selectFabric(fabric) {\n    if (blockedByScope('fabric', fabric)) return;\n    _selectedFabric = fabric;\n\n    var field = document.getElementById(_config.fabricFieldId);\n    if (field) {\n      field.value = fabric.title + ' ($' + fabric.price.toFixed(2) + window.TRHFabricMin.per(fabric) + ')';\n      field.classList.add(PREFIX + '__search-input--selected');\n    }\n\n    if (_els.fabricSwatchImage) {\n      _els.fabricSwatchImage.src = fabric.imageUrl;\n      _els.fabricSwatchImage.alt = fabric.altText || fabric.title;\n      _els.fabricSwatchImage.classList.add(PREFIX + '__swatch-image--clickable');\n      _els.fabricSwatchImage.onclick = function () {\n        showZoomModal(fabric, 'fabric', null);\n      };\n    }\n\n    if (_els.fabricSwatchPrice) {\n      _els.fabricSwatchPrice.textContent = '$' + fabric.price.toFixed(2) + window.TRHFabricMin.per(fabric);\n    }\n\n    if (_els.fabricSwatch) {\n      _els.fabricSwatch.classList.add(PREFIX + '__swatch-preview--visible');\n    }\n\n    var suggestionsEl = document.getElementById(_config.fabricSuggestionsId);\n    if (suggestionsEl) {\n      suggestionsEl.style.display = 'none';\n      suggestionsEl.classList.remove(PREFIX + '__suggestions--active');\n    }\n\n    updateSwatchClearVisibility('fabric');\n\n    if (typeof _config.onFabricSelect === 'function') {\n      _config.onFabricSelect(fabric);\n    }\n\n    dispatchEvent('trh:fabric-selected', { fabric: fabric });\n\n    console.log('[TRHFabricBrowser] Fabric selected:', fabric.title);\n  }\n\n  /**\n   * Selects a trim item. Updates the search input field, shows the swatch\n   * preview, hides suggestions, and fires the selection callback and custom event.\n   * @param {Object} trim - Trim item object\n   * @param {string} trim.id - Variant ID\n   * @param {string} trim.title - Display title\n   * @param {number} trim.price - Price per yard\n   * @param {string} trim.imageUrl - Thumbnail image URL\n   * @param {string} trim.altText - Image alt text\n   */\n  function selectTrim(trim) {\n    if (blockedByScope('trim', trim)) return;\n    _selectedTrim = trim;\n\n    var field = document.getElementById(_config.trimFieldId);\n    if (field) {\n      field.value = trim.title + ' ($' + trim.price.toFixed(2) + '/yd)';\n      field.classList.add(PREFIX + '__search-input--selected');\n    }\n\n    if (_els.trimSwatchImage) {\n      _els.trimSwatchImage.src = trim.imageUrl;\n      _els.trimSwatchImage.alt = trim.altText || trim.title;\n      _els.trimSwatchImage.classList.add(PREFIX + '__swatch-image--clickable');\n      _els.trimSwatchImage.onclick = function () {\n        showZoomModal(trim, 'trim', null);\n      };\n    }\n\n    if (_els.trimSwatchPrice) {\n      _els.trimSwatchPrice.textContent = '$' + trim.price.toFixed(2) + '/yd';\n    }\n\n    if (_els.trimSwatch) {\n      _els.trimSwatch.classList.add(PREFIX + '__swatch-preview--visible');\n    }\n\n    var suggestionsEl = document.getElementById(_config.trimSuggestionsId);\n    if (suggestionsEl) {\n      suggestionsEl.style.display = 'none';\n      suggestionsEl.classList.remove(PREFIX + '__suggestions--active');\n    }\n\n    updateSwatchClearVisibility('trim');\n\n    if (typeof _config.onTrimSelect === 'function') {\n      _config.onTrimSelect(trim);\n    }\n\n    dispatchEvent('trh:trim-selected', { trim: trim });\n\n    console.log('[TRHFabricBrowser] Trim selected:', trim.title);\n  }\n\n  /**\n   * Clears the current fabric or trim selection. Resets the search field,\n   * hides the swatch preview, and fires the clear callback and custom event.\n   * @param {string} type - 'fabric' or 'trim'\n   */\n  function clearSelection(type) {\n    if (type === 'fabric') {\n      _selectedFabric = null;\n\n      var fabricField = document.getElementById(_config.fabricFieldId);\n      if (fabricField) {\n        fabricField.value = '';\n        fabricField.classList.remove(PREFIX + '__search-input--selected');\n      }\n\n      if (_els.fabricSwatch) {\n        _els.fabricSwatch.classList.remove(PREFIX + '__swatch-preview--visible');\n      }\n\n      var fabricSuggestions = document.getElementById(_config.fabricSuggestionsId);\n      if (fabricSuggestions) {\n        fabricSuggestions.style.display = 'none';\n        fabricSuggestions.classList.remove(PREFIX + '__suggestions--active');\n      }\n\n      updateSwatchClearVisibility('fabric');\n\n      if (typeof _config.onFabricClear === 'function') {\n        _config.onFabricClear();\n      }\n\n      forgetPreselect('fabric');\n      dispatchEvent('trh:fabric-cleared');\n\n      console.log('[TRHFabricBrowser] Fabric selection cleared.');\n    } else {\n      _selectedTrim = null;\n\n      var trimField = document.getElementById(_config.trimFieldId);\n      if (trimField) {\n        trimField.value = '';\n        trimField.classList.remove(PREFIX + '__search-input--selected');\n      }\n\n      if (_els.trimSwatch) {\n        _els.trimSwatch.classList.remove(PREFIX + '__swatch-preview--visible');\n      }\n\n      var trimSuggestions = document.getElementById(_config.trimSuggestionsId);\n      if (trimSuggestions) {\n        trimSuggestions.style.display = 'none';\n        trimSuggestions.classList.remove(PREFIX + '__suggestions--active');\n      }\n\n      updateSwatchClearVisibility('trim');\n\n      if (typeof _config.onTrimClear === 'function') {\n        _config.onTrimClear();\n      }\n\n      forgetPreselect('trim');\n      dispatchEvent('trh:trim-cleared');\n\n      console.log('[TRHFabricBrowser] Trim selection cleared.');\n    }\n  }\n\n  /* =========================================================================\n   * POSTMESSAGE HANDLER (catalog popup communication)\n   * ========================================================================= */\n\n  /**\n   * Handles incoming postMessage events from catalog popup windows. Processes\n   * 'fabricSelected' and 'trimSelected' message types, fetches additional\n   * metafield data when needed, and applies the selection.\n   * @param {MessageEvent} event - The message event\n   */\n  async function handlePostMessage(event) {\n    /* Only accept selection messages from our own storefront pages (the\n     * catalog popup is opened on the same store domain). Without this check\n     * any window with a reference to this page could inject a fabric\n     * \"selection\" with an arbitrary price into the calculator. */\n    var trustedOrigins = [\n      window.location.origin,\n      /* The store is reachable on both domains; the popup may be on either\n       * one regardless of which domain this page is served from. */\n      'https://therebornhouse.com',\n      'https://www.therebornhouse.com'\n    ];\n    if (_config) {\n      trustedOrigins.push('https://' + _config.storeDomain);\n      if (_config.storefrontDomain) {\n        trustedOrigins.push('https://' + _config.storefrontDomain);\n      }\n    }\n    if (trustedOrigins.indexOf(event.origin) === -1) return;\n\n    var data = event.data;\n    if (!data || typeof data !== 'object') return;\n\n    /* Catalog iframe selections (blob URL iframe) */\n    if (data.type === 'trhCatalogSelect' && data.item) {\n      /* 2026-10-26: same price-upon-request guard as the popup path */\n      if (isNotSellable(data.item)) {\n        closeCatalogModal(data.itemType);\n        routeToInquiry(data.item, data.itemType === 'fabric' ? 'fabric' : 'trim');\n        return;\n      }\n      if (data.itemType === 'fabric') {\n        selectFabric(data.item);\n      } else {\n        selectTrim(data.item);\n      }\n      closeCatalogModal(data.itemType);\n      return;\n    }\n\n    if (data.type === 'trhCatalogClose') {\n      closeCatalogModal('fabric');\n      closeCatalogModal('trim');\n      return;\n    }\n\n    /* Legacy popup window selections */\n    if (data.type === 'fabricSelected' && data.fabricData) {\n      await handleCatalogFabricSelection(data.fabricData);\n    }\n\n    if (data.type === 'trimSelected' && data.trimData) {\n      await handleCatalogTrimSelection(data.trimData);\n    }\n  }\n\n  /**\n   * Processes a fabric selection received from the catalog popup window.\n   * Fetches width metafield data from the API if a product ID is provided.\n   * @param {Object} fabricData - Data from the collection page postMessage\n   * @param {string} [fabricData.productId] - Shopify product ID\n   * @param {string} [fabricData.variantId] - Shopify variant ID\n   * @param {string} fabricData.title - Product title\n   * @param {string} [fabricData.variant] - Variant title\n   * @param {number} [fabricData.price] - Price per yard\n   * @param {string} [fabricData.imageUrl] - Product image URL\n   * @param {number} [fabricData.fabricWidth] - Pre-resolved fabric width\n   */\n  /**\n   * 2026-10-26: a price-upon-request (or $0 / not-offered) fabric picked from\n   * the catalog popup is never added to the cart. Explain and offer the\n   * contact page instead.\n   */\n  function isNotSellable(item) {\n    if (!item) return true;\n    if (!(Number(item.price) > 0)) return true;\n    return (item.tags || []).some(function (t) { return String(t).toLowerCase().trim() === 'hide-price'; });\n  }\n\n  function routeToInquiry(fabricData, kind) {\n    var name = (fabricData && fabricData.title) || (kind === 'trim' ? 'This trim' : 'This fabric');\n    console.warn('[TRHFabricBrowser] Not selectable in calculator (price upon request):', name);\n    dispatchEvent('trh:por-fabric-blocked', { title: name, productId: fabricData && fabricData.productId });\n    var msg = name + ' is priced upon request, so it can\\u2019t be added through this calculator. ' +\n      'Would you like to contact us for a quote?';\n    if (window.confirm(msg)) {\n      window.open('/pages/contact?' + (kind === 'trim' ? 'trim' : 'fabric') + '=' + encodeURIComponent(name), '_blank');\n    }\n  }\n\n  async function handleCatalogFabricSelection(fabricData) {\n    var variantId = fabricData.variantId || fabricData.id || null;\n    var fabricWidth = DEFAULT_FABRIC_WIDTH;\n    var fmInfo = null;\n\n    if (!(Number(fabricData.price) > 0)) { routeToInquiry(fabricData); return; }\n    if (fabricData.productId) {\n      var meta = await fetchProductMetafields(fabricData.productId);\n      if (meta.notOffered || (meta.tags || []).some(function (t) { return String(t).toLowerCase().trim() === 'hide-price'; })) {\n        routeToInquiry(fabricData); return;\n      }\n      fabricWidth = meta.fabricWidth;\n      if (meta.tags) fmInfo = window.TRHFabricMin.fromTags(meta.tags, meta.vendor, meta.productType);\n      if (!variantId && meta.variantId) {\n        variantId = meta.variantId;\n      }\n      var catMeta = meta;\n    } else if (fabricData.fabricWidth) {\n      fabricWidth = fabricData.fabricWidth;\n    }\n\n    var displayTitle = fabricData.variant\n      ? fabricData.title + ' - ' + fabricData.variant\n      : fabricData.title;\n\n    var fabric = {\n      id: variantId,\n      title: displayTitle,\n      price: fabricData.price || 0,\n      imageUrl: fabricData.imageUrl || '',\n      altText: fabricData.title || '',\n      fabricWidth: fabricWidth\n    };\n    /* 2026-10-26 welt: keep type/tags so a contrast-welt pick can be checked */\n    if (typeof catMeta !== 'undefined' && catMeta) {\n      fabric.productType = catMeta.productType || '';\n      fabric.tags = catMeta.tags || [];\n      fabric.vendor = catMeta.vendor || '';\n    }\n    if (fmInfo) {\n      fabric.minQty = fmInfo.min;\n      fabric.unit = fmInfo.unit;\n      window.TRHFabricMin.register(fabric);\n    }\n\n    selectFabric(fabric);\n    closeCatalogModal('fabric');\n  }\n\n  /**\n   * Processes a trim selection received from the catalog popup window.\n   * Fetches the variant ID from the API if not provided in the postMessage.\n   * @param {Object} trimData - Data from the collection page postMessage\n   * @param {string} [trimData.productId] - Shopify product ID\n   * @param {string} [trimData.variantId] - Shopify variant ID\n   * @param {string} trimData.title - Product title\n   * @param {string} [trimData.variant] - Variant title\n   * @param {number} [trimData.price] - Price per yard\n   * @param {string} [trimData.imageUrl] - Product image URL\n   */\n  async function handleCatalogTrimSelection(trimData) {\n    var variantId = trimData.variantId || trimData.id || null;\n\n    /* 2026-10-26: $0 / price-upon-request / not-offered trims are quoted, not added */\n    if (!(Number(trimData.price) > 0)) { closeCatalogModal('trim'); routeToInquiry(trimData, 'trim'); return; }\n    if (trimData.productId) {\n      var meta = await fetchProductMetafields(trimData.productId);\n      if (meta.notOffered || (meta.tags || []).some(function (t) { return String(t).toLowerCase().trim() === 'hide-price'; })) {\n        closeCatalogModal('trim'); routeToInquiry(trimData, 'trim'); return;\n      }\n      if (!variantId && meta.variantId) {\n        variantId = meta.variantId;\n      }\n    }\n\n    var displayTitle = trimData.variant\n      ? trimData.title + ' - ' + trimData.variant\n      : trimData.title;\n\n    var trim = {\n      id: variantId,\n      title: displayTitle,\n      price: trimData.price || 0,\n      imageUrl: trimData.imageUrl || '',\n      altText: trimData.title || ''\n    };\n    /* 2026-10-29: keep the trim's tags (as the fabric path does) so an opt-in\n     * trimRequireAnyTag scope (outdoor curtain) can see them */\n    if (typeof meta !== 'undefined' && meta && meta.tags) trim.tags = meta.tags;\n\n    selectTrim(trim);\n    closeCatalogModal('trim');\n  }\n\n  /* =========================================================================\n   * EVENT BINDING\n   * ========================================================================= */\n\n  /** @type {Array<Object>} Tracked event listeners for cleanup */\n  var _boundListeners = [];\n\n  /**\n   * Adds an event listener and tracks it for later cleanup.\n   * @param {EventTarget} target - The DOM element or object\n   * @param {string} event - Event name\n   * @param {Function} handler - Event handler\n   * @param {Object} [options] - addEventListener options\n   */\n  function addTrackedListener(target, event, handler, options) {\n    target.addEventListener(event, handler, options);\n    _boundListeners.push({ target: target, event: event, handler: handler, options: options });\n  }\n\n  /**\n   * Removes all tracked event listeners. Called during cleanup/destroy.\n   */\n  function removeAllListeners() {\n    _boundListeners.forEach(function (entry) {\n      entry.target.removeEventListener(entry.event, entry.handler, entry.options);\n    });\n    _boundListeners = [];\n  }\n\n  /**\n   * Binds all interactive event listeners for search inputs, dropdowns,\n   * modals, keyboard shortcuts, and postMessage communication.\n   */\n  function bindEvents() {\n    var fabricField = document.getElementById(_config.fabricFieldId);\n    var trimField = _config.trimFieldId ? document.getElementById(_config.trimFieldId) : null;\n    var fabricSuggestions = document.getElementById(_config.fabricSuggestionsId);\n    var trimSuggestions = _config.trimSuggestionsId\n      ? document.getElementById(_config.trimSuggestionsId)\n      : null;\n\n    // Debounced search handlers\n    if (fabricField) {\n      var debouncedFabricSearch = debounce(function (e) {\n        showSuggestions(e.target.value, 'fabric');\n      }, DEBOUNCE_MS);\n\n      addTrackedListener(fabricField, 'input', debouncedFabricSearch);\n    }\n\n    if (trimField && _config.showTrim) {\n      var debouncedTrimSearch = debounce(function (e) {\n        showSuggestions(e.target.value, 'trim');\n      }, DEBOUNCE_MS);\n\n      addTrackedListener(trimField, 'input', debouncedTrimSearch);\n    }\n\n    // Swatch clear buttons\n    if (_els.fabricSwatchClear) {\n      addTrackedListener(_els.fabricSwatchClear, 'click', function () {\n        clearSelection('fabric');\n      });\n    }\n\n    if (_els.trimSwatchClear) {\n      addTrackedListener(_els.trimSwatchClear, 'click', function () {\n        clearSelection('trim');\n      });\n    }\n\n    // Click-away to close suggestions\n    var clickAwayHandler = debounce(function (e) {\n      if (_isChoosingAnother || (e.target && e.target.closest && e.target.closest('.' + PREFIX + '__zoom-btn--secondary'))) {\n        return;\n      }\n\n      if (fabricField && fabricSuggestions) {\n        var isFabricArea = fabricField.contains(e.target) ||\n          fabricSuggestions.contains(e.target) ||\n          (_els.fabricSwatch && _els.fabricSwatch.contains(e.target));\n        if (!isFabricArea) {\n          fabricSuggestions.style.display = 'none';\n          fabricSuggestions.classList.remove(PREFIX + '__suggestions--active');\n        }\n      }\n\n      if (trimField && trimSuggestions) {\n        var isTrimArea = trimField.contains(e.target) ||\n          trimSuggestions.contains(e.target) ||\n          (_els.trimSwatch && _els.trimSwatch.contains(e.target));\n        if (!isTrimArea) {\n          trimSuggestions.style.display = 'none';\n          trimSuggestions.classList.remove(PREFIX + '__suggestions--active');\n        }\n      }\n    }, 50);\n\n    addTrackedListener(document, 'click', clickAwayHandler);\n\n    // Escape key handler\n    addTrackedListener(document, 'keydown', function (e) {\n      if (e.key === 'Escape') {\n        closeZoomModal();\n\n        if (fabricSuggestions) {\n          fabricSuggestions.style.display = 'none';\n          fabricSuggestions.classList.remove(PREFIX + '__suggestions--active');\n        }\n        if (trimSuggestions) {\n          trimSuggestions.style.display = 'none';\n          trimSuggestions.classList.remove(PREFIX + '__suggestions--active');\n        }\n\n        closeCatalogModal('fabric');\n        closeCatalogModal('trim');\n      }\n    });\n\n    // Zoom modal events\n    addTrackedListener(_els.zoomOverlay, 'click', function (e) {\n      if (e.target === _els.zoomOverlay) closeZoomModal();\n    });\n\n    addTrackedListener(_els.zoomCloseBtn, 'click', function () {\n      closeZoomModal();\n    });\n\n    addTrackedListener(_els.zoomSelectBtn, 'click', selectFromZoom);\n\n    addTrackedListener(_els.zoomChooseBtn, 'click', function () {\n      closeZoomModal(true);\n    });\n\n    addTrackedListener(_els.zoomPrevBtn, 'click', function () {\n      if (!this.disabled) navigateZoom(-1);\n    });\n\n    addTrackedListener(_els.zoomNextBtn, 'click', function () {\n      if (!this.disabled) navigateZoom(1);\n    });\n\n    // Catalog lightbox events (close on overlay background click)\n    if (_els.catalogFabricOverlay) {\n      addTrackedListener(_els.catalogFabricOverlay, 'click', function (e) {\n        if (e.target === _els.catalogFabricOverlay) closeCatalogModal('fabric');\n      });\n    }\n\n    if (_els.catalogTrimOverlay) {\n      addTrackedListener(_els.catalogTrimOverlay, 'click', function (e) {\n        if (e.target === _els.catalogTrimOverlay) closeCatalogModal('trim');\n      });\n    }\n\n    // PostMessage listener for catalog popup selections (legacy support)\n    addTrackedListener(window, 'message', handlePostMessage);\n  }\n\n  /* =========================================================================\n   * INITIALIZATION & CLEANUP\n   * ========================================================================= */\n\n  /**\n   * Injects all dynamically created DOM elements (modals, swatch previews)\n   * into the page. Elements are appended to document.body for modals, and\n   * adjacent to the search inputs for swatch previews.\n   */\n  function injectDOM() {\n    // Inject styles\n    injectStyles();\n\n    // Create and inject zoom modal\n    var zoomModal = createZoomModal();\n    document.body.appendChild(zoomModal);\n\n    // Create and inject catalog modals\n    var fabricCatalogModal = createCatalogModal('fabric');\n    document.body.appendChild(fabricCatalogModal);\n\n    if (_config.showTrim) {\n      var trimCatalogModal = createCatalogModal('trim');\n      document.body.appendChild(trimCatalogModal);\n    }\n\n    // Create and inject swatch previews next to search inputs\n    var fabricField = document.getElementById(_config.fabricFieldId);\n    if (fabricField && fabricField.parentNode) {\n      var fabricSwatch = createSwatchPreview('fabric');\n      // Insert after the suggestions container if it exists, otherwise after field\n      var fabricSuggestions = document.getElementById(_config.fabricSuggestionsId);\n      if (fabricSuggestions && fabricSuggestions.parentNode === fabricField.parentNode) {\n        fabricField.parentNode.insertBefore(fabricSwatch, fabricSuggestions.nextSibling);\n      } else {\n        fabricField.parentNode.appendChild(fabricSwatch);\n      }\n    }\n\n    if (_config.showTrim && _config.trimFieldId) {\n      var trimField = document.getElementById(_config.trimFieldId);\n      if (trimField && trimField.parentNode) {\n        var trimSwatch = createSwatchPreview('trim');\n        var trimSuggestions = document.getElementById(_config.trimSuggestionsId);\n        if (trimSuggestions && trimSuggestions.parentNode === trimField.parentNode) {\n          trimField.parentNode.insertBefore(trimSwatch, trimSuggestions.nextSibling);\n        } else {\n          trimField.parentNode.appendChild(trimSwatch);\n        }\n      }\n    }\n\n    // Add BEM class to suggestions containers\n    if (_config.fabricSuggestionsId) {\n      var fSug = document.getElementById(_config.fabricSuggestionsId);\n      if (fSug) fSug.classList.add(PREFIX + '__suggestions');\n    }\n    if (_config.trimSuggestionsId) {\n      var tSug = document.getElementById(_config.trimSuggestionsId);\n      if (tSug) tSug.classList.add(PREFIX + '__suggestions');\n    }\n  }\n\n  /**\n   * Removes all dynamically created DOM elements and cleans up event listeners.\n   * Called when re-initializing or when the module is no longer needed.\n   */\n  function cleanup() {\n    removeAllListeners();\n\n    // Remove injected DOM elements\n    var elementsToRemove = [\n      _els.zoomOverlay,\n      _els.catalogFabricOverlay,\n      _els.catalogTrimOverlay,\n      _els.fabricSwatch,\n      _els.trimSwatch\n    ];\n\n    elementsToRemove.forEach(function (el) {\n      if (el && el.parentNode) {\n        el.parentNode.removeChild(el);\n      }\n    });\n\n    // Remove injected styles\n    var styleEl = document.getElementById(PREFIX + '-styles');\n    if (styleEl && styleEl.parentNode) {\n      styleEl.parentNode.removeChild(styleEl);\n    }\n\n    // Remove BEM classes from suggestions containers\n    if (_config && _config.fabricSuggestionsId) {\n      var fSug = document.getElementById(_config.fabricSuggestionsId);\n      if (fSug) fSug.classList.remove(PREFIX + '__suggestions');\n    }\n    if (_config && _config.trimSuggestionsId) {\n      var tSug = document.getElementById(_config.trimSuggestionsId);\n      if (tSug) tSug.classList.remove(PREFIX + '__suggestions');\n    }\n\n    // Reset state\n    _els = {};\n    _selectedFabric = null;\n    _selectedTrim = null;\n    _currentSearchResults = [];\n    _zoomType = null;\n    _isChoosingAnother = false;\n  }\n\n\n  /* =========================================================================\n   * TRH project row preselect (2026-10-26)\n   * A fabric (or trim) page's \"Make it into...\" row links here with\n   * ?trh_fabric=<variantId> (and/or ?trh_trim=<variantId>). The choice is also\n   * kept in sessionStorage for 60 minutes so it follows the customer from a\n   * collection (custom furniture, Roman shade styles) onto the piece they open.\n   * The item is mapped by mapVariantsToItems, so the shared TRHFabricMin rule\n   * (min-N tags, per-meter, min-1) and the zero-price / sold-out guards apply\n   * exactly as if the customer had picked it from search.\n   * ========================================================================= */\n  var PRESELECT_KEY = 'trhProjectSelection';\n  var PRESELECT_TTL = 60 * 60 * 1000;\n\n  function readPreselect() {\n    var out = { fabric: null, trim: null };\n    try {\n      var qs = new URLSearchParams(window.location.search);\n      var f = qs.get('trh_fabric'), t = qs.get('trh_trim');\n      if (f && /^\\d+$/.test(f)) out.fabric = f;\n      if (t && /^\\d+$/.test(t)) out.trim = t;\n      if (out.fabric || out.trim) {\n        sessionStorage.setItem(PRESELECT_KEY, JSON.stringify({ fabric: out.fabric, trim: out.trim, ts: Date.now() }));\n        return out;\n      }\n      var s = JSON.parse(sessionStorage.getItem(PRESELECT_KEY) || 'null');\n      if (s && s.ts && (Date.now() - s.ts) < PRESELECT_TTL) {\n        out.fabric = s.fabric || null;\n        out.trim = s.trim || null;\n      }\n    } catch (e) { /* storage blocked: URL param only */ }\n    return out;\n  }\n\n  function forgetPreselect(type) {\n    try {\n      var s = JSON.parse(sessionStorage.getItem(PRESELECT_KEY) || 'null');\n      if (!s) return;\n      s[type] = null;\n      if (!s.fabric && !s.trim) sessionStorage.removeItem(PRESELECT_KEY);\n      else sessionStorage.setItem(PRESELECT_KEY, JSON.stringify(s));\n    } catch (e) { /* ignore */ }\n  }\n\n  async function fetchVariantItem(variantId, includeFabricWidth) {\n    try {\n      return await fetchVariantItemOnce(variantId, includeFabricWidth);\n    } catch (e) {\n      await new Promise(function (r) { setTimeout(r, 1500); });\n      return fetchVariantItemOnce(variantId, includeFabricWidth);\n    }\n  }\n\n  async function fetchVariantItemOnce(variantId, includeFabricWidth) {\n    var q = [\n      'query preselect($id: ID!) {',\n      '  node(id: $id) {',\n      '    ... on ProductVariant {',\n      '      id title price { amount } availableForSale',\n      '      image { url(transform: { maxWidth: 800, maxHeight: 800 }) altText }',\n      '      product {',\n      '        id title productType vendor availableForSale tags',\n      '        widthCustom: metafield(namespace: \"custom\", key: \"width\") { value }',\n      '        images(first: 1) { edges { node { url(transform: { maxWidth: 800, maxHeight: 800 }) altText } } }',\n      '      }',\n      '    }',\n      '  }',\n      '}'\n    ].join('\\n');\n    var response = await fetch(\n      'https://' + _config.storeDomain + '/api/' + API_VERSION + '/graphql.json',\n      {\n        method: 'POST',\n        headers: {\n          'Content-Type': 'application/json',\n          'X-Shopify-Storefront-Access-Token': _config.storefrontToken\n        },\n        body: JSON.stringify({ query: q, variables: { id: 'gid://shopify/ProductVariant/' + variantId } })\n      }\n    );\n    if (!response.ok) throw new Error('Storefront API HTTP ' + response.status);\n    var data = await response.json();\n    var v = data && data.data && data.data.node;\n    if (!v || !v.product) return null;\n    var p = v.product;\n    var node = {\n      title: p.title, productType: p.productType, vendor: p.vendor, tags: p.tags,\n      widthCustom: p.widthCustom, images: p.images,\n      variants: { edges: [{ node: { id: v.id, title: v.title, price: v.price, availableForSale: v.availableForSale, image: v.image } }] }\n    };\n    var items = mapVariantsToItems(node, includeFabricWidth);\n    return items[0] || null;\n  }\n\n  async function applyPreselect() {\n    var want = readPreselect();\n    if (!want.fabric && !want.trim) return;\n    var cfg = _config;\n    try {\n      if (want.fabric && !_selectedFabric) {\n        var fab = await fetchVariantItem(want.fabric, true);\n        if (cfg !== _config) return;\n        if (fab && !_selectedFabric) {\n          selectFabric(fab);\n          dispatchEvent('trh:fabric-preselected', { fabric: fab });\n        }\n      }\n      if (want.trim && _config.showTrim && !_selectedTrim) {\n        var trim = await fetchVariantItem(want.trim, false);\n        if (cfg !== _config) return;\n        if (trim && !_selectedTrim) {\n          selectTrim(trim);\n          dispatchEvent('trh:trim-preselected', { trim: trim });\n        }\n      }\n    } catch (err) {\n      console.warn('[TRHFabricBrowser] Preselect skipped:', err);\n    }\n  }\n\n  /* =========================================================================\n   * PUBLIC API\n   * ========================================================================= */\n\n  /**\n   * @namespace TRHFabricBrowser\n   * @description Public API for the TRH Fabric Browser module.\n   */\n  /* 2026-10-26 welt: a contrast welt must be a sellable Fabric or Vinyl\n   * (no hide-price, no $0). Unknown type (older callers) is allowed because\n   * price and hide-price have already been checked upstream. */\n  function isWeltFabric(f) {\n    if (!f || isNotSellable(f)) return false;\n    var t = String(f.productType || '');\n    return !t || t === 'Fabric' || t === 'Vinyl';\n  }\n  function weltBlocked(f) {\n    var name = (f && f.title) || 'This item';\n    if (f && isNotSellable(f)) { routeToInquiry({ title: name }); return; }\n    window.alert(name + ' can\\u2019t be used for a contrast welt. Please choose a fabric or vinyl.');\n  }\n\n  window.TRHFabricBrowser = {\n    isWeltFabric: isWeltFabric,\n    weltBlocked: weltBlocked,\n\n    /**\n     * Initializes the fabric browser module. Sets up configuration, injects\n     * dynamic DOM elements (modals, swatch previews), binds all event listeners,\n     * and begins loading products from the Storefront API.\n     *\n     * Can be called multiple times - will clean up previous instance first.\n     *\n     * @param {Object} config - Configuration object\n     * @param {string} config.storeDomain - Shopify .myshopify.com domain for API calls\n     * @param {string} config.storefrontToken - Storefront API access token\n     * @param {string} [config.storefrontDomain] - Public-facing domain for popup URLs\n     *   (defaults to storeDomain if not provided)\n     * @param {string} config.fabricFieldId - DOM ID of the fabric search input element\n     * @param {string} [config.trimFieldId] - DOM ID of the trim search input element\n     * @param {string} config.fabricSuggestionsId - DOM ID of fabric suggestions container\n     * @param {string} [config.trimSuggestionsId] - DOM ID of trim suggestions container\n     * @param {boolean} [config.showTrim=false] - Whether to enable trim selection\n     * @param {Function} [config.onFabricSelect] - Callback invoked when a fabric is selected.\n     *   Receives the fabric object as its argument.\n     * @param {Function} [config.onTrimSelect] - Callback invoked when a trim is selected.\n     *   Receives the trim object as its argument.\n     * @param {Function} [config.onFabricClear] - Callback invoked when fabric selection is cleared.\n     * @param {Function} [config.onTrimClear] - Callback invoked when trim selection is cleared.\n     *\n     * @example\n     * TRHFabricBrowser.init({\n     *   storeDomain: 'z10i3p-9c.myshopify.com',\n     *   storefrontToken: '52130f6f2cd60f3e7a7003d2da243ff2',\n     *   fabricFieldId: 'singleFabricField',\n     *   fabricSuggestionsId: 'searchSuggestions',\n     *   trimFieldId: 'singleTrimField',\n     *   trimSuggestionsId: 'trimSearchSuggestions',\n     *   showTrim: true,\n     *   onFabricSelect: function(fabric) {\n     *     document.getElementById('fabricWidth').value = fabric.fabricWidth || 54;\n     *   }\n     * });\n     */\n    init: function (config) {\n      // Clean up any previous initialization\n      if (_config) {\n        cleanup();\n      }\n\n      // Validate required config\n      if (!config.storeDomain) {\n        console.error('[TRHFabricBrowser] config.storeDomain is required.');\n        return;\n      }\n      if (!config.storefrontToken) {\n        console.error('[TRHFabricBrowser] config.storefrontToken is required.');\n        return;\n      }\n      if (!config.fabricFieldId) {\n        console.error('[TRHFabricBrowser] config.fabricFieldId is required.');\n        return;\n      }\n      if (!config.fabricSuggestionsId) {\n        console.error('[TRHFabricBrowser] config.fabricSuggestionsId is required.');\n        return;\n      }\n\n      _config = {\n        storeDomain: config.storeDomain,\n        storefrontToken: config.storefrontToken,\n        storefrontDomain: config.storefrontDomain || config.storeDomain,\n        fabricFieldId: config.fabricFieldId,\n        trimFieldId: config.trimFieldId || null,\n        fabricSuggestionsId: config.fabricSuggestionsId,\n        trimSuggestionsId: config.trimSuggestionsId || null,\n        showTrim: !!config.showTrim,\n        onFabricSelect: config.onFabricSelect || null,\n        onTrimSelect: config.onTrimSelect || null,\n        onFabricClear: config.onFabricClear || null,\n        onTrimClear: config.onTrimClear || null,\n        /* TRH outdoor curtain 2026-10-28: optional scope settings (absent = default behaviour) */\n        fabricScope: config.fabricScope || null,\n        trimScope: config.trimScope || null,\n        fabricCollection: config.fabricCollection || null,\n        trimCollection: config.trimCollection || null,\n        fabricRequireAnyTag: config.fabricRequireAnyTag || null,\n        trimRequireAnyTag: config.trimRequireAnyTag || null,\n        scopeBlockedMessage: config.scopeBlockedMessage || null\n      };\n\n      injectDOM();\n      bindEvents();\n\n      // Begin loading products in the background\n      loadProducts();\n\n      // TRH project row: apply ?trh_fabric= / ?trh_trim= after the calculator's own listeners attach\n      setTimeout(applyPreselect, 0);\n\n      console.log('[TRHFabricBrowser] Initialized.', {\n        showTrim: _config.showTrim,\n        domain: _config.storeDomain\n      });\n    },\n\n    /**\n     * Returns the currently selected fabric object, or null if no fabric\n     * is selected.\n     * @returns {Object|null} The selected fabric object, or null\n     */\n    getSelectedFabric: function () {\n      return _selectedFabric;\n    },\n\n    /**\n     * Returns the currently selected trim object, or null if no trim\n     * is selected.\n     * @returns {Object|null} The selected trim object, or null\n     */\n    getSelectedTrim: function () {\n      return _selectedTrim;\n    },\n\n    /**\n     * Returns a copy of the preloaded fabric cache.\n     * @deprecated The cache is bounded (first 250 fabric products) \u2014 it does\n     *   NOT contain the full 5,000+ fabric inventory. Use searchFabrics()\n     *   for anything search-related.\n     * @returns {Array<Object>} Array of cached fabric product objects\n     */\n    getAllFabrics: function () {\n      return _allFabrics.slice();\n    },\n\n    /**\n     * Returns a copy of the preloaded trim cache.\n     * @deprecated The cache is bounded (first 250 trim products). Use\n     *   searchTrims() for anything search-related.\n     * @returns {Array<Object>} Array of cached trim product objects\n     */\n    getAllTrims: function () {\n      return _allTrims.slice();\n    },\n\n    /**\n     * Searches the FULL fabric inventory server-side via the Storefront API.\n     * @param {string} term - Raw user search input\n     * @param {number} [limit=10] - Maximum results to return\n     * @returns {Promise<Array<Object>>} Matching fabric items, best first\n     */\n    searchFabrics: function (term, limit) {\n      if (!_config) {\n        console.error('[TRHFabricBrowser] Not initialized. Call init() first.');\n        return Promise.resolve([]);\n      }\n      return searchProductsRemote(term, 'fabric', limit);\n    },\n\n    /**\n     * Searches the FULL trim inventory server-side via the Storefront API.\n     * @param {string} term - Raw user search input\n     * @param {number} [limit=10] - Maximum results to return\n     * @returns {Promise<Array<Object>>} Matching trim items, best first\n     */\n    searchTrims: function (term, limit) {\n      if (!_config) {\n        console.error('[TRHFabricBrowser] Not initialized. Call init() first.');\n        return Promise.resolve([]);\n      }\n      return searchProductsRemote(term, 'trim', limit);\n    },\n\n    /**\n     * Programmatically selects a fabric. Equivalent to the user clicking\n     * a search suggestion. Updates UI, fires callbacks and custom events.\n     * @param {Object} fabric - Fabric object to select\n     * @param {string} fabric.id - Variant ID\n     * @param {string} fabric.title - Display title\n     * @param {number} fabric.price - Price per yard\n     * @param {string} fabric.imageUrl - Thumbnail image URL\n     * @param {string} [fabric.altText] - Image alt text\n     * @param {number} [fabric.fabricWidth] - Fabric width in inches\n     */\n    selectFabric: function (fabric) {\n      if (!_config) {\n        console.error('[TRHFabricBrowser] Not initialized. Call init() first.');\n        return;\n      }\n      selectFabric(fabric);\n    },\n\n    /**\n     * Programmatically selects a trim. Equivalent to the user clicking\n     * a search suggestion. Updates UI, fires callbacks and custom events.\n     * @param {Object} trim - Trim object to select\n     * @param {string} trim.id - Variant ID\n     * @param {string} trim.title - Display title\n     * @param {number} trim.price - Price per yard\n     * @param {string} trim.imageUrl - Thumbnail image URL\n     * @param {string} [trim.altText] - Image alt text\n     */\n    /**\n     * TRH project panel (2026-10-26): run a Storefront search string and get\n     * mapped items (TRHFabricMin, zero-price and sold-out guards applied).\n     */\n    queryItems: function (searchString, first, includeFabricWidth) {\n      if (!_config) return Promise.resolve([]);\n      return executeProductSearch(searchString, Math.min(first || 20, 50), !!includeFabricWidth);\n    },\n\n    selectTrim: function (trim) {\n      if (!_config) {\n        console.error('[TRHFabricBrowser] Not initialized. Call init() first.');\n        return;\n      }\n      selectTrim(trim);\n    },\n\n    /**\n     * Clears the current fabric or trim selection. Resets the search input,\n     * hides the swatch preview, and fires the appropriate callbacks and\n     * custom events.\n     * @param {string} type - 'fabric' or 'trim'\n     */\n    clearSelection: function (type) {\n      if (!_config) {\n        console.error('[TRHFabricBrowser] Not initialized. Call init() first.');\n        return;\n      }\n      clearSelection(type);\n    },\n\n    /**\n     * Opens the catalog browser popup for the specified product type.\n     * If the popup is blocked by the browser, a modal with instructions\n     * and a retry button will be shown instead.\n     * @param {string} type - 'fabric' or 'trim'\n     */\n    showCatalog: function (type) {\n      if (!_config) {\n        console.error('[TRHFabricBrowser] Not initialized. Call init() first.');\n        return;\n      }\n      showCatalog(type);\n    },\n\n    /**\n     * Forces a reload of all products from the Storefront API, discarding\n     * the current cached data. Useful after product catalog changes or when\n     * recovering from a load failure.\n     * @returns {Promise<void>}\n     */\n    reloadProducts: function () {\n      if (!_config) {\n        console.error('[TRHFabricBrowser] Not initialized. Call init() first.');\n        return Promise.resolve();\n      }\n      _productsLoaded = false;\n      _allFabrics = [];\n      _allTrims = [];\n      _searchCache.clear();\n      return loadProducts();\n    },\n\n    /**\n     * Destroys the module instance. Removes all injected DOM elements,\n     * unbinds all event listeners, and resets internal state.\n     * The module can be re-initialized by calling init() again.\n     */\n    destroy: function () {\n      cleanup();\n      _config = null;\n      _allFabrics = [];\n      _allTrims = [];\n      _productsLoaded = false;\n      _loadingProducts = false;\n      _searchCache.clear();\n      console.log('[TRHFabricBrowser] Destroyed.');\n    }\n  };\n})();\n"],
  "mappings": "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SAAS,cAAc,OAAO,EAC1C,MAAM,GAAK,OAAS,UACpB,MAAM,YAAc,CAElB,IAAM,OAAS,kBACf,wBACA,eACA,aACA,cACA,sBACA,4BACA,sBACA,gCACA,uBACA,sBACA,mBACA,mBACA,4CACA,IACA,IAAM,OAAS,0BACf,oBACA,mBACA,IAGA,IAAM,OAAS,iBACf,mBACA,qBACA,mCACA,qBACA,mBACA,yBACA,eACA,gCACA,sBACA,wBACA,IACA,IAAM,OAAS,sBACf,IAAM,OAAS,2BACf,yBACA,oBACA,IACA,IAAM,OAAS,4BACf,yBACA,IAGA,IAAM,OAAS,uBACf,iBACA,kBACA,uBACA,wBACA,4BACA,oBACA,yBACA,qBACA,IAGA,IAAM,OAAS,yBACf,aACA,kBACA,IACA,IAAM,OAAS,sBACf,sBACA,uBACA,yBACA,sBACA,6BACA,IACA,IAAM,OAAS,uBACf,qBACA,iBACA,eACA,IAGA,IAAM,OAAS,iBACf,mBACA,iBACA,wBACA,wBACA,IAGA,IAAM,OAAS,qBACf,mBACA,qBACA,yBACA,cACA,wBACA,IACA,IAAM,OAAS,8BACf,mBACA,IACA,IAAM,OAAS,mBACf,iBACA,kBACA,uBACA,wBACA,4BACA,yBACA,IACA,IAAM,OAAS,8BACf,qBACA,IACA,IAAM,OAAS,mBACf,qBACA,iBACA,eACA,IACA,IAAM,OAAS,mBACf,6BACA,kBACA,mBACA,sBACA,qBACA,qBACA,iBACA,mBACA,2BACA,oBACA,gBACA,uBACA,yBACA,iBACA,kBACA,sBACA,uBACA,IACA,IAAM,OAAS,4BACf,2BACA,IACA,IAAM,OAAS,yBACf,iBACA,IAGA,IAAM,OAAS,mBACf,mBACA,qBACA,YACA,aACA,cACA,eACA,gCACA,oBACA,6BACA,yBACA,qBACA,IACA,IAAM,OAAS,4BACf,mBACA,IACA,IAAM,OAAS,mBACf,sBACA,yBACA,mBACA,qBACA,sBACA,wBACA,wBACA,sBACA,qBACA,IACA,IAAM,OAAS,iBACf,wBACA,eACA,iBACA,qBACA,uBACA,qBACA,iBACA,iCAC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",
  "names": []
}
