{"version":3,"file":"product-card-element-section.min.js","mappings":"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","sources":["webpack://lange/webpack/bootstrap","webpack://lange/./lib/video.ts","webpack://lange/webpack/runtime/jsonp chunk loading","webpack://lange/./sections/product-card-element/product-card-image-carousel/index.ts","webpack://lange/./lib/perf.ts","webpack://lange/./lib/growthbook-feature.ts","webpack://lange/../node_modules/@integrabeauty/assert/src/index.ts","webpack://lange/./sections/product-card-element/experiment.ts","webpack://lange/./sections/product-card-element/product-card/index.html","webpack://lange/./sections/product-card-element/product-card/index.ts","webpack://lange/./lib/dom-ready.ts","webpack://lange/./sections/product-card-element/sale-discount-badge/index.ts"],"sourcesContent":["// The module cache\nlet __webpack_module_cache__ = {};\n\n// The require function\nfunction __webpack_require__(moduleId) {\n\t// Check if module is in cache\n\tlet cachedModule = __webpack_module_cache__[moduleId];\n\tif (cachedModule !== undefined) {\n\t\treturn cachedModule.exports;\n\t}\n\t// Create a new module (and put it into the cache)\n\tlet module = __webpack_module_cache__[moduleId] = {\n\t\t// no module.id needed\n\t\t// no module.loaded needed\n\t\texports: {}\n\t};\n\n\t// Execute the module function\n\t__webpack_modules__[moduleId](module, module.exports, __webpack_require__);\n\n\t// Return the exports of the module\n\treturn module.exports;\n}\n\n","/**\n * Try to play a video, and always report the outcome as a promise.\n *\n * Do not inline this function. Playing a video is inconsistent across browsers in several ways, and\n * wrapping the call in an async function is what smooths all of them over at once.\n *\n * Some browsers have no `play` function on the element at all, so the call throws a TypeError. Some\n * have one that returns undefined instead of a promise, which is how `play` worked before the spec\n * gained a return value. Some return a promise that rejects later, which is the modern behavior and\n * is how a browser reports that it refused to autoplay. And some throw synchronously, right out of\n * the call, rather than rejecting.\n *\n * An async function turns all four into the same thing. Whatever happens inside it, the caller gets\n * a promise back, so a synchronous throw arrives as a rejection and a missing or non-promise `play`\n * still produces a promise that resolves. That means one `catch` on the call site covers every\n * failure mode, and the caller never has to check whether it got a promise before attaching a\n * handler.\n *\n * Nothing here is actionable, because a refusal is the browser enforcing its own autoplay policy.\n * Restrictive in-app browsers and low power mode refuse even after a user gesture, and interrupted\n * or unsupported sources reject too. The video simply stays paused, so callers should keep a debug\n * breadcrumb and never warn.\n */\nexport async function play(video: HTMLVideoElement) {\n  return video.play();\n}\n","__webpack_require__.b = (typeof document !== 'undefined' && document.baseURI) || self.location.href;\n\n// no installed chunks\n\n// no chunk on demand loading\n\n// no prefetching\n\n// no preloaded\n\n// no HMR\n\n// no HMR manifest\n\n// no on chunks loaded\n\n// no jsonp function","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport { play } from '../../../lib/video.js';\nimport html from './index.html';\nimport styles from './index.scss';\n\n/**\n * Renders a horizontal media carousel for a product card. Each slide shows one product image or\n * video wrapped in a link to the product page. Arrows appear while the cursor is over the carousel\n * and hide otherwise, keeping the media area clean at rest. They stay hidden even when they have\n * keyboard focus, which costs keyboard users the ability to see them, and that is deliberate.\n *\n * Media come as light DOM <img> and <video> children. On connect the element clones them into\n * slides of a carousel-element inside the shadow DOM. Videos are paused when off-screen and played\n * when visible. When only one slide is present the arrows are hidden unconditionally.\n *\n * The carousel-element's events are composed, so the product card hears them from outside this\n * element's shadow root. It listens for the mount to swap in the gallery, and for moves to hide its\n * badges.\n *\n * @example Usage example in a product card template:\n *  <product-card-image-carousel data-url=\"{{ product_url }}\">\n *    <template>\n *      {%- for media in product.metafields.product.product_card_gallery.value -%}\n *        {%- if media.media_type == 'video' -%}\n *          {{- media | video_tag: image_size: '434x', loop: true, muted: true, controls: false,\n *            playsinline: true, preload: 'none' -}}\n *        {%- else -%}\n *          {{- media | image_url: width: 434 | image_tag: alt: \"\" -}}\n *        {%- endif -%}\n *      {%- endfor -%}\n *    </template>\n *  </product-card-image-carousel>\n */\nclass ProductCardImageCarousel extends HTMLElement implements CustomElement {\n  declare readonly dataset: {\n    /**\n     * The relative URL of the product page. Each slide's image links to this URL.\n     */\n    url: string;\n  };\n\n  /**\n   * The index of the slide the carousel is on or moving to. The carousel's own `data-index` only\n   * catches up once a move ends, and the hover preview needs to know where a move is headed as soon\n   * as it starts.\n   */\n  private index = 0;\n\n  /**\n   * Whether the carousel is showing the second slide as a hover preview, rather than sitting on a\n   * slide the visitor moved it to. Only the preview is undone when the cursor leaves.\n   */\n  private hoverPreview = false;\n\n  private hoverTimeout: ReturnType<typeof setTimeout>;\n  private onMouseEnterBound = this.onMouseEnter.bind(this);\n  private onMouseLeaveBound = this.onMouseLeave.bind(this);\n  private onClickBound = this.onClick.bind(this);\n  private onCarouselMountedBound = this.onCarouselMounted.bind(this);\n  private onSlideMoveStartedBound = this.onSlideMoveStarted.bind(this);\n\n  public connectedCallback() {\n    this.initializeShadowRoot();\n\n    // Listening on this element cause a bug where the event.target is always the host element when\n    // any of the shadow DOM children are clicked. Listening on the carousel itself works as\n    // expected.\n    const carousel = this.shadowRoot.querySelector('carousel-element');\n    carousel.addEventListener('click', this.onClickBound);\n    carousel.addEventListener('carousel-element-mounted', this.onCarouselMountedBound);\n    carousel.addEventListener('carousel-slide-move-started', this.onSlideMoveStartedBound);\n    carousel.addEventListener('carousel-slide-visible', onSlideVisible);\n    carousel.addEventListener('carousel-slide-hidden', onSlideHidden);\n\n    if (carousel.querySelectorAll('[data-carousel-slide]').length > 1) {\n      this.addEventListener('pointerenter', this.onMouseEnterBound);\n      this.addEventListener('pointerleave', this.onMouseLeaveBound);\n    }\n  }\n\n  public disconnectedCallback() {\n    clearTimeout(this.hoverTimeout);\n    this.removeEventListener('pointerenter', this.onMouseEnterBound);\n    this.removeEventListener('pointerleave', this.onMouseLeaveBound);\n\n    const carousel = this.shadowRoot.querySelector('carousel-element');\n    carousel.removeEventListener('click', this.onClickBound);\n    carousel.removeEventListener('carousel-element-mounted', this.onCarouselMountedBound);\n    carousel.removeEventListener('carousel-slide-move-started', this.onSlideMoveStartedBound);\n    carousel.removeEventListener('carousel-slide-visible', onSlideVisible);\n    carousel.removeEventListener('carousel-slide-hidden', onSlideHidden);\n  }\n\n  private initializeShadowRoot() {\n    if (!this.shadowRoot) {\n      this.attachShadow({ mode: 'open' });\n      this.shadowRoot.innerHTML = `<style>${styles}</style>${html}`;\n      this.buildSlides();\n    }\n  }\n\n  private onClick(event: MouseEvent) {\n    if (!(event.target instanceof Element)) {\n      return;\n    }\n\n    if (!event.target.closest('[data-carousel-slide]')) {\n      return;\n    }\n\n    const url = this.dataset.url;\n    if (url) {\n      location.href = url;\n    }\n  }\n\n  /**\n   * Hands the media over to the carousel as slides. The carousel waits a tick before it builds, so\n   * the slides are in place by then.\n   */\n  private buildSlides() {\n    // We support only a single <template> child, which is the easiest way to pass multiple media\n    // elements in light DOM without them being rendered in the light DOM.\n    const template = this.querySelector('template');\n    if (!template) {\n      return;\n    }\n\n    const carousel = this.shadowRoot.querySelector('carousel-element');\n\n    // Shopify's video_tag puts a fallback <img> of the poster inside each <video>. We skip images\n    // that sit inside a video, so the poster does not turn into a slide of its own.\n    const media = template.content.querySelectorAll('img:not(video img), video');\n\n    for (const element of media) {\n      const slide = document.createElement('div');\n      slide.dataset.carouselSlide = '';\n\n      const cloned = <HTMLElement>element.cloneNode(true);\n      cloned.classList.add('slide-media');\n\n      if (cloned instanceof HTMLVideoElement) {\n        // Playback is managed by the carousel's visibility events, not the native autoplay\n        // attribute. We don't expect videos to have autoplay set, but it's very sensitive from a\n        // performance perspective, so we make sure it's not set.\n        cloned.autoplay = false;\n      }\n\n      slide.appendChild(cloned);\n      carousel.appendChild(slide);\n    }\n  }\n\n  private onCarouselMounted(event: Event) {\n    if (!(event.currentTarget instanceof HTMLElement)) {\n      return;\n    }\n\n    const carousel = event.currentTarget;\n    this.index = parseInt(carousel.dataset.index, 10);\n\n    const video = carousel.querySelector('[data-carousel-slide]')?.querySelector('video');\n    if (video) {\n      // A refusal here is not actionable, so keep a breadcrumb and never warn.\n      play(video).catch(console.debug);\n    }\n\n    this.announceFirstImage(carousel);\n\n    // The carousel checks overflow at mount time to decide whether to set data-overflowing. All\n    // carousels share the same dynamic import promise, so they all mount in the same microtask\n    // batch before the browser has laid out the shadow DOM. The overflow check sees a zero-width\n    // track and skips data-overflowing. A refresh after the first frame gives it real dimensions.\n    requestAnimationFrame(() => {\n      carousel.dataset.refresh = 'true';\n    });\n  }\n\n  /**\n   * Tells the product card once the first slide's image has loaded. The card keeps its static image\n   * up until then, because the slide images are lazy and swapping right at mount left the photo\n   * blank for a moment while the first one loaded. A first slide with no image, such as a video,\n   * has nothing to wait for.\n   */\n  private announceFirstImage(carousel: HTMLElement) {\n    // We look up the first slide and then the image inside it on purpose. Folding the two into one\n    // selector like '[data-carousel-slide] img' looks the same but is not. That finds the first\n    // image in any slide, so when the first slide is a video it would grab the second slide's image\n    // instead, and the card would wait on a lazy image nobody is looking at.\n    const image = carousel.querySelector('[data-carousel-slide]')?.querySelector('img');\n    if (!image || image.complete) {\n      this.dispatchEvent(new Event('product-card-gallery-loaded', { bubbles: true }));\n      return;\n    }\n\n    // An image that fails to load still has to let the card go, or the static image would sit over\n    // the carousel for good.\n    const onSettled = () => {\n      image.removeEventListener('load', onSettled);\n      image.removeEventListener('error', onSettled);\n      this.dispatchEvent(new Event('product-card-gallery-loaded', { bubbles: true }));\n    };\n\n    image.addEventListener('load', onSettled);\n    image.addEventListener('error', onSettled);\n  }\n\n  /**\n   * The product card hears this event too and hides its badges as the carousel leaves the first\n   * slide, before the animation finishes.\n   */\n  private onSlideMoveStarted(event: GlobalEventHandlersEventMap['carousel-slide-move-started']) {\n    this.index = event.detail.index;\n\n    // Every move clears the preview, ours included. onMouseEnter claims it back once its own move\n    // has started.\n    this.hoverPreview = false;\n  }\n\n  private onMouseEnter(event: PointerEvent) {\n    if (event.pointerType === 'touch') {\n      return;\n    }\n\n    // don't switch to the second slide on hover if the user has already moved the carousel to a\n    // different slide\n    if (this.index !== 0) {\n      return;\n    }\n\n    this.hoverTimeout = setTimeout(() => {\n      const carousel = this.shadowRoot.querySelector('carousel-element');\n      carousel.dataset.go = '1';\n\n      // The carousel starts the move from inside setting data-go, and our move handler clears the\n      // flag, so we claim the preview afterward rather than before. Before the carousel mounts,\n      // data-go does nothing and the index stays where it was.\n      //\n      // We go by the index we ended up on rather than by whether our own move is what moved the\n      // carousel. So if the visitor clicked the arrow through to the second slide themselves while\n      // this wait was still running, our move does nothing and we would still call their slide our\n      // preview and take them back to the first one when the cursor left. The wait is short enough\n      // that nobody gets a click in, but a longer one would want us to check whether this call is\n      // what moved anything.\n      this.hoverPreview = this.index === 1;\n    }, 150);\n  }\n\n  private onMouseLeave(event: PointerEvent) {\n    if (event.pointerType === 'touch') {\n      return;\n    }\n\n    clearTimeout(this.hoverTimeout);\n\n    // Scrolling the card out of the viewport fires this the same as moving the cursor off it, so\n    // going back to the first slide here would throw away whatever slide the visitor had picked\n    // every time the card left the screen.\n    if (!this.hoverPreview) {\n      return;\n    }\n\n    this.hoverPreview = false;\n\n    const carousel = this.shadowRoot.querySelector('carousel-element');\n    carousel.dataset.go = '0';\n  }\n}\n\nfunction onSlideVisible(event: GlobalEventHandlersEventMap['carousel-slide-visible']) {\n  const video = event.detail.slide.querySelector('video');\n  if (video) {\n    // A refusal here is not actionable, so keep a breadcrumb and never warn.\n    play(video).catch(console.debug);\n  }\n}\n\nfunction onSlideHidden(event: GlobalEventHandlersEventMap['carousel-slide-hidden']) {\n  const video = event.detail.slide.querySelector('video');\n  video?.pause();\n}\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'product-card-image-carousel': ProductCardImageCarousel;\n  }\n\n  interface GlobalEventHandlersEventMap {\n    /**\n     * The first slide of a product card's image carousel has loaded its image, or has no image to\n     * wait for.\n     */\n    'product-card-gallery-loaded': Event;\n  }\n}\n\nif (!customElements.get('product-card-image-carousel')) {\n  customElements.define('product-card-image-carousel', ProductCardImageCarousel);\n}\n","/**\n * Returns a timestamp in milliseconds for measuring durations.\n *\n * This decorates performance.now().\n *\n * This tries to use the native high-resolution epoch, falling back to Date.now().\n *\n * If native throws, this still returns a value, but it logs a message as a side effect.\n *\n * This happens in a try/catch, so it is safe.\n *\n * @see https://developer.mozilla.org/en-US/docs/Web/API/Performance/now\n */\nexport function now() {\n  try {\n    if (typeof performance?.now === 'function') {\n      return performance.now();\n    }\n  } catch (error) {\n    console.debug(error);\n  }\n\n  return Date.now();\n}\n\n/**\n * Records a named point on the User Timing timeline in devtools.\n *\n * This decorates performance.mark within a try/catch that suppresses errors.\n *\n * @see https://developer.mozilla.org/en-US/docs/Web/API/Performance/mark\n */\nexport function mark(name: string) {\n  try {\n    if (typeof performance?.mark === 'function') {\n      performance.mark(name);\n    }\n  } catch (error) {\n    console.debug(error);\n  }\n}\n\n/**\n * Records a named span between two timestamps on the User Timing timeline in devtools.\n *\n * This decorates performance.measure within a try/catch that suppresses errors.\n *\n * The bounds are explicit timestamps rather than named marks, which avoids the error the marked\n * name form throws when a start mark was never recorded.\n *\n * The detail param is optional and must be structured-cloneable. It should be a plain object. If\n * the value cannot be cloned, then performance.measure throws, which is caught and logged by this\n * wrapper. If specified, the detail shows up in the tooltip in the devtools panel.\n *\n * @see https://developer.mozilla.org/en-US/docs/Web/API/Performance/measure\n */\nexport function measure(name: string, start: number, end: number, detail?: unknown) {\n  try {\n    if (typeof performance?.measure === 'function') {\n      performance.measure(name, { start, end, detail });\n    }\n  } catch (error) {\n    console.debug(error);\n  }\n}\n","import { measure, now } from './perf.js';\n\n/**\n * Waits for the runtime's load to reach a terminal state. Resolves true once a client is ready to\n * read, and false once the runtime has failed or the given number of milliseconds passes.\n *\n * The caller does not have to care whether the runtime loaded before or after it, which is the\n * whole point. Arriving late is covered by reading the load state, because the broadcast has\n * already been and gone by then, and arriving early is covered by listening for the broadcast.\n * Between them there is no ordering a caller can lose to.\n *\n * A failure resolves as soon as it is known rather than waiting out the timeout. That matters\n * because two of the failures are decided long before any caller's deadline. The theme editor is\n * known before a single bundle runs, and a missing anonymous id is known the moment the runtime\n * looks for one. Sitting out two or three seconds for an answer that already exists would delay\n * everything downstream of the value for nothing.\n *\n * Anything other than pending is terminal, which is what the check tests for, so a failure reason\n * added later needs no change here.\n *\n * On a preview theme the wait is extended, because the runtime deliberately holds the broadcast\n * open there to give the GrowthBook DevTools extension time to push forced variant overrides in. A\n * caller that gave up partway through that hold would silently drop the forced variant and make the\n * extension unreliable to debug with. How long the hold lasts comes off the runtime global, so the\n * two sides cannot drift apart into a hold that outlives the wait covering it.\n *\n * Neither the preview check nor the hold length can be read from anything the runtime's boot script\n * writes, because that script loads async and can execute after a caller has already asked. Both\n * the Shopify globals and the runtime's inline seed are set in the head instead, so they are always\n * available.\n *\n * The timeout stays as a floor for the one case no broadcast can cover, the boot script never\n * running at all. On that path the listener is left registered rather than torn down, since a\n * broadcast that does eventually arrive resolves a promise that has already settled, which changes\n * nothing.\n */\nfunction whenLoaded(milliseconds: number) {\n  return new Promise<boolean>(resolve => {\n    if (growthbook_runtime.load_state !== 'pending') {\n      resolve(growthbook_runtime.load_state === 'ready');\n      return;\n    }\n\n    const extraMillis = Shopify.previewMode ? growthbook_runtime.preview_hold_ms : 0;\n    const timer = setTimeout(resolve, milliseconds + extraMillis, false);\n\n    addEventListener('growthbook-load-settled', () => {\n      clearTimeout(timer);\n      resolve(growthbook_runtime.load_state === 'ready');\n    }, { once: true });\n  });\n}\n\n/**\n * Fault-tolerant wrapper around GrowthBook's own getFeatureValue SDK method.\n *\n * In GrowthBook the unit is a feature (a feature flag) identified by a feature key. The feature's\n * value for the current user is decided by evaluating its rules. One kind of rule is an experiment\n * rule (a \"feature flag experiment\"): it randomly buckets the user into a variation and returns\n * that variation's value. So the same feature key can resolve to a static flag value or to an A/B\n * test assignment, and this call returns whatever it resolves to. The caller cannot tell which, and\n * does not need to. When the feature is backed by an experiment rule, the returned value is the\n * user's assigned variation.\n *\n * Calling this is not a passive read, and that is the thing to know before adding a call. Bucketing\n * happens during the read, and the SDK reports the exposure to the runtime's tracking callback on\n * the way back out, which is what puts the visitor in the experiment's analytics. So the moment a\n * section calls this, that visitor counts as having seen whatever variation came back.\n *\n * That makes where a call sits matter as much as what it does with the answer. A read the visitor\n * could not possibly have seen still counts, and every such visitor lands in both groups and\n * behaves the same in both, which drags the measured difference between them toward zero. So gate\n * the call on the thing being on the page and reachable, rather than reading up front and deciding\n * later.\n *\n * On top of the raw SDK call this wrapper adds:\n *\n * - Fault tolerance. On a timeout, an error, or an undefined feature it returns the caller's\n *   fallback instead of throwing. It does assume the runtime global is present, which the always-on\n *   layer guarantees, so a page that somehow lacks it rejects rather than falling back.\n * - A shared, pre-initialized client. It reads the single client the cross-section coordination\n *   global exposes, so the SDK is initialized once per page rather than once per call.\n * - Indifference to load order. It waits on the runtime's ready signal rather than on the client\n *   promise existing yet, so a caller whose bundle runs before the runtime's is not penalized for\n *   it.\n * - A bounded wait. It always resolves within the given timeout, or within a global default when no\n *   per-call timeout is passed.\n * - Per-page memoization. The first resolved value for a feature key is cached and returned for\n *   every later call in the same page load. Once the payload has loaded this buys nothing on its\n *   own, because the SDK evaluates from attributes that do not change during a page, so repeated\n *   reads would agree with each other anyway. Where it earns its place is the timeout boundary. A\n *   caller that gave up waiting holds the fallback and the page has already acted on it, the client\n *   can become ready a moment later, and without it the next caller would get the real value and\n *   disagree with what the page is showing.\n *\n * @param featureKey The GrowthBook feature key.\n * @param fallback The value returned when the feature is undefined or when the lookup times out or\n * errors.\n * @param timeoutMs Optional per-call override for the lookup timeout, in milliseconds.\n *\n * @see https://docs.growthbook.io/lib/js#quick-usage GrowthBook JS SDK getFeatureValue\n * @see https://docs.growthbook.io/feature-flag-experiments How A/B tests are delivered via features\n * @see https://docs.growthbook.io/features/basics Feature flag fundamentals (keys, values, rules)\n */\nexport async function getFeatureValue<T extends string = string>(featureKey: string,\n  fallback: NoInfer<T>, timeoutMs?: number) {\n  const resolvedFeatures = growthbook_runtime.resolved_features;\n  if (resolvedFeatures[featureKey]) {\n    return <T>resolvedFeatures[featureKey];\n  }\n\n  let value: T | null = null;\n\n  // The span starts here rather than at function entry, so that it only ever covers a real wait.\n  // The memoized return above answers without waiting on anything, and timing it would put a span\n  // on the timeline for every read in the page. Leaving it outside means a span showing up at all\n  // is a caller that actually waited. Closing the span in a finally is what covers the timeout and\n  // the failure paths, which are the ones most worth a number.\n\n  const startMillis = now();\n  const effectiveTimeoutMs = timeoutMs ?? growthbook_runtime.default_timeout_ms;\n\n  try {\n    const loaded = await whenLoaded(effectiveTimeoutMs);\n\n    // The runtime either failed or ran out of time, and neither is reported here so as to avoid\n    // Sentry noise, so we return instead of throwing. The fallback is cached so that later calls\n    // for the same feature key in the same page load get the same answer, since a feature read in\n    // several places has to agree with itself.\n\n    if (!loaded) {\n      resolvedFeatures[featureKey] = fallback;\n      return fallback;\n    }\n\n    // The signal says the client is ready, so this await is already settled and only defends\n    // against the impossible case of the two disagreeing.\n\n    const growthbook = await growthbook_runtime.client_loader;\n\n    if (!growthbook) {\n      resolvedFeatures[featureKey] = fallback;\n      return fallback;\n    }\n\n    // Passing null here rather than the caller's fallback is deliberate. The SDK applies its own\n    // default only on a strict null check against the evaluated value, so handing it null makes\n    // this a straight pass-through of whatever the feature evaluated to, and every \"nothing usable\"\n    // case then lands on the single type check below.\n    //\n    // The evaluated value is null for an unknown feature key, for a feature with no default value\n    // configured, and for an unmet prerequisite. Passing the fallback here would cover exactly\n    // those and nothing else.\n    //\n    // What it would not cover is a value that is present but not a string. A feature configured\n    // with a number, a boolean, or a JSON value evaluates to that value, the SDK's null check does\n    // not fire, and the value comes back untouched. Undefined slips through for the same reason. So\n    // the type check below is load-bearing either way, and routing everything through it keeps the\n    // decision about what counts as usable in one place.\n\n    value = <T | null>growthbook.getFeatureValue<string | null>(featureKey, null);\n  } catch (error) {\n    console.warn('Error getting feature value from GrowthBook:', error);\n  } finally {\n    measure('growthbook-feature-value', startMillis, now(), {\n      feature: featureKey,\n      timeout: effectiveTimeoutMs\n    });\n  }\n\n  // The value comes from GrowthBook, so its type is an unverified boundary assertion. Coalesce\n  // anything that is not a string (null, or a missing or unexpected value) to the caller's default,\n  // so the resolved value is always a defined string.\n\n  if (typeof value !== 'string') {\n    value = fallback;\n  }\n\n  resolvedFeatures[featureKey] = value;\n\n  return value;\n}\n\n/**\n * Compares a value read through {@link getFeatureValue} against an expected value, ignoring case.\n * Every comparison against a feature value goes through this, even where a plain equality check\n * would do today, so that the casing decision lives in one place and new code copied from an\n * existing call site picks it up.\n *\n * The value is GrowthBook boundary data whose casing is not guaranteed, and a feature has been seen\n * returning an uppercase value. The expected side can be typed by hand too, such as an experiment\n * id typed into a section setting in the theme editor. Neither side's casing is enforced anywhere,\n * so we lowercase both. We leave the value itself untouched in the wrapper, so a caller reading it\n * directly still sees exactly what GrowthBook served.\n *\n * @param value The feature value\n * @param expected The value to compare against\n */\nexport function featureValueMatches(value: string, expected: string) {\n  return value.toLowerCase() === expected.toLowerCase();\n}\n","/**\n * Throws an assertion error if the given condition is falsy.\n *\n * @param expression any expression, although one that evaluates to true or false is preferred by\n * convention\n * @param message optional error text\n */\nexport function assert(expression: unknown, message?: string) {\n  if (!expression) {\n    throw new AssertionError(message);\n  }\n}\n\nexport class AssertionError extends Error {\n  constructor(message = 'Assertion error') {\n    super(message);\n    this.name = this.constructor.name;\n  }\n}\n","import { assert } from '@integrabeauty/assert';\nimport { getFeatureValue } from '../../lib/growthbook-feature.js';\n\n// The Quick Add label A/B test, run through the sale-page-quick-add-label GrowthBook feature. It\n// tries \"Quick Add\" against the usual \"Select Option\" as the label of the product card's select\n// option button.\n\n/**\n * The groups of the Quick Add label test, matching the values the sale-page-quick-add-label\n * GrowthBook feature serves.\n */\nexport type Group = 'quick-add' | 'select-option';\n\nfunction isGroup(value: any): value is Group {\n  return value === 'quick-add' || value === 'select-option';\n}\n\n/**\n * Resolves to the visitor's group in the Quick Add label test, falling back to select-option.\n *\n * Reading the feature value counts the visitor into the experiment, so only call this once the\n * visitor can actually see the thing being tested.\n */\nexport async function getGroup() {\n  let group: Group = 'select-option';\n\n  // The wrapper rejects when the GrowthBook runtime global is missing, and the value it resolves to\n  // is whatever string GrowthBook served, so we check it really is one of the groups. Either\n  // failure lands in the catch, and we still resolve to a group so the caller always has something\n  // to render.\n\n  try {\n    const value = await getFeatureValue('sale-page-quick-add-label', 'select-option');\n    assert(isGroup(value), 'Unexpected quick add label group');\n    group = <Group>value;\n  } catch (error) {\n    console.warn(error);\n  }\n\n  return group;\n}\n","// Imports\nvar ___HTML_LOADER_IMPORT_0___ = new URL(\"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\", import.meta.url);\n// Module\nvar code = \"<slot name=\\\"data\\\" hidden aria-hidden=\\\"true\\\"></slot><slot name=\\\"skeleton\\\" aria-hidden=\\\"true\\\"></slot><article class=\\\"container\\\" hidden><div style=\\\"display: flex; flex-flow: column; flex-grow: 1;\\\"><div style=\\\"position: relative;\\\"><div class=\\\"awards\\\" style=\\\"position: absolute; top: 0; left: 0; z-index: 1; pointer-events: none;\\\" hidden><div style=\\\"width: 80px; height: 80px; margin-right: 8px;\\\"><img alt=\\\"\\\" width=\\\"80\\\" height=\\\"80\\\" src=\\\"\" + ___HTML_LOADER_IMPORT_0___ + \"\\\" sizes=\\\"80px\\\" loading=\\\"eager\\\" style=\\\"width: 100%; height: auto;\\\"></div></div><div class=\\\"badges\\\" style=\\\"position: absolute; top: 10px; left: 10px; z-index: 1; pointer-events: none; flex-direction: column; align-items: flex-start; gap: 8px;\\\"><div class=\\\"badge main-badge\\\" hidden></div><div class=\\\"badge out-of-stock\\\" hidden>Out of Stock</div></div><slot name=\\\"savings-badge\\\" style=\\\"display: block; position: absolute; top: 10px; right: 10px; z-index: 1; pointer-events: none;\\\"></slot><div class=\\\"new-formula\\\" style=\\\"position: absolute; right: 0; bottom: 0; z-index: 1; pointer-events: none;\\\" hidden><svg viewBox=\\\"0 0 55 55\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\" style=\\\"display: block; width: 100%; height: 100%;\\\"><title>New formula</title><g clip-path=\\\"url(#clip0_175_206)\\\"><path d=\\\"M27.5933 55C42.5033 55 54.5933 42.91 54.5933 28C54.5933 13.09 42.5133 1 27.5933 1C12.6733 1 0.593262 13.08 0.593262 28C0.593262 42.92 12.6833 55 27.5933 55Z\\\" fill=\\\"#DEA5AD\\\"></path><path d=\\\"M21.6132 8.06001L20.8232 4.40001L21.4932 4.25001L23.7632 6.45001L23.2032 3.88001L23.8732 3.73001L24.6632 7.39001L23.9932 7.53001L21.7232 5.33001L22.2832 7.90001L21.6132 8.05001V8.06001Z\\\" fill=\\\"white\\\"></path><path d=\\\"M26.1235 7.28001L26.0835 3.54001L28.5235 3.51001V4.06001L26.7735 4.08001V5.11001L28.3835 5.09001V5.63001L26.7935 5.65001V6.73001L28.5635 6.71001V7.26001L26.1235 7.28001Z\\\" fill=\\\"white\\\"></path><path d=\\\"M30.1436 7.38001L29.9336 3.51001L30.6536 3.66001L30.7036 6.71001L32.0736 3.96001L32.8136 4.11001L32.9636 7.18001L34.2336 4.40001L34.9636 4.55001L33.2136 8.01001L32.4136 7.84001L32.2436 4.97001L30.9236 7.53001L30.1336 7.37001L30.1436 7.38001Z\\\" fill=\\\"white\\\"></path><path d=\\\"M11.4336 46.29L13.7636 43.36L15.6536 44.86L15.3136 45.29L13.9536 44.21L13.2936 45.03L14.3936 45.9L14.0536 46.32L12.9536 45.44L11.9536 46.7L11.4136 46.27V46.29H11.4336Z\\\" fill=\\\"white\\\"></path><path d=\\\"M16.8735 49.93C16.5335 49.77 16.2735 49.55 16.0935 49.28C15.9135 49.01 15.8135 48.71 15.7935 48.38C15.7735 48.05 15.8535 47.71 16.0135 47.36C16.1835 47.01 16.3935 46.75 16.6635 46.55C16.9335 46.35 17.2335 46.25 17.5535 46.22C17.8735 46.19 18.2135 46.26 18.5435 46.42C18.8735 46.58 19.1335 46.8 19.3235 47.07C19.5135 47.34 19.6035 47.64 19.6235 47.97C19.6235 48.3 19.5635 48.64 19.3935 48.98C19.2235 49.32 19.0135 49.59 18.7435 49.79C18.4735 49.98 18.1735 50.09 17.8535 50.12C17.5235 50.15 17.1935 50.08 16.8635 49.92L16.8735 49.93ZM17.1435 49.38C17.4635 49.54 17.7735 49.55 18.0635 49.43C18.3535 49.31 18.5935 49.06 18.7735 48.69C18.9535 48.32 18.9935 47.98 18.9035 47.68C18.8135 47.38 18.6135 47.14 18.2835 46.99C17.9635 46.83 17.6535 46.82 17.3535 46.94C17.0535 47.06 16.8235 47.31 16.6435 47.68C16.4635 48.05 16.4235 48.39 16.5135 48.69C16.6035 48.99 16.8135 49.23 17.1335 49.38H17.1435Z\\\" fill=\\\"white\\\"></path><path d=\\\"M20.9836 51.43L21.7836 47.77L23.1236 48.06C23.4136 48.12 23.6436 48.23 23.8136 48.37C23.9836 48.51 24.0936 48.68 24.1536 48.87C24.2136 49.06 24.2136 49.26 24.1736 49.46C24.1236 49.68 24.0236 49.87 23.8636 50.04C23.7036 50.2 23.4936 50.29 23.2336 50.32L23.6536 52.01L22.8836 51.84L22.5236 50.26L21.9636 50.14L21.6436 51.58L20.9736 51.43H20.9836ZM22.0736 49.65L22.7036 49.79C22.9236 49.84 23.1036 49.82 23.2336 49.74C23.3636 49.65 23.4436 49.52 23.4836 49.34C23.5236 49.17 23.5036 49.01 23.4236 48.89C23.3436 48.77 23.1836 48.67 22.9536 48.62L22.3236 48.48L22.0736 49.65Z\\\" fill=\\\"white\\\"></path><path d=\\\"M26.1935 52.33L26.0835 48.59L26.9035 48.57L28.1635 50.91L29.2735 48.51L30.0935 48.49L30.2035 52.23L29.5135 52.25L29.4435 49.67L28.4535 51.74L27.9135 51.76L26.8135 49.75L26.8835 52.33L26.1935 52.35V52.33Z\\\" fill=\\\"white\\\"></path><path d=\\\"M34.3235 51.38C34.0635 51.46 33.8035 51.47 33.5635 51.43C33.3235 51.39 33.1035 51.28 32.9135 51.1C32.7235 50.92 32.5835 50.67 32.4935 50.35L31.8535 48.13L32.5135 47.94L33.1635 50.16C33.2435 50.44 33.3735 50.63 33.5435 50.73C33.7235 50.83 33.9235 50.84 34.1635 50.77C34.4035 50.7 34.5635 50.58 34.6635 50.4C34.7635 50.22 34.7635 49.99 34.6835 49.71L34.0435 47.49L34.7035 47.3L35.3435 49.52C35.4335 49.85 35.4535 50.13 35.3835 50.39C35.3135 50.65 35.1835 50.85 34.9935 51.02C34.8035 51.19 34.5835 51.31 34.3135 51.39L34.3235 51.38Z\\\" fill=\\\"white\\\"></path><path d=\\\"M38.0034 49.95L36.2534 46.65L36.8634 46.33L38.3634 49.16L39.8234 48.38L40.0734 48.85L38.0034 49.95Z\\\" fill=\\\"white\\\"></path><path d=\\\"M41.8832 47.7L40.5332 43.95L41.1232 43.45L44.5732 45.43L44.0132 45.9L43.2232 45.43L42.1032 46.37L42.4232 47.23L41.8732 47.69L41.8832 47.7ZM41.9032 45.86L42.7332 45.16L41.3232 44.32L41.9132 45.85L41.9032 45.86Z\\\" fill=\\\"white\\\"></path><path d=\\\"M30.5033 15.29C30.5033 15.51 30.2933 17.75 31.8833 19.49C33.4733 21.23 34.5033 20.43 34.9833 24.93C35.4633 29.42 35.9433 37.32 34.9833 39.34C34.0233 41.36 27.5533 41.44 27.5533 41.44C27.5533 41.44 23.0733 42.31 21.6333 40.28C20.1933 38.26 20.8733 24.05 20.8733 24.05C20.8733 24.05 20.7433 21.81 22.1133 20.79C23.4833 19.77 24.3833 19.92 24.5833 18.47C24.7333 17.42 24.6833 16.31 24.7133 15.24C24.8433 15.13 24.8733 15.04 25.0533 15.03C26.9033 14.93 28.7733 14.95 30.6233 15.09C30.5833 15.12 30.5333 15.26 30.5033 15.29Z\\\" stroke=\\\"white\\\" stroke-width=\\\"0.7\\\" stroke-linecap=\\\"round\\\" stroke-linejoin=\\\"round\\\"></path><path d=\\\"M30.4732 15.12C30.7832 15.07 30.9532 14.9 31.0432 14.73C31.1232 14.59 31.1632 14.4 31.1732 14.25C31.2332 13.74 31.2132 13.21 31.1032 12.71C31.0832 12.57 30.9432 12.52 30.8432 12.49C29.0232 12.47 27.2032 12.46 25.3832 12.44C25.0232 12.44 24.5932 12.47 24.4232 12.79C24.3332 12.96 24.3232 13.15 24.3232 13.33C24.3232 13.75 24.3132 13.87 24.3032 14.28\\\" stroke=\\\"white\\\" stroke-width=\\\"0.7\\\" stroke-linecap=\\\"round\\\" stroke-linejoin=\\\"round\\\"></path><path d=\\\"M21.0732 24.13C23.1432 23.15 25.5332 23.66 27.7732 24.06C30.0132 24.46 32.5032 24.69 34.3632 23.31\\\" stroke=\\\"white\\\" stroke-width=\\\"0.7\\\" stroke-linecap=\\\"round\\\" stroke-linejoin=\\\"round\\\"></path><path d=\\\"M27.3935 30.86C27.3935 30.86 26.5235 29.17 25.4935 29.74C24.4635 30.31 24.6235 31.64 25.6835 33.19C26.1735 33.87 26.9835 34.35 27.5435 34.3C28.3335 34.24 28.8835 33.6 29.5435 32.51C30.0935 31.61 30.6935 29.89 29.6035 29.38C28.8735 29.04 27.7835 28.82 27.3635 30.86H27.3935Z\\\" stroke=\\\"white\\\" stroke-width=\\\"0.7\\\" stroke-linecap=\\\"round\\\" stroke-linejoin=\\\"round\\\"></path></g><defs><clipPath id=\\\"clip0_175_206\\\"><rect width=\\\"54.09\\\" height=\\\"54.19\\\" fill=\\\"white\\\" transform=\\\"translate(0.503418 0.809998)\\\"></rect></clipPath></defs></svg></div><slot name=\\\"discount-badge\\\" class=\\\"round-badge\\\" style=\\\"display: block; position: absolute; right: 0; bottom: 0; z-index: 1; pointer-events: none;\\\"></slot><clickable-area class=\\\"picture\\\" style=\\\"box-sizing: border-box; margin: 0 auto 16px; line-height: 0;\\\"><img width=\\\"400\\\" height=\\\"500\\\" src=\\\"\" + ___HTML_LOADER_IMPORT_0___ + \"\\\" alt=\\\"\\\" loading=\\\"eager\\\" style=\\\"width: 100%; height: auto;\\\"></clickable-area><slot name=\\\"gallery\\\" style=\\\"margin-bottom: 12px;\\\" hidden></slot></div><a class=\\\"link\\\" style=\\\"display: block; text-decoration: none; text-align: left; margin-bottom: 8px;\\\"><h3 class=\\\"title\\\" style=\\\"margin: 0; font-size: 1.25rem; font-weight: 700; color: var(--color-gray);\\\"></h3><p class=\\\"subtitle\\\" style=\\\"margin: 0; font-size: 1rem; font-weight: 400; line-height: 1.5; color: var(--color-gray);\\\"></p></a><slot name=\\\"rating-stars\\\"></slot><slot name=\\\"price\\\"></slot><slot name=\\\"variant-options\\\"></slot><slot name=\\\"discount-message\\\"></slot><div class=\\\"options\\\" style=\\\"font-size: 0.875rem; line-height: 1.5; color: #616161; padding-bottom: 8px; font-weight: 400;\\\"></div></div><slot name=\\\"add-to-cart-button\\\"></slot></article>\";\n// Exports\nexport default code;","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport { ready, type ReleaseHandle } from '../../../lib/dom-ready.js';\nimport { featureValueMatches } from '../../../lib/growthbook-feature.js';\nimport { getGroup } from '../experiment.js';\nimport html from './index.html';\nimport styles from './index.scss';\n\n/**\n * Renders a product tile (image, badges, title, price, CTA) for product grids such as collections,\n * search results, slideshows, and featured products.\n *\n * It exists to compose those pieces into one uniform tile so every listing surface presents\n * products the same way, rather than each surface assembling its own.\n *\n * Product data is gathered from nested product elements.\n *\n * It waits to render until that content exists and the document has loaded, because the nested\n * markup may not exist when the element first connects.\n *\n * The CTA depends on variant count, stock, and whether inline options are exposed. It is either an\n * add to cart element, a select option button that opens the variant selector modal, or a view\n * details link.\n */\nclass ProductCard extends HTMLElement implements CustomElement {\n  static IMAGE_ASPECT_RATIO = '4:5';\n\n  static IMAGE_SRCSET_WIDTHS = [315, 315 * 2, 315 * 3];\n\n  declare readonly dataset: {\n    /**\n     * Badge displayed on top of the product image\n     */\n    badgeType: Product['badge_type'];\n\n    /**\n     * Selected Shopify country code, it is used by the add to cart button element.\n     */\n    countryCode: string;\n\n    /**\n     * Currency ISO Code. Required.\n     */\n    currencyCode: string;\n\n    /**\n     * Currency Symbol. Required.\n     */\n    currencySymbol: string;\n\n    /**\n     * Email of currently signed in customer.\n     */\n    customerEmail: string;\n\n    /**\n     * Boolean represented as string to render variant options.\n     */\n    enableVariantOptions?: string;\n\n    /**\n     * Whether the product card has a media gallery to display in a carousel, boolean value\n     * represented as a string.\n     */\n    hasGallery: 'false' | 'true';\n\n    /**\n     * Defines is the add to bag element rendered (visible), boolean value.\n     */\n    hideBuyButton: string;\n\n    /**\n     * The ID of the product\n     *\n     * @see https://shopify.dev/docs/api/liquid/objects/product#product-id\n     */\n    id: string;\n\n    /**\n     * Boolean as a string which indicates whether the product card should render a \"Learn More\"\n     * link instead of an \"Add to Bag\" button and display the product's short description instead of\n     * the subtitle.\n     */\n    isLearnMore: 'false' | 'true';\n\n    /**\n     * Boolean as a string which puts the select option button into the Quick Add label A/B test, so\n     * its label comes from the sale-page-quick-add-label GrowthBook feature.\n     */\n    quickAddLabelExperiment: 'false' | 'true';\n\n    /**\n     * If data-select-option dataset attribute is passed in with a value, we want to replace the add\n     * to cart button with a select option button that will render a variant select modal when\n     * clicked.\n     */\n    selectOption: string;\n\n    /**\n     * A boolean represented as string to indicate product images should be squared versus standard\n     * aspect ratio of 4 / 5\n     */\n    squareImage: 'false' | 'true';\n\n    /**\n     * Space separated tag handles for a client side filter to match on. There is one per namespaced\n     * tag, taken from the part after the \"::\", e.g. \"curly\" for a \"Hair Type::Curly\" tag, and tags\n     * with no \"::\" are left out. Not rendered on a card in a carousel.\n     *\n     * @see https://shopify.dev/docs/api/liquid/objects/product#product-tags\n     */\n    tags: string;\n\n    /**\n     * The product's type as a handle, for a client side filter to match on, e.g. \"hair-dryer\". Only\n     * rendered where tags are, so a card in a carousel has none.\n     */\n    type: string;\n\n    /**\n     * The relative URL of the product\n     *\n     * @see https://shopify.dev/docs/api/liquid/objects/product#product-url\n     */\n    url: string;\n  };\n\n  private onCarouselMountedBound = this.onCarouselMounted.bind(this);\n  private onGalleryLoadedBound = this.onGalleryLoaded.bind(this);\n  private onSlideMoveStartedBound = this.onSlideMoveStarted.bind(this);\n  private onSelectOptionClickBound = this.onSelectOptionClick.bind(this);\n  private onSlotChangeBound = this.onSlotChange.bind(this);\n  private onProductVariantOptionChangedBound = this.onProductVariantOptionChanged.bind(this);\n  private onReadyBound = this.onReady.bind(this);\n  private product: Partial<Product> = {};\n\n  /**\n   * Whether the gallery's first image has loaded. We can hear about that before or after we handle\n   * the carousel's mount, so the swap to the gallery waits on both.\n   */\n  private galleryImageLoaded = false;\n\n  private declare releaseReady: ReleaseHandle;\n\n  public connectedCallback() {\n    this.initializeShadowRoot();\n\n    // this should work only for a product card with the exposed options selector, currently only in\n    // the featured products section.\n    if (this.dataset.enableVariantOptions === 'true') {\n      addEventListener('product-variant-option-changed', this.onProductVariantOptionChangedBound);\n    }\n\n    // The gallery's carousel sits in the image carousel's shadow root, and its events are composed,\n    // so they reach this element\n    this.addEventListener('carousel-element-mounted', this.onCarouselMountedBound);\n    this.addEventListener('carousel-slide-move-started', this.onSlideMoveStartedBound);\n    this.addEventListener('product-card-gallery-loaded', this.onGalleryLoadedBound);\n\n    this.releaseReady = ready(this.onReadyBound);\n  }\n\n  public disconnectedCallback() {\n    this.removeEventListener('carousel-element-mounted', this.onCarouselMountedBound);\n    this.removeEventListener('carousel-slide-move-started', this.onSlideMoveStartedBound);\n    this.removeEventListener('product-card-gallery-loaded', this.onGalleryLoadedBound);\n\n    const slot = this.shadowRoot.querySelector<HTMLSlotElement>('slot[name=\"data\"]');\n    slot?.removeEventListener('slotchange', this.onSlotChangeBound);\n\n    removeEventListener('product-variant-option-changed', this.onProductVariantOptionChangedBound);\n\n    if (this.releaseReady) {\n      this.releaseReady();\n    }\n  }\n\n  private initializeShadowRoot() {\n    if (!this.shadowRoot) {\n      this.attachShadow({ mode: 'open' });\n      this.shadowRoot.innerHTML = `<style>${styles}</style>${html}`;\n    }\n  }\n\n  private onReady() {\n    const slot = this.shadowRoot.querySelector<HTMLSlotElement>('slot[name=\"data\"]');\n    slot?.addEventListener('slotchange', this.onSlotChangeBound);\n\n    this.onSlotChange(null);\n  }\n\n  private onSlotChange(_event: Event) {\n    // this event is called when the slot is changed, but also when the element is destroyed this\n    // causes race conditions in exotic browsers, i.e. neither the slot contents, nor the shadowRoot\n    // contents are guaranteed to exist here\n\n    // only gather data and render if we have the slot properly filled\n    const productElement = this.querySelector('[slot=\"data\"]');\n    if (productElement) {\n      this.gatherData();\n      this.render();\n    }\n  }\n\n  private onProductVariantOptionChanged(event: WindowEventMap['product-variant-option-changed']) {\n    // Ignore main image updates if the product card has a gallery. We don't support PLP carousel\n    // per variant at the moment.\n    if (this.dataset.hasGallery === 'true') {\n      return;\n    }\n\n    if (this.product.id === event.detail.id) {\n      const element = this.querySelector<HTMLElement>(\n        `[slot=\"data\"] > [data-id=\"${event.detail.selected_variant_id}\"]`);\n      if (element) {\n        const url = element.dataset.imageSrc;\n        if (url) {\n          this.renderMainImage(url);\n        }\n      }\n    }\n  }\n\n  /**\n   * Gathers state into the product property and then calls render.\n   */\n  private gatherData() {\n    type VariantElement = HTMLElementTagNameMap['variant-element'];\n\n    const productElement = this.querySelector<HTMLElementTagNameMap['product-element']>(\n      '[slot=\"data\"]');\n    if (!productElement) {\n      return;\n    }\n\n    this.product.award_badge_image = productElement.dataset.awardBadgeImage;\n    this.product.award_tag = productElement.dataset.awardTag;\n    this.product.available = productElement.dataset.available === 'true';\n    this.product.badge_type = <BadgeTypeOptions>productElement.dataset.badgeType;\n    this.product.metafields_badge = productElement.dataset.metafieldsBadge;\n    this.product.metafields_discount_code = productElement.dataset.discountCode;\n    this.product.metafields_discount_percentage = productElement.dataset.discountPercentage ?\n      parseFloat(productElement.dataset.discountPercentage) :\n      0;\n    this.product.metafields_discount_override = productElement.dataset.discountOverride;\n    this.product.id = parseInt(productElement.dataset.id, 10);\n    this.product.lazyload = productElement.dataset.lazyload === 'true';\n    this.product.main_image = productElement.dataset.mainImage;\n    this.product.metafields_title = productElement.dataset.metafieldsTitle;\n    this.product.metafields_subtitle = productElement.dataset.metafieldsSubtitle;\n    this.product.reviews_average = productElement.dataset.reviewsAverage ?\n      parseFloat(productElement.dataset.reviewsAverage) : 0;\n    this.product.reviews_count = productElement.dataset.reviewsCount ?\n      parseInt(productElement.dataset.reviewsCount, 10) : 0;\n    this.product.short_description = productElement.dataset.shortDescription;\n    this.product.tags = productElement.dataset.tags;\n    this.product.title = productElement.dataset.title;\n    this.product.type = productElement.dataset.type;\n    this.product.url = productElement.dataset.url;\n    this.product.selected_variant_id = parseInt(productElement.dataset.selectedVariantId, 10);\n\n    this.product.option_groups = <Product['option_groups']>[];\n    type ProductOptionGroup = HTMLElementTagNameMap['product-option-group'];\n    const optionGroupElements = productElement.querySelectorAll<ProductOptionGroup>(\n      'product-option-groups product-option-group[data-name]');\n    for (const optionGroupElement of optionGroupElements) {\n      const optionGroup = <Product['option_groups'][0]>{};\n      optionGroup.name = optionGroupElement.dataset.name;\n      optionGroup.selected_option = optionGroupElement.dataset.selectedOption;\n      if (optionGroupElement.dataset.position) {\n        optionGroup.position = parseInt(optionGroupElement.dataset.position, 10);\n      }\n\n      if (optionGroupElement.dataset.useSwatchesForColor) {\n        optionGroup.use_swatches_for_color = optionGroupElement.dataset.useSwatchesForColor;\n      }\n\n      optionGroup.options = [];\n\n      type ProductOption = HTMLElementTagNameMap['product-option'];\n      const productOptionElements = optionGroupElement.querySelectorAll<ProductOption>(\n        'product-option[data-value]');\n\n      for (const productOptionElement of productOptionElements) {\n        const productOption = <Product['option_groups'][0]['options'][0]>{};\n        productOption.value = productOptionElement.dataset.value;\n        if (productOptionElement.dataset.color) {\n          productOption.color = productOptionElement.dataset.color;\n        }\n\n        productOption.variants = [];\n\n        const variantElements = productOptionElement.querySelectorAll<VariantElement>(\n          'variant-element[data-id]');\n\n        for (const variantElement of variantElements) {\n          const variant = <ProductVariant>{};\n          variant.available = variantElement.dataset.available === 'true';\n          variant.backorder = variantElement.dataset.backorder;\n          variant.backorder_selling_plan_id = variantElement.dataset.backorderSellingPlanId ?\n            parseInt(variantElement.dataset.backorderSellingPlanId, 10) : null;\n          variant.compare_at_price = variantElement.dataset.compareAtPrice ?\n            parseInt(variantElement.dataset.compareAtPrice, 10) : null;\n          variant.id = parseInt(variantElement.dataset.id, 10);\n          variant.price = parseInt(variantElement.dataset.price, 10);\n          variant.selected = variantElement.dataset.selected === 'true';\n          variant.sku = variantElement.dataset.sku;\n          variant.title = variantElement.dataset.title || productElement.dataset.title || '';\n          variant.sizeDescription = variantElement.dataset.sizeDescription || '';\n          variant.option1 = variantElement.dataset.option1 || '';\n          variant.option2 = variantElement.dataset.option2 || '';\n          variant.option3 = variantElement.dataset.option3 || '';\n\n          productOption.variants.push(variant);\n        }\n\n        optionGroup.options.push(productOption);\n      }\n\n      this.product.option_groups.push(optionGroup);\n    }\n\n    this.product.variants = [];\n\n    // We must isolate the query selector to children of the parent <product-element> custom element\n    // to ensure we exclude any <variant-element> child instances in the <product-option-groups>\n    // custom element.\n    const variantElements = this.querySelectorAll<VariantElement>(\n      'product-element[slot=\"data\"] > variant-element[data-id]');\n\n    for (const variantElement of variantElements) {\n      const variant = <ProductVariant>{};\n      variant.available = variantElement.dataset.available === 'true';\n      variant.backorder = variantElement.dataset.backorder;\n      variant.backorder_selling_plan_id = variantElement.dataset.backorderSellingPlanId ?\n        parseInt(variantElement.dataset.backorderSellingPlanId, 10) : null;\n      variant.compare_at_price = variantElement.dataset.compareAtPrice ?\n        parseInt(variantElement.dataset.compareAtPrice, 10) : null;\n      variant.hide_backorder_in_collection = variantElement.dataset.hideBackorderInCollection;\n      variant.id = parseInt(variantElement.dataset.id, 10);\n      variant.image_src = variantElement.dataset.imageSrc || '';\n      variant.metafield_compare_at_price_modal_content =\n        variantElement.dataset.compareAtPriceModalContent || '';\n      variant.option1 = variantElement.dataset.option1;\n      variant.option2 = variantElement.dataset.option2;\n      variant.option3 = variantElement.dataset.option3;\n      variant.price = parseInt(variantElement.dataset.price, 10);\n      variant.selected = variantElement.dataset.selected === 'true';\n      variant.sku = variantElement.dataset.sku;\n      variant.title = variantElement.dataset.title || productElement.dataset.title || '';\n      this.product.variants.push(variant);\n    }\n  }\n\n  private render() {\n    // this.product is guaranteed to be defined here because it is default initialized to an empty\n    // object and gatherData only populates the existing object's properties and never does an\n    // unsafe reassignment.\n\n    // gatherData() did not return a valid product (yet)\n    if (!this.product.id) {\n      return;\n    }\n\n    this.renderAwards();\n    this.renderMainBadge();\n    this.renderOutOfStockBadge();\n    this.renderNewFormulaOverlay();\n\n    // Always render the main image first as a fallback; onCarouselMounted() swaps to the gallery.\n    this.renderMainImage();\n\n    this.renderTitles();\n    this.renderProductOptions();\n    this.renderAddToCart();\n    this.renderLinks();\n\n    const addToCartButton = this.querySelector('[slot=\"add-to-cart-button\"]');\n    if (this.dataset.enableVariantOptions !== 'true' && !addToCartButton) {\n      // In the case when variant options are rendered the \"Add to Bag\" button is passed to the slot\n      this.renderSelectOptionButton();\n    } else {\n      this.renderViewDetailsLink();\n    }\n\n    this.revealCard();\n  }\n\n  /**\n   * Swaps the skeleton out for the card. The card starts hidden so the skeleton stays on screen\n   * until there is real content to replace it, rather than handing over to an empty card.\n   */\n  private revealCard() {\n    const skeleton = this.shadowRoot.querySelector<HTMLSlotElement>('slot[name=\"skeleton\"]');\n    if (skeleton) {\n      skeleton.hidden = true;\n    }\n\n    const container = this.shadowRoot.querySelector<HTMLElement>('.container');\n    if (container) {\n      container.hidden = false;\n    }\n  }\n\n  private renderAwards() {\n    const awardsContainer = this.shadowRoot.querySelector<HTMLElement>('.awards');\n    if (!awardsContainer) {\n      return;\n    }\n\n    if (this.product.award_tag && this.product.award_badge_image) {\n      const badge = awardsContainer.querySelector('img');\n      badge.alt = this.product.award_tag;\n      badge.src = this.product.award_badge_image;\n      awardsContainer.hidden = false;\n    }\n  }\n\n  /**\n   * Renders the main badge for the product which can be derived from the legacy product.badge_type\n   * property or the new product.metafields_badge property. The metafields badge takes precedence\n   * over the legacy badge type and once all products have been migrated over to from product tags\n   * to product metafields, the legacy badge type consideration will be removed.\n   */\n  private renderMainBadge() {\n    const badge = this.shadowRoot.querySelector<HTMLElement>('.main-badge');\n    const hasBadgeCopy = this.product.badge_type || this.product.metafields_badge;\n    if (!badge || !hasBadgeCopy || !this.product.available) {\n      return;\n    }\n\n    let badgeTitle = getBadgeTitle(this.product.badge_type);\n\n    // If present, the product metafields badge takes precedence over the legacy badge type\n    if (this.product.metafields_badge) {\n      badgeTitle = this.product.metafields_badge;\n    }\n\n    badge.dataset.badgeType = badgeTitle;\n    badge.textContent = badgeTitle;\n    badge.hidden = false;\n  }\n\n  private renderOutOfStockBadge() {\n    const badge = this.shadowRoot.querySelector<HTMLElement>('.out-of-stock');\n    if (!badge) {\n      return;\n    }\n\n    // This is the out of stock badge, so it shows when the product is not available. That flip is\n    // why this looks awkward.\n    //\n    // - if the product is available, then the oos badge should be hidden\n    // - if the product is unavailable, then the oos badge should be visible\n\n    badge.hidden = this.product.available;\n  }\n\n  private renderProductOptions() {\n    const productOptionsEl = this.shadowRoot.querySelector<HTMLElement>('.options');\n\n    if (this.product.type === 'bundle') {\n      return;\n    }\n\n    if (this.product.variants.length <= 1) {\n      return;\n    }\n\n    if (!productOptionsEl || !this.product.option_groups.length) {\n      return;\n    }\n\n    const options: string[] = [];\n\n    for (const optionGroup of this.product.option_groups) {\n      const text = `${optionGroup.options.length} ${optionGroup.name.toLowerCase()}s`;\n\n      if (optionGroup.options.length <= 1) {\n        continue;\n      }\n\n      options.push(text);\n    }\n\n    productOptionsEl.innerText = options.join(' & ');\n  }\n\n  private renderNewFormulaOverlay() {\n    // Don't show new formula overlay if the product already has a \"new\" batch.\n    if (this.product.badge_type === 'new') {\n      return;\n    }\n\n    // Not all products have tags or tags may not be a defined string, so use optional chaining\n\n    // Add new formula overlay\n    if (this.product.tags?.includes('new-formula')) {\n      const container = this.shadowRoot.querySelector<HTMLElement>('.new-formula');\n\n      if (container) {\n        container.hidden = false;\n      } else {\n        console.log('<product-card> cannot render new formula overlay');\n      }\n    }\n  }\n\n  private renderMainImage(imageSrc?: string) {\n    const newImageSrc = imageSrc || this.product.main_image;\n\n    const picture = this.shadowRoot.querySelector<HTMLPictureElement>('.picture');\n    if (!picture) {\n      console.log('<product-card> element not found .picture');\n      return;\n    }\n\n    const image = picture.querySelector('img');\n\n    if (this.dataset.squareImage === 'true') {\n      image.height = 400;\n    } else {\n      image.height = 500;\n    }\n\n    // We are intentionally only setting the loading attribute for lazy loaded images. This is to\n    // mitigate issues loading images in Safari when loading attribute value is explicitly set to\n    // `eager`. Since the default value is eager and safari can load the images properly when not\n    // set; we are not setting it, if the image is not to be lazy loaded.\n    if (this.product.lazyload) {\n      image.loading = 'lazy';\n      image.setAttribute('fetchpriority', 'low');\n    }\n\n    if (newImageSrc) {\n      image.srcset = this.generateSrcSet(newImageSrc, ProductCard.IMAGE_SRCSET_WIDTHS);\n    } else {\n      console.warn('cannot render the main image, there is no image source');\n    }\n\n    image.setAttribute('sizes',\n      '(min-width: 1024px) 25vw, (min-width: 768px) 33.3vw, (min-width: 320px) 50vw, 100vw');\n\n    if (image.dataset.loaded !== 'true') {\n      this.holdSkeletonImage(picture);\n\n      if (image.complete) {\n        image.dataset.loaded = 'true';\n      } else {\n        image.addEventListener('load', revealImage);\n        image.addEventListener('error', revealImage);\n      }\n    }\n  }\n\n  /**\n   * In the first row the skeleton has already painted the product image by the time we render, and\n   * our own image starts transparent until it loads, so the photo would blink out and fade back in.\n   * We put the skeleton's copy behind ours as a background so the photo stays on screen until ours\n   * fades in over it. Further down the page the skeleton has no real image, so there is nothing to\n   * hold.\n   */\n  private holdSkeletonImage(picture: HTMLElement) {\n    const skeletonImage = this.querySelector('[slot=\"skeleton\"] img');\n    if (!(skeletonImage instanceof HTMLImageElement)) {\n      return;\n    }\n\n    const src = skeletonImage.currentSrc;\n    if (src && !src.startsWith('data:')) {\n      picture.style.backgroundImage = `url(\"${src}\")`;\n    }\n  }\n\n  /**\n   * Hides the static product image and reveals the carousel gallery slot.\n   */\n  private renderGallery() {\n    const gallery = this.shadowRoot.querySelector<HTMLSlotElement>('slot[name=\"gallery\"]');\n    if (!gallery) {\n      return;\n    }\n\n    gallery.hidden = false;\n    delete gallery.dataset.loading;\n\n    const picture = this.shadowRoot.querySelector<HTMLElement>('.picture');\n    if (picture) {\n      picture.hidden = true;\n    }\n  }\n\n  /**\n   * Swaps the static product image for the carousel once it has mounted and its first image has\n   * loaded. The main image is always rendered first as a fallback so the user sees something before\n   * the carousel JS loads.\n   *\n   * The slide images are lazy, so the first one does not start loading until the carousel is shown.\n   * Until it loads, we lay the carousel out invisibly under the static image, which keeps the photo\n   * on screen while still letting the image load.\n   */\n  private onCarouselMounted() {\n    if (this.galleryImageLoaded) {\n      this.renderGallery();\n      return;\n    }\n\n    const gallery = this.shadowRoot.querySelector<HTMLSlotElement>('slot[name=\"gallery\"]');\n    if (gallery) {\n      gallery.dataset.loading = 'true';\n      gallery.hidden = false;\n    }\n  }\n\n  private onGalleryLoaded() {\n    this.galleryImageLoaded = true;\n\n    // When the image was already loaded, the image carousel tells us from its own mount handler,\n    // which runs before ours. The gallery is still hidden then, so our mount handler does the swap.\n    const gallery = this.shadowRoot.querySelector<HTMLSlotElement>('slot[name=\"gallery\"]');\n    if (gallery && !gallery.hidden) {\n      this.renderGallery();\n    }\n  }\n\n  /**\n   * Hides badges when the carousel starts moving away from the first slide and shows them when it\n   * starts moving back. Badges overlay the product image and would cover unrelated carousel slides.\n   */\n  private onSlideMoveStarted(event: GlobalEventHandlersEventMap['carousel-slide-move-started']) {\n    const badges = this.shadowRoot.querySelector<HTMLElement>('.badges');\n    if (!badges) {\n      return;\n    }\n\n    if (event.detail.index === 0) {\n      badges.hidden = false;\n    } else {\n      badges.hidden = true;\n    }\n  }\n\n  private renderTitles() {\n    const title = this.shadowRoot.querySelector<HTMLElement>('.title');\n\n    if (title) {\n      title.dataset.productTitle = this.product.metafields_title || this.product.title;\n      title.textContent = this.product.metafields_title || this.product.title;\n    }\n\n    const subtitle = this.shadowRoot.querySelector<HTMLElement>('.subtitle');\n\n    if (subtitle) {\n      if (this.dataset.isLearnMore === 'true' && this.product.short_description) {\n        subtitle.dataset.productTitle = this.product.short_description;\n        subtitle.innerHTML = decode(this.product.short_description);\n      } else if (this.product.metafields_subtitle) {\n        subtitle.dataset.productTitle = this.product.metafields_subtitle;\n        subtitle.innerHTML = decode(this.product.metafields_subtitle);\n      } else {\n        subtitle.hidden = true;\n      }\n    }\n  }\n\n  private renderAddToCart() {\n    if (this.dataset.hideBuyButton === 'true') {\n      return;\n    }\n\n    if (this.shouldRenderSelectOptionButton()) {\n      return;\n    }\n\n    let selectedVariant = this.product.variants.find(variant =>\n      variant.id === this.product.selected_variant_id);\n\n    if (!selectedVariant) {\n      selectedVariant = this.product.variants[0];\n    }\n\n    if (!selectedVariant) {\n      return;\n    }\n\n    let addToCart = this.shadowRoot.querySelector('add-to-cart');\n\n    if (addToCart) {\n      // We may be re-rendering the add-to-cart element, which may already have a product-element\n      // element. Reset the content of the element since we are about to append a product-element\n      // again\n      addToCart.innerHTML = '';\n    } else {\n      addToCart = document.createElement('add-to-cart');\n      addToCart.dataset.country = this.dataset.countryCode;\n      addToCart.dataset.productListingDisplay = 'true';\n      addToCart.dataset.backorder = selectedVariant.backorder;\n      addToCart.dataset.discountCode = this.product.metafields_discount_code;\n      if (this.dataset.customerEmail) {\n        addToCart.dataset.customerEmail = this.dataset.customerEmail;\n      }\n\n      const container = this.shadowRoot.querySelector('.container');\n\n      if (container) {\n        container.appendChild(addToCart);\n      } else {\n        console.log('Element matching \".container\" not found');\n      }\n    }\n\n    const product = document.createElement('product-element');\n    product.slot = 'product';\n    product.dataset.id = `${this.product.id}`;\n    product.dataset.available = `${this.product.available}`;\n    product.dataset.url = this.product.url;\n    product.dataset.title = this.product.title;\n    product.dataset.type = this.product.type;\n    product.dataset.selectedVariantId = `${selectedVariant.id}`;\n    product.dataset.tags = this.product.tags;\n\n    for (const variant of this.product.variants) {\n      const variantElement = document.createElement('variant-element');\n      variantElement.dataset.id = `${variant.id}`;\n      variantElement.dataset.available = `${variant.available}`;\n      variantElement.dataset.sku = variant.sku;\n      variantElement.dataset.title = variant.title;\n\n      if (variant.backorder) {\n        variantElement.dataset.backorder = variant.backorder;\n        if (variant.hide_backorder_in_collection) {\n          variantElement.dataset.hideBackorderInCollection = variant.hide_backorder_in_collection;\n        }\n      }\n\n      if (this.product.tags?.includes('Contains Alcohol')) {\n        const attributeEl = document.createElement('custom-attribute');\n        attributeEl.dataset.key = '_contains_alcohol';\n        attributeEl.dataset.value = 'true';\n        variantElement.appendChild(attributeEl);\n      }\n\n      if (this.product.tags?.includes('compatibility-soleil')) {\n        const attributeEl = document.createElement('custom-attribute');\n        attributeEl.dataset.key = '_compatibility';\n        attributeEl.dataset.value = 'soleil';\n        variantElement.appendChild(attributeEl);\n      }\n\n      if (this.product.tags?.includes('compatibility-max-high-volume')) {\n        const attributeEl = document.createElement('custom-attribute');\n        attributeEl.dataset.key = '_compatibility';\n        attributeEl.dataset.value = 'max-high-volume';\n        variantElement.appendChild(attributeEl);\n      }\n\n      if (this.product.tags?.includes('final-sale')) {\n        const attributeEl = document.createElement('custom-attribute');\n        attributeEl.dataset.key = '_final_sale';\n        attributeEl.dataset.value = 'true';\n        variantElement.appendChild(attributeEl);\n\n        // signals to our returns vendor Loop that this line item is final sale and not eligible for\n        // return, mirroring add-to-cart.liquid\n        const loopAttributeEl = document.createElement('custom-attribute');\n        loopAttributeEl.dataset.key = '_LPROP';\n        loopAttributeEl.dataset.value = 'final-sale';\n        variantElement.appendChild(loopAttributeEl);\n      }\n\n      product.appendChild(variantElement);\n    }\n\n    addToCart.appendChild(product);\n\n    // if the product has selling plans we need to add it to the item properties, so that we can\n    // upgrade to a subscription in the cart\n    const subscriptionPlansData = this.querySelector<HTMLScriptElement>(\n      '.subscription-plans-options-data');\n    if (subscriptionPlansData) {\n      const script = <HTMLScriptElement>subscriptionPlansData.cloneNode(true);\n      script.slot = 'subscription-plans';\n      addToCart.appendChild(script);\n    }\n\n    addToCart.dataset.full = 'true';\n    addToCart.dataset.update = `${Date.now()}`;\n  }\n\n  private renderSelectOptionButton() {\n    if (!this.shouldRenderSelectOptionButton()) {\n      return;\n    }\n\n    const selectOptionButton = this.shadowRoot.querySelector('.primary-button');\n\n    if (selectOptionButton) {\n      // button already exists, does not need to be re-rendered\n      return;\n    }\n\n    const button = document.createElement('button');\n    button.classList.add('primary-button');\n\n    if (this.dataset.quickAddLabelExperiment === 'true') {\n      const loader = document.createElement('loading-indicator');\n      button.appendChild(loader);\n      this.renderExperimentLabel(button).catch(console.warn);\n    } else {\n      this.renderSelectOptionLabel(button);\n    }\n\n    button.classList.add('full');\n\n    button.addEventListener('click', this.onSelectOptionClickBound);\n\n    const container = this.shadowRoot.querySelector('.container');\n    if (container) {\n      container.appendChild(button);\n    } else {\n      console.log('could not find container to append button');\n    }\n  }\n\n  /**\n   * Labels the select option button for the visitor's group in the Quick Add label test.\n   *\n   * The button shows a loading indicator until the group comes back, so nobody in the Quick Add\n   * group sees Select Option flash first. Setting the label replaces the loading indicator. The\n   * button has a fixed height and full width, so the swap does not change its size.\n   */\n  private async renderExperimentLabel(button: HTMLButtonElement) {\n    const group = await getGroup();\n\n    if (featureValueMatches(group, 'quick-add')) {\n      button.innerText = 'Quick Add';\n      button.setAttribute('aria-label', ['Quick add', this.product.title].join(' '));\n    } else {\n      this.renderSelectOptionLabel(button);\n    }\n  }\n\n  private renderSelectOptionLabel(button: HTMLButtonElement) {\n    button.innerText = 'Select Option';\n    button.setAttribute('aria-label', ['Select option for', this.product.title].join(' '));\n  }\n\n  private renderViewDetailsLink() {\n    const viewDetailsLink = this.shadowRoot.querySelector('.view-details-link');\n    if (viewDetailsLink) {\n      // button already exists, does not need to be re-rendered\n      return;\n    }\n\n    const newViewDetailsLink = document.createElement('a');\n    newViewDetailsLink.href = this.getProductUrl();\n    newViewDetailsLink.innerText = 'View Details';\n    newViewDetailsLink.classList.add('view-details-link');\n\n    const container = this.shadowRoot.querySelector('.container');\n    container.appendChild(newViewDetailsLink);\n  }\n\n  private renderLinks() {\n    const productUrl = this.getProductUrl();\n\n    const links = this.shadowRoot.querySelectorAll<HTMLAnchorElement>('.link');\n    for (const link of links) {\n      link.href = productUrl;\n    }\n\n    const picture = this.shadowRoot.querySelector<HTMLElementTagNameMap['clickable-area']>(\n      '.picture');\n    if (picture) {\n      picture.dataset.url = productUrl;\n    }\n  }\n\n  private shouldRenderSelectOptionButton() {\n    // Although selectOption is true we still want to render \"Add to cart\" button in the case when\n    // the product is out of stock to show \"Notify me\" button. The \"Notify me\" button is a part of\n    // the \"Add to cart\" button implementation.\n\n    const selectOption = this.dataset.selectOption === 'true';\n    const isOutOfStock = this.product.variants.every(variant => !variant.available);\n\n    return this.product.variants.length > 1 && selectOption && !isOutOfStock;\n  }\n\n  private onSelectOptionClick(event: MouseEvent) {\n    if (!(event.currentTarget instanceof HTMLElement)) {\n      return;\n    }\n\n    const focusEvent = new Event('focus-stack-push-requested', {\n      bubbles: true,\n      composed: true\n    });\n    event.currentTarget.dispatchEvent(focusEvent);\n\n    type Detail = WindowEventMap['select-option-requested']['detail'];\n    const requestEvent = new CustomEvent<Detail>('select-option-requested', {\n      bubbles: true,\n      composed: true,\n      detail: {\n        country: this.dataset.countryCode,\n        product: this.product\n      }\n    });\n\n    this.dispatchEvent(requestEvent);\n  }\n\n  private getProductUrl() {\n    // We must use the URL object as Shopify sometimes appends query parameters to the product.url\n    // property, such as when presenting search results.\n    const productUrl = new URL(this.product.url, location.origin);\n    if (this.product.variants.length > 1 && this.product.selected_variant_id) {\n      productUrl.searchParams.set('variant', this.product.selected_variant_id.toString());\n    }\n\n    // Marketing can use offer override templates and metaobjects to create unique pricing and\n    // discount codes. We must check if the view query string parameter exists and contains the word\n    // \"offer\" and if so also append the value to product URL.\n    const url = new URL(location.href, 'https://langehair.com');\n    const viewParam = url.searchParams.get('view');\n    if (viewParam?.includes('offer')) {\n      productUrl.searchParams.set('view', viewParam);\n    }\n\n    return productUrl.toString();\n  }\n\n  /**\n   * Calculates the image height based on width and the squareImage setting. Returns width for\n   * square images, or calculates height using the standard aspect ratio (4:5) for non-square\n   * images.\n   */\n  private calculateImageHeight(imageWidth: number) {\n    if (this.dataset.squareImage === 'true') {\n      return imageWidth;\n    }\n\n    const [width, height] = ProductCard.IMAGE_ASPECT_RATIO.split(':');\n    return Math.round(imageWidth / parseInt(width, 10) * parseInt(height, 10));\n  }\n\n  /**\n   * Create a responsive srcset value for images\n   */\n  private generateSrcSet(url: string, responsiveSteps: number[]) {\n    if (!url) {\n      return '';\n    }\n\n    const output = [];\n    for (let i = 0; i < responsiveSteps.length; i++) {\n      if (url.includes('height=') && url.includes('width=')) {\n        output.push(url\n          .replace(/height=\\d+/, `height=${this.calculateImageHeight(responsiveSteps[i])}`)\n          .replace(/width=\\d+/, `width=${responsiveSteps[i]}`)\n          .concat(` ${responsiveSteps[i]}w`));\n      } else {\n        output.push(`${url}&width=${responsiveSteps[i]}&height=` +\n          `${this.calculateImageHeight(responsiveSteps[i])} ${i + 1}x`);\n      }\n    }\n\n    return output.join(',');\n  }\n}\n\n/**\n * Reveals the main product image once it has loaded so the initial paint eases in through the\n * stylesheet transition rather than hard-cutting from the transparent placeholder. Wired on both\n * load and error so a failed image still surfaces instead of staying invisible, and detaches itself\n * from both either way.\n */\nfunction revealImage(event: Event) {\n  if (event.currentTarget instanceof HTMLImageElement) {\n    event.currentTarget.dataset.loaded = 'true';\n    event.currentTarget.removeEventListener('load', revealImage);\n    event.currentTarget.removeEventListener('error', revealImage);\n  }\n}\n\n/**\n * Quickly do a partial decoding of html entities in a string.\n *\n * To encode is to replace characters with a code. To decode is to replace the code with the\n * character.\n *\n * This is written to tolerate non-string inputs. Any non-string input such as undefined or null or\n * any other value is returned as is.\n *\n * This is not a complete decoding. This only decodes some of the codes we actually witness in the\n * data.\n */\nfunction decode(value: string) {\n  if (typeof value === 'string') {\n    return value.replace(/&amp;/g, '&').replace(/&lt;/, '<').replace(/&gt;/, '>');\n  }\n\n  return value;\n}\n\nfunction getBadgeTitle(badgeType: string) {\n  let badgeTitle = '';\n  switch (badgeType) {\n    case 'best-seller':\n      badgeTitle = 'Best Seller';\n      break;\n    case 'new':\n      badgeTitle = 'New';\n      break;\n    case 'favorite':\n      badgeTitle = 'Staff\\'s Favorite';\n      break;\n    case 'travel-friendly':\n      badgeTitle = 'Travel Friendly';\n      break;\n  }\n\n  return badgeTitle;\n}\n\n/**\n * Represents a Shopify product variant\n */\ninterface ProductVariant {\n  /**\n   * Whether the variant is in stock\n   */\n  available: boolean;\n\n  /**\n   * Whether the variant is on backorder\n   */\n  backorder: string;\n\n  /**\n   * The \"Pre-Order\" deferred payment selling plan ID associated with a backordered variant.\n   */\n  backorder_selling_plan_id: number;\n\n  /**\n   * The MSRP, or original sales price before discounts, per unit\n   */\n  compare_at_price: number;\n\n  /**\n   * The currency code associated with the price\n   */\n  currency_code: string;\n\n  /**\n   * Boolean as a string to indicate whether to hide any backorder message if the product card is\n   * being viewed in a collection context.\n   */\n  hide_backorder_in_collection: string;\n\n  /**\n   * Variant id\n   *\n   * @example 12345568943\n   */\n  id: number;\n\n  /**\n   * The URL for the variant image\n   */\n  image_src: string;\n\n  /**\n   * Content to be displayed in the compare at price modal for a particular variant\n   */\n  metafield_compare_at_price_modal_content: string;\n\n  /**\n   * The value of the variant for the first product option. If there's no first product option, then\n   * nil is returned.\n   */\n  option1: string;\n\n  /**\n   * The value of the variant for the second product option. If there's no first product option,\n   * then nil is returned.\n   */\n  option2: string;\n\n  /**\n   * The value of the variant for the third product option. If there's no first product option, then\n   * nil is returned.\n   */\n  option3: string;\n\n  /**\n   * The price of the variant represented in cents\n   */\n  price: number;\n\n  /**\n   * Whether this variant is the currently selected variant out of all the variants for the product\n   */\n  selected: boolean;\n\n  /**\n   * The short description associated with the variant size option.\n   */\n  sizeDescription: string;\n\n  /**\n   * The Stock Keeping Unit associated with the variant. This is another kind of product id.\n   */\n  sku: string;\n\n  /**\n   * The title of the variant.\n   */\n  title: string;\n}\n\ntype BadgeTypeOptions = '' | 'best-seller' | 'favorite' | 'new' | 'travel-friendly';\ninterface Product {\n  /**\n   * Product inventory availability\n   */\n  available: boolean;\n\n  /**\n   * Product award image\n   */\n  award_badge_image: string;\n\n  /**\n   * Product award tag\n   */\n  award_tag: string;\n\n  /**\n   * Product badge type\n   */\n  badge_type: BadgeTypeOptions;\n\n  /**\n   * Bundles created using the Shopify app use an abstraction product created by the application to\n   * represent the bundle. If a product's variant options are being presented in the context of a\n   * product in a bundle this attribute identifies the product ID for the abstraction product\n   * created by the Shopify Bundle App to represent that bundle.\n   */\n  bundle_product_id: number;\n\n  /**\n   * Shopify product id\n   */\n  id: number;\n\n  /**\n   * Product main image loading method\n   */\n  lazyload: boolean;\n\n  /**\n   * Product main image\n   */\n  main_image: string;\n\n  /**\n   * Product metafield with string value to display instead of the legacy badge type driven by tags.\n   */\n  metafields_badge: string;\n\n  /**\n   * Product metafields discount code (used to promote public discount codes)\n   */\n  metafields_discount_code: string;\n\n  /**\n   * Product metafields discount override to use one-off messages instead of generated text\n   */\n  metafields_discount_override: string;\n\n  /**\n   * Product metafields discount percentage (used to render public discount code prices)\n   */\n  metafields_discount_percentage: number;\n\n  /**\n   * Product metafields subtitle\n   */\n  metafields_subtitle: string;\n\n  /**\n   * Product metafields title\n   */\n  metafields_title: string;\n\n  option_groups: {\n    name: string;\n    options: {\n      color?: string;\n      value: string;\n      variants: Partial<{\n        /**\n         * Whether the variant has inventory in stock and is available for sale.\n         */\n        available: boolean;\n\n        backorder_message: string;\n\n        compare_at_price: number;\n\n        customAttributes: {\n          /**\n           * Key or name of the attribute.\n           */\n          key: string;\n\n          /**\n           * Value of the attribute.\n           */\n          value: string;\n        }[];\n\n        hideBackorderInCollection: string;\n\n        /**\n         * Variant id\n         */\n        id: number;\n\n        price: number;\n\n        sku: string;\n\n        title: string;\n      }>[];\n    }[];\n    position?: number;\n    selected_option: string;\n    use_swatches_for_color?: 'false' | 'true';\n  }[];\n\n  /**\n   * Product yotpo reviews average value\n   */\n  reviews_average: number;\n\n  /**\n   * Product yotpo reviews count\n   */\n  reviews_count: number;\n\n  /**\n   * Selected variant id\n   */\n  selected_variant_id: number;\n\n  /**\n   * A single sentence description of the product, used instead of description in some contests.\n   */\n  short_description: string;\n\n  /**\n   * Product sku\n   */\n  sku: string;\n\n  /**\n   * Product tags\n   */\n  tags: string;\n\n  /**\n   * Product title\n   */\n  title: string;\n\n  /**\n   * Product type\n   *\n   * @example \"hair-care\"\n   */\n  type: string;\n\n  /**\n   * Shopify product url\n   */\n  url: string;\n\n  /**\n   * A list of the product's variants with some information about variant\n   */\n  variants: ProductVariant[];\n}\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'product-card': ProductCard;\n  }\n}\n\nif (!customElements.get('product-card')) {\n  customElements.define('product-card', ProductCard);\n}\n","/**\n * Releases the handle returned from the ready() function.\n *\n * This is idempotent. Additional calls are pointless but harmless.\n */\nexport type ReleaseHandle = () => void;\n\n/**\n * A subset of events that pertain to document ready state.\n */\ntype LoadingEventMap = Pick<DocumentEventMap, 'DOMContentLoaded' | 'load'>;\n\n/**\n * Calls the provided callback when the document is ready. This works both when the document is\n * already ready and when it is not, so callers do not have to care. This exists because it ensures\n * that each caller consistently expresses its dependency on document readiness, and to save a bit\n * of boilerplate.\n *\n * A document is considered ready when it has been parsed. This corresponds to the ready state of\n * 'interactive'. When the document has been fully loaded, the ready state is 'complete'. The\n * callback runs either way, so callers do not have to check which one they are in.\n *\n * Ready means the html is parsed and the DOM is there to query. It does not mean the page's other\n * scripts have run. Deferred scripts and module scripts run after the ready state turns\n * 'interactive' and before DOMContentLoaded fires, so a callback here can run before them. When a\n * caller needs some other script to have run already, this is the wrong tool, and it should wait on\n * whatever that script produces instead.\n *\n * That remedy has a catch of its own when the calling bundle is loaded with async. An async bundle\n * can execute after DOMContentLoaded has already fired, so a plain listener for that event never\n * runs and the work silently never happens. Nothing here can detect that, because by the time this\n * file executes the event is long gone. Catching it needs something registered at parse time,\n * before any bundle loads.\n *\n * There is a second trap right next to that one, and it applies only when waiting for\n * DOMContentLoaded. The ready state stays 'interactive' after DOMContentLoaded has fired, all the\n * way until 'complete', so the ready state cannot tell you whether DOMContentLoaded is still coming\n * or has already gone. Waiting for load is not exposed to this, because the ready state turns\n * 'complete' and load fires in the same task, so seeing 'complete' means load has fired too.\n *\n * When the document is already ready, the callback is invoked via setTimeout. This way, if the\n * callback throws, the error surfaces to the global error handler in a way that is similar to what\n * happens when an event listener throws.\n *\n * We might one day want to move this to an NPM library.\n *\n * Beware of a quirk here. setTimeout yields, and the busier the frontend, the longer it takes to\n * fire. Page load is exactly when the frontend is busiest. The timeout is a floor and not a\n * guarantee, so there is no real bound on how long that yield lasts. What this means is that using\n * this library can introduce more delay than you might expect. Compare code that runs on async load\n * and checks ready state and invokes immediately to code that runs after setTimeout. The former\n * runs in the same tick, the latter runs in \"some later tick\". That later tick can be surprisingly\n * much later. That can cause initial paint issues.\n *\n * @see https://developer.mozilla.org/en-US/docs/Web/API/Document/readyState\n * @see https://developer.mozilla.org/en-US/docs/Web/API/Document/DOMContentLoaded_event\n * @see https://html.spec.whatwg.org/multipage/parsing.html#the-end\n */\nexport function ready(callback: (event?: Event) => void,\n  type: keyof LoadingEventMap = 'DOMContentLoaded') {\n  if (document.readyState === 'complete' || (type === 'DOMContentLoaded' &&\n    document.readyState === 'interactive')) {\n    const timer = setTimeout(callback, 0);\n    const handle: ReleaseHandle = () => clearTimeout(timer);\n    return handle;\n  } else {\n    addEventListener(type, callback);\n    const handle: ReleaseHandle = () => removeEventListener(type, callback);\n    return handle;\n  }\n}\n","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport styles from './index.scss';\n\n/**\n * Shows how much a product is discounted, as a badge a product card places on its image on the sale\n * pages. Big savings read better as an amount and small ones as a percentage, so the badge shows\n * \"$60 OFF\" once the savings reach a threshold set in the theme settings, and \"20% OFF\" below it.\n *\n * A product can be discounted by a discount code, by a compare at price, or both. The savings are\n * measured against the compare at price the same way product-discount-message measures them, so the\n * badge agrees with the discount message under the price.\n */\nclass SaleDiscountBadge extends HTMLElement implements CustomElement {\n  declare readonly dataset: {\n    /**\n     * The savings, in whole currency units, at which the badge switches from a percentage to an\n     * amount. When absent, the badge always shows a percentage.\n     *\n     * @example \"50\"\n     */\n    amountThreshold: string;\n\n    /**\n     * The compare at price in subunits. When absent, the price is the compare at price.\n     */\n    compareAtPrice: string;\n\n    /**\n     * The symbol of the currency.\n     *\n     * @example \"$\"\n     */\n    currencySymbol: string;\n\n    /**\n     * The percentage the product's discount code takes off the price, as a floating point string.\n     * Absent when the product has no discount code.\n     */\n    discountPercentage: string;\n\n    /**\n     * The price in subunits.\n     */\n    price: string;\n  };\n\n  public connectedCallback() {\n    this.initializeShadowRoot();\n\n    this.render();\n  }\n\n  private initializeShadowRoot() {\n    if (!this.shadowRoot) {\n      this.attachShadow({ mode: 'open' });\n      this.shadowRoot.innerHTML = `<style>${styles}</style>`;\n    }\n  }\n\n  private render() {\n    const price = parseInt(this.dataset.price, 10);\n\n    let compareAtPrice = parseInt(this.dataset.compareAtPrice, 10);\n    if (!Number.isInteger(compareAtPrice)) {\n      compareAtPrice = price;\n    }\n\n    let salePrice = price;\n    if (this.dataset.discountPercentage) {\n      const discountPercentage = parseFloat(this.dataset.discountPercentage);\n      salePrice = Math.round(price - (price * (discountPercentage / 100)));\n    }\n\n    const savings = compareAtPrice - salePrice;\n    if (!(savings > 0)) {\n      this.hidden = true;\n      return;\n    }\n\n    // With no threshold this comes out as NaN, and the comparison below is then always false, so\n    // the badge falls back to the percentage.\n    const threshold = parseFloat(this.dataset.amountThreshold) * 100;\n\n    let text = '';\n    if (savings >= threshold) {\n      // We round down so the badge never claims more savings than the shopper gets.\n      text = `${this.dataset.currencySymbol}${Math.floor(savings / 100)} OFF`;\n    } else {\n      const savingsPercentage = 100 - Math.round(salePrice / compareAtPrice * 100);\n      text = `${savingsPercentage}% OFF`;\n    }\n\n    this.hidden = false;\n    this.shadowRoot.innerHTML = `<style>${styles}</style>${text}`;\n  }\n}\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'sale-discount-badge': SaleDiscountBadge;\n  }\n}\n\nif (!customElements.get('sale-discount-badge')) {\n  customElements.define('sale-discount-badge', SaleDiscountBadge);\n}\n"],"names":["__webpack_module_cache__","__webpack_require__","moduleId","cachedModule","undefined","exports","module","__webpack_modules__","async","play","video","b","document","baseURI","self","location","href","ProductCardImageCarousel","HTMLElement","index","hoverPreview","hoverTimeout","onMouseEnterBound","this","onMouseEnter","bind","onMouseLeaveBound","onMouseLeave","onClickBound","onClick","onCarouselMountedBound","onCarouselMounted","onSlideMoveStartedBound","onSlideMoveStarted","connectedCallback","initializeShadowRoot","carousel","shadowRoot","querySelector","addEventListener","onSlideVisible","onSlideHidden","querySelectorAll","length","disconnectedCallback","clearTimeout","removeEventListener","attachShadow","mode","innerHTML","buildSlides","event","target","Element","closest","url","dataset","template","media","content","element","slide","createElement","carouselSlide","cloned","cloneNode","classList","add","HTMLVideoElement","autoplay","appendChild","_carousel$querySelect","currentTarget","parseInt","catch","console","debug","announceFirstImage","requestAnimationFrame","refresh","_carousel$querySelect2","image","complete","dispatchEvent","Event","bubbles","onSettled","detail","pointerType","setTimeout","go","pause","now","_performance","performance","error","Date","getFeatureValue","featureKey","fallback","timeoutMs","resolvedFeatures","growthbook_runtime","resolved_features","value","startMillis","effectiveTimeoutMs","default_timeout_ms","milliseconds","Promise","resolve","load_state","extraMillis","Shopify","previewMode","preview_hold_ms","timer","once","growthbook","client_loader","warn","name","start","end","_performance3","measure","feature","timeout","customElements","get","define","AssertionError","Error","constructor","message","super","getGroup","group","expression","assert","isGroup","___HTML_LOADER_IMPORT_0___","URL","product_card","ProductCard","static","onGalleryLoadedBound","onGalleryLoaded","onSelectOptionClickBound","onSelectOptionClick","onSlotChangeBound","onSlotChange","onProductVariantOptionChangedBound","onProductVariantOptionChanged","onReadyBound","onReady","product","galleryImageLoaded","enableVariantOptions","releaseReady","callback","type","readyState","ready","slot","_event","gatherData","render","hasGallery","id","selected_variant_id","imageSrc","renderMainImage","productElement","award_badge_image","awardBadgeImage","award_tag","awardTag","available","badge_type","badgeType","metafields_badge","metafieldsBadge","metafields_discount_code","discountCode","metafields_discount_percentage","discountPercentage","parseFloat","metafields_discount_override","discountOverride","lazyload","main_image","mainImage","metafields_title","metafieldsTitle","metafields_subtitle","metafieldsSubtitle","reviews_average","reviewsAverage","reviews_count","reviewsCount","short_description","shortDescription","tags","title","selectedVariantId","option_groups","optionGroupElements","optionGroupElement","optionGroup","selected_option","selectedOption","position","useSwatchesForColor","use_swatches_for_color","options","productOptionElements","productOptionElement","productOption","color","variants","variantElements","variantElement","variant","backorder","backorder_selling_plan_id","backorderSellingPlanId","compare_at_price","compareAtPrice","price","selected","sku","sizeDescription","option1","option2","option3","push","hide_backorder_in_collection","hideBackorderInCollection","image_src","metafield_compare_at_price_modal_content","compareAtPriceModalContent","renderAwards","renderMainBadge","renderOutOfStockBadge","renderNewFormulaOverlay","renderTitles","renderProductOptions","renderAddToCart","renderLinks","addToCartButton","renderViewDetailsLink","renderSelectOptionButton","revealCard","skeleton","hidden","container","awardsContainer","badge","alt","src","hasBadgeCopy","badgeTitle","getBadgeTitle","textContent","productOptionsEl","text","toLowerCase","innerText","join","_this$product$tags","includes","log","newImageSrc","picture","squareImage","height","loading","setAttribute","srcset","generateSrcSet","IMAGE_SRCSET_WIDTHS","loaded","holdSkeletonImage","revealImage","skeletonImage","HTMLImageElement","currentSrc","startsWith","style","backgroundImage","renderGallery","gallery","badges","productTitle","subtitle","isLearnMore","decode","hideBuyButton","shouldRenderSelectOptionButton","selectedVariant","find","addToCart","country","countryCode","productListingDisplay","customerEmail","_this$product$tags2","_this$product$tags3","_this$product$tags4","_this$product$tags5","attributeEl","key","loopAttributeEl","subscriptionPlansData","script","full","update","button","quickAddLabelExperiment","loader","renderExperimentLabel","renderSelectOptionLabel","expected","newViewDetailsLink","getProductUrl","productUrl","links","link","selectOption","isOutOfStock","every","focusEvent","composed","requestEvent","CustomEvent","origin","searchParams","set","toString","viewParam","calculateImageHeight","imageWidth","width","IMAGE_ASPECT_RATIO","split","Math","round","responsiveSteps","output","i","replace","concat","SaleDiscountBadge","sale_discount_badge_namespaceObject","Number","isInteger","salePrice","savings","amountThreshold","currencySymbol","floor"],"sourceRoot":""}