@lime-bundles/widget 4.6.1 → 4.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../../core/src/storefront-api/client.ts","../../core/src/storefront-api/queries.ts","../../core/src/storefront-api/cache-key.ts","../../core/src/bundle/types.ts","../../core/src/bundle/widget-config.ts","../../core/src/bundle/parser.ts","../../core/src/bundle/presentment.ts","../../core/src/bundle/fetch-by-product.ts","../../core/src/bundle/tier-calculator.ts","../../core/src/bundle/market.ts","../../core/src/bundle/context-product.ts","../../core/src/bundle/validator.ts","../../core/src/bundle/qty.ts","../../core/src/bundle/multi-step.ts","../../core/src/analytics/reporter.ts","../../core/src/styles/sanitize.ts","../../core/src/styles/custom-css-injector.ts","../../core/src/utils/money.ts","../../core/src/utils/pricing.ts","../../core/src/utils/image.ts","../../core/src/utils/countdown.ts","../../core/src/dropdown/index.ts","../../core/src/dropdown/compute-position.ts","../../core/src/dropdown/keyboard.ts","../../core/src/dropdown/type-ahead.ts","../../core/src/variants/picker.ts","../../core/src/inventory/predicate.ts","../../core/src/ab-test/assign.ts","../../render/src/host.ts","../../render/src/adapt.ts","../../render/src/product/variants.ts","../../render/src/fixed/cart.ts","../../render/src/fixed/pricing.ts","../../render/src/product/option-selects.ts","../../render/src/product/hydrate.ts","../../render/src/product/row.ts","../../render/src/skeleton.ts","../../render/src/countdown.ts","../../render/src/strings.ts","../../render/src/dropdown/scroll.ts","../../render/src/dropdown/open-registry.ts","../../render/src/dropdown/bind.ts","../../render/src/dropdown/auto-bind.ts","../src/renderers/dropdown-host.ts","../src/renderers/fixed.ts","../../render/src/picker/i18n.ts","../../render/src/mix-match/i18n.ts","../../render/src/picker/rules.ts","../../render/src/picker/images.ts","../../render/src/utils.ts","../../render/src/picker/row.ts","../../render/src/picker/focus-trap.ts","../../render/src/picker/scroll-lock.ts","../../render/src/picker/sort.ts","../../render/src/mix-match/modal.ts","../../render/src/mix-match/rules.ts","../../render/src/picker/slot-card.ts","../../render/src/mix-match/slots.ts","../src/renderers/pricing-paint.ts","../../render/src/mix-match/seed.ts","../../render/src/picker-modal.ts","../src/renderers/mix-match.ts","../../render/src/volume/stock.ts","../../render/src/volume/pricing.ts","../src/renderers/volume.ts","../../render/src/bogo/pricing.ts","../../render/src/bogo/cart.ts","../src/renderers/bogo.ts","../../render/src/multi-step/i18n.ts","../../render/src/multi-step/rules.ts","../../render/src/multi-step/body.ts","../../render/src/multi-step/cart.ts","../../render/src/multi-step/wizard.ts","../src/renderers/multi-step.ts","../src/utils/input-mode.ts","../src/styles/bundle-css.ts","../src/lime-bundle.ts"],"sourcesContent":["/**\n * @lime-bundles/widget — Vanilla JS Web Component for headless storefronts.\n *\n * Usage:\n * <script src=\"https://unpkg.com/@lime-bundles/widget/dist/lime-bundle.js\"></script>\n * <lime-bundle shop-domain=\"...\" storefront-token=\"...\" bundle-gid=\"...\"></lime-bundle>\n */\nimport { LimeBundleElement } from \"./lime-bundle\";\n\n// Register custom element\nif (\n typeof customElements !== \"undefined\" &&\n !customElements.get(\"lime-bundle\")\n) {\n customElements.define(\"lime-bundle\", LimeBundleElement);\n}\n\nexport { LimeBundleElement };\nexport { trackInputMode } from \"./utils/input-mode\";\n","/**\n * Lightweight Storefront API client.\n * No framework dependencies — just fetch.\n *\n * SSR and Trusted Publishing environments (Hydrogen, Next.js server\n * components) MUST forward the end-buyer's IP via the `buyerIp` config\n * field. Without it Shopify may return `430 Shopify Security Rejection`\n * for server-originated traffic. Pass the IP in IPv4 or IPv6 string form.\n */\n\nexport class StorefrontApiError extends Error {\n constructor(\n public readonly errors: Array<{ message: string; locations?: unknown[] }>,\n ) {\n super(errors.map((e) => e.message).join(\"; \"));\n this.name = \"StorefrontApiError\";\n }\n}\n\nexport interface StorefrontClient {\n query<T = unknown>(\n query: string,\n variables?: Record<string, unknown>,\n options?: QueryOptions,\n ): Promise<T>;\n}\n\nexport interface QueryOptions {\n /** Abort the request mid-flight. */\n signal?: AbortSignal;\n}\n\n/**\n * B2B buyer identity for Storefront API `@inContext(buyer: ...)`.\n * `customerAccessToken` is required when `buyer` is set (per Shopify's\n * BuyerInput spec). `companyLocationId` selects a specific B2B location\n * for catalog pricing when the customer has access to multiple.\n *\n * Security: this token MUST come from a same-origin authenticated endpoint\n * (Customer Account API PKCE flow) and MUST NOT be passed via URL params,\n * localStorage shared with third-party scripts, or static prop trees that\n * land in DOM snapshots. See each package's README (\"Markets & B2B\").\n */\nexport interface BuyerInput {\n customerAccessToken: string;\n companyLocationId?: string;\n}\n\n/** Lazy buyer resolver — called per request so tokens stay off static props. */\nexport type BuyerResolver = BuyerInput | (() => BuyerInput | Promise<BuyerInput>);\n\nexport interface StorefrontClientConfig {\n shopDomain: string;\n accessToken: string;\n apiVersion?: string;\n /**\n * End-buyer's IP, forwarded as `Shopify-Storefront-Buyer-IP`. Required\n * for server-side calls with private tokens and recommended for all SSR.\n * See the class doc-comment.\n */\n buyerIp?: string;\n /**\n * ISO-3166 alpha-2 country code passed as `@inContext(country: ...)`.\n * Drives Shopify Markets pricing, currency, and availability. Omit for\n * the shop's default market.\n */\n country?: string;\n /**\n * ISO-639-1 language code passed as `@inContext(language: ...)`.\n * Translations come from the shop's locale settings. Omit for default.\n */\n language?: string;\n /**\n * B2B buyer identity. Either a plain object (token captured at config\n * time) or a callback that resolves per request (recommended for\n * security). When set, the SDK adds `@inContext(buyer: ...)` to queries\n * and the response carries B2B catalog prices for that customer.\n */\n buyer?: BuyerResolver;\n}\n\n// Keep in sync with CLAUDE.md → Shopify API Version Alignment.\nexport const DEFAULT_API_VERSION = \"2026-07\";\n\nasync function resolveBuyer(buyer?: BuyerResolver): Promise<BuyerInput | undefined> {\n if (!buyer) return undefined;\n if (typeof buyer === \"function\") {\n return await buyer();\n }\n return buyer;\n}\n\n/**\n * Returns true if any `@inContext` field is set. Used by the query layer\n * to decide whether to emit the directive-wrapped or directive-free\n * variant — keeps cache fragmentation off for the no-context case.\n */\nexport function hasInContext(config: StorefrontClientConfig): boolean {\n return Boolean(config.country || config.language || config.buyer);\n}\n\nexport function createStorefrontClient(\n config: StorefrontClientConfig,\n): StorefrontClient {\n const version = config.apiVersion ?? DEFAULT_API_VERSION;\n const endpoint = `https://${config.shopDomain}/api/${version}/graphql.json`;\n\n return {\n async query<T = unknown>(\n query: string,\n variables?: Record<string, unknown>,\n options?: QueryOptions,\n ): Promise<T> {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n \"X-Shopify-Storefront-Access-Token\": config.accessToken,\n };\n if (config.buyerIp) {\n headers[\"Shopify-Storefront-Buyer-IP\"] = config.buyerIp;\n }\n\n // Inject @inContext variables when set. The query string template\n // already declares the optional `$country`, `$language`, and\n // `$buyer` variables; the directive is emitted via the caller\n // selecting the *_INCONTEXT query variant.\n const buyer = await resolveBuyer(config.buyer);\n const mergedVariables: Record<string, unknown> = { ...(variables ?? {}) };\n if (config.country) mergedVariables.country = config.country;\n if (config.language) mergedVariables.language = config.language;\n if (buyer) mergedVariables.buyer = buyer;\n\n const response = await fetch(endpoint, {\n method: \"POST\",\n headers,\n body: JSON.stringify({ query, variables: mergedVariables }),\n signal: options?.signal,\n });\n\n if (!response.ok) {\n throw new StorefrontApiError([\n {\n message: `Storefront API error: ${response.status} ${response.statusText}`,\n },\n ]);\n }\n\n const json = (await response.json()) as {\n data?: T;\n errors?: Array<{ message: string }>;\n };\n\n if (json.errors?.length) {\n throw new StorefrontApiError(json.errors);\n }\n\n return json.data as T;\n },\n };\n}\n","/**\n * Storefront API GraphQL queries.\n *\n * Pricing-sensitive queries (BUNDLE_METAOBJECT_QUERY, BUNDLES_FOR_PRODUCT_QUERY)\n * are wrapped with `@inContext(country:, language:, buyer:)` by\n * `withInContext()` below when the SDK config has any context field set.\n * The unwrapped variant stays publicly cacheable; the wrapped variant\n * must be marked `Cache-Control: private` per Hydrogen guidance for\n * buyer-contextual responses.\n *\n * The SDK fetcher selects the variant at request time via `hasInContext()`.\n */\nimport type { MetaobjectField } from \"./types\";\n\n/**\n * Returns the `@inContext`-wrapped variant of `query`. Adds the three\n * optional variables to the parameter list and the directive to the\n * operation root. Variable substitution itself is handled by the\n * Storefront client which injects `country`, `language`, `buyer` into\n * the request variables.\n *\n * Handles both arg-list (`query Name($x: Int) { ... }`) and zero-arg\n * (`query Name { ... }`) operations. Mutations are not supported —\n * adjust if one ever needs `@inContext`.\n */\nexport function withInContext(query: string): string {\n return query.replace(\n /query\\s+(\\w+)\\s*(?:\\(([^)]*)\\))?\\s*\\{/,\n (_match, name, args) => {\n const trimmed = (args || \"\").trim();\n const extra = \"$country: CountryCode, $language: LanguageCode, $buyer: BuyerInput\";\n const combined = trimmed ? `${trimmed}, ${extra}` : extra;\n return `query ${name}(${combined}) @inContext(country: $country, language: $language, buyer: $buyer) {`;\n },\n );\n}\n\nexport const BUNDLE_METAOBJECT_QUERY = `#graphql\n query BundleMetaobject($id: ID!) {\n metaobject(id: $id) {\n id\n type\n fields {\n key\n value\n reference {\n ... on Product {\n id\n title\n handle\n productType\n featuredImage { url altText width height }\n priceRange {\n minVariantPrice { amount currencyCode }\n maxVariantPrice { amount currencyCode }\n }\n variants(first: 100) {\n nodes {\n id\n title\n availableForSale\n currentlyNotInStock\n quantityAvailable\n price { amount currencyCode }\n compareAtPrice { amount currencyCode }\n unitPrice { amount currencyCode }\n unitPriceMeasurement { referenceValue referenceUnit }\n selectedOptions { name value }\n image { url altText width height }\n }\n }\n }\n }\n references(first: 50) {\n nodes {\n ... on Product {\n id\n title\n handle\n productType\n featuredImage { url altText width height }\n priceRange {\n minVariantPrice { amount currencyCode }\n maxVariantPrice { amount currencyCode }\n }\n variants(first: 100) {\n nodes {\n id\n title\n availableForSale\n currentlyNotInStock\n quantityAvailable\n price { amount currencyCode }\n compareAtPrice { amount currencyCode }\n unitPrice { amount currencyCode }\n unitPriceMeasurement { referenceValue referenceUnit }\n selectedOptions { name value }\n image { url altText width height }\n }\n }\n }\n ... on Collection {\n id\n title\n handle\n products(first: 50) {\n nodes {\n id\n title\n handle\n productType\n featuredImage { url altText width height }\n priceRange {\n minVariantPrice { amount currencyCode }\n maxVariantPrice { amount currencyCode }\n }\n variants(first: 100) {\n nodes {\n id\n title\n availableForSale\n currentlyNotInStock\n quantityAvailable\n price { amount currencyCode }\n compareAtPrice { amount currencyCode }\n unitPrice { amount currencyCode }\n unitPriceMeasurement { referenceValue referenceUnit }\n selectedOptions { name value }\n image { url altText width height }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n`;\n\n/**\n * Shop-level settings the storefront needs, in one round trip.\n *\n * `customCss` is auto-injected as a scoped <style> tag so headless storefronts\n * get CSS parity with classic-theme merchants.\n */\nexport const SHOP_SETTINGS_QUERY = `#graphql\n query ShopSettings {\n shop {\n customCss: metafield(namespace: \"$app\", key: \"custom_css\") {\n value\n }\n }\n }\n`;\n\nexport interface ShopSettingsResponse {\n shop: {\n customCss: { value: string | null } | null;\n } | null;\n}\n\n/**\n * Product-aware bundle lookup. Mirrors the pattern the classic Liquid theme\n * block uses (`product.metafields[\"$app\"].active_bundle_ids`): one query\n * returns every bundle that applies to the current product, fully resolved\n * (nested product/collection references included) so the caller gets the\n * same shape as `parseMetaobjectBundleStrict` expects.\n *\n * The product handle is the natural input on a headless storefront — every\n * canonical Shopify product URL is `/products/<handle>`, so the widget can\n * often derive it from `window.location` without merchant input.\n *\n * Complexity: the nested references (bundle → products → variants) are deep\n * but stay within the Storefront API budget for token-based queries at\n * realistic bundle sizes (≤10 bundles per product, ≤50 products per bundle).\n */\nexport const BUNDLES_FOR_PRODUCT_QUERY = `#graphql\n query BundlesForProduct($handle: String!) {\n product(handle: $handle) {\n id\n handle\n title\n metafield(namespace: \"$app\", key: \"active_bundle_ids\") {\n references(first: 10) {\n nodes {\n ... on Metaobject {\n id\n type\n fields {\n key\n value\n reference {\n ... on Product {\n id\n title\n handle\n productType\n featuredImage { url altText width height }\n priceRange {\n minVariantPrice { amount currencyCode }\n maxVariantPrice { amount currencyCode }\n }\n variants(first: 100) {\n nodes {\n id\n title\n availableForSale\n currentlyNotInStock\n quantityAvailable\n price { amount currencyCode }\n compareAtPrice { amount currencyCode }\n unitPrice { amount currencyCode }\n unitPriceMeasurement { referenceValue referenceUnit }\n selectedOptions { name value }\n image { url altText width height }\n }\n }\n }\n }\n references(first: 50) {\n nodes {\n ... on Product {\n id\n title\n handle\n productType\n featuredImage { url altText width height }\n priceRange {\n minVariantPrice { amount currencyCode }\n maxVariantPrice { amount currencyCode }\n }\n variants(first: 100) {\n nodes {\n id\n title\n availableForSale\n currentlyNotInStock\n quantityAvailable\n price { amount currencyCode }\n compareAtPrice { amount currencyCode }\n unitPrice { amount currencyCode }\n unitPriceMeasurement { referenceValue referenceUnit }\n selectedOptions { name value }\n image { url altText width height }\n }\n }\n }\n ... on Collection {\n id\n title\n handle\n products(first: 50) {\n nodes {\n id\n title\n handle\n productType\n featuredImage { url altText width height }\n priceRange {\n minVariantPrice { amount currencyCode }\n maxVariantPrice { amount currencyCode }\n }\n variants(first: 100) {\n nodes {\n id\n title\n availableForSale\n currentlyNotInStock\n quantityAvailable\n price { amount currencyCode }\n compareAtPrice { amount currencyCode }\n unitPrice { amount currencyCode }\n unitPriceMeasurement { referenceValue referenceUnit }\n selectedOptions { name value }\n image { url altText width height }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n`;\n\n/**\n * Return shape for BUNDLES_FOR_PRODUCT_QUERY. The inner `fields` array\n * reuses `MetaobjectField` verbatim so callers can pass it straight to\n * `parseMetaobjectBundleStrict` without a type cast — the same shape the\n * BUNDLE_METAOBJECT_QUERY response produces for a single bundle.\n */\nexport interface BundlesForProductResponse {\n product: {\n id: string;\n handle: string;\n title: string;\n metafield: {\n references: {\n nodes: Array<{\n id: string;\n type: string;\n fields: MetaobjectField[];\n }>;\n };\n } | null;\n } | null;\n}\n\n/**\n * Tokenless cart mutations. Storefront API docs: \"Cart (read/write)\" is\n * available on the Tokenless Access tier. The widget's default-cart path\n * uses these so merchants don't need a cart implementation.\n */\nexport const CART_CREATE_MUTATION = `#graphql\n mutation CartCreate($input: CartInput!) {\n cartCreate(input: $input) {\n cart { id checkoutUrl }\n userErrors { field message code }\n warnings { code message target }\n }\n }\n`;\n\nexport const CART_LINES_ADD_MUTATION = `#graphql\n mutation CartLinesAdd($cartId: ID!, $lines: [CartLineInput!]!) {\n cartLinesAdd(cartId: $cartId, lines: $lines) {\n cart { id checkoutUrl }\n userErrors { field message code }\n warnings { code message target }\n }\n }\n`;\n\n/**\n * Current lines of a saved cart, with their line-item attributes. The\n * widget's default-cart flow runs this before reusing a persisted cart id\n * so it can find (and remove) its own earlier lines — the ones tagged with\n * the `_lime_bundle_gid` attribute — instead of stacking a second copy of\n * an abandoned attempt on top. `first: 250` is the Storefront API's page\n * maximum; a default-flow cart holds a handful of bundle lines, so one\n * page is always enough in practice.\n */\nexport const CART_LINES_QUERY = `#graphql\n query CartLines($cartId: ID!) {\n cart(id: $cartId) {\n id\n lines(first: 250) {\n nodes {\n id\n attributes { key value }\n }\n }\n }\n }\n`;\n\n/** Top-level response shape for `CART_LINES_QUERY`. `cart` is null when\n * the id no longer resolves (expired or merged on Shopify's side). */\nexport interface CartLinesQueryResponse {\n cart: {\n id: string;\n lines: {\n nodes: Array<{\n id: string;\n attributes: Array<{ key: string; value: string | null }>;\n }>;\n };\n } | null;\n}\n\nexport const CART_LINES_REMOVE_MUTATION = `#graphql\n mutation CartLinesRemove($cartId: ID!, $lineIds: [ID!]!) {\n cartLinesRemove(cartId: $cartId, lineIds: $lineIds) {\n cart { id checkoutUrl }\n userErrors { field message code }\n warnings { code message target }\n }\n }\n`;\n\n/**\n * Inner payload shape shared by both cart mutations. Not a GraphQL\n * response on its own — see `CartCreateResponse` and `CartLinesAddResponse`\n * for the correctly-wrapped top-level response shapes.\n *\n * `warnings` is where Shopify reports lines it silently adjusted —\n * `MERCHANDISE_NOT_ENOUGH_STOCK` when a quantity was capped below what the\n * bundle needs. A capped bundle checks out at full price with no discount,\n * so callers must treat those warnings as failures, not footnotes.\n */\nexport interface CartMutationPayload {\n cart: { id: string; checkoutUrl: string } | null;\n userErrors: Array<{ field: string[] | null; message: string; code?: string | null }>;\n warnings?: Array<{ code?: string | null; message: string; target?: string | null }>;\n}\n\n/** Top-level response shape for `CART_CREATE_MUTATION`. */\nexport interface CartCreateResponse {\n cartCreate: CartMutationPayload;\n}\n\n/** Top-level response shape for `CART_LINES_ADD_MUTATION`. */\nexport interface CartLinesAddResponse {\n cartLinesAdd: CartMutationPayload;\n}\n\n/** Top-level response shape for `CART_LINES_REMOVE_MUTATION`. */\nexport interface CartLinesRemoveResponse {\n cartLinesRemove: CartMutationPayload;\n}\n","/**\n * Stable cache-key helper for SWR / TanStack Query / Hydrogen consumers.\n *\n * Buyer-contextual queries (Storefront `@inContext(buyer:)`) must NEVER\n * be shared across users — per Hydrogen's caching guidance, set\n * Cache-Control: private on those responses and ensure your cache key\n * includes a buyer fingerprint, never the raw token.\n *\n * Example usage with TanStack Query:\n * queryKey: getCacheKey(\"BundleMetaobject\", { id }, {\n * shopDomain, country, language, buyer,\n * })\n */\nimport type { BuyerInput } from \"./client\";\n\nexport interface CacheKeyContext {\n shopDomain: string;\n country?: string;\n language?: string;\n /**\n * Pass the resolved BuyerInput or undefined. Never pass a function —\n * the consumer is responsible for resolving lazy buyers before keying.\n * The raw `customerAccessToken` is hashed before being included in the\n * key so it doesn't appear in devtools / log output.\n */\n buyer?: BuyerInput;\n}\n\nexport function getCacheKey(\n queryName: string,\n variables: Record<string, unknown>,\n ctx: CacheKeyContext,\n): string[] {\n return [\n ctx.shopDomain,\n queryName,\n stableStringify(variables),\n ctx.country ?? \"_\",\n ctx.language ?? \"_\",\n ctx.buyer ? buyerFingerprint(ctx.buyer) : \"_\",\n ];\n}\n\n/**\n * Deterministic JSON.stringify with sorted keys. Avoids cache misses caused\n * by object property order varying across consumers.\n */\nfunction stableStringify(value: unknown): string {\n if (value === null || typeof value !== \"object\") return JSON.stringify(value);\n if (Array.isArray(value)) {\n return \"[\" + value.map(stableStringify).join(\",\") + \"]\";\n }\n const entries = Object.entries(value as Record<string, unknown>).sort(\n ([a], [b]) => a.localeCompare(b),\n );\n return (\n \"{\" +\n entries.map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`).join(\",\") +\n \"}\"\n );\n}\n\n/**\n * Lightweight non-cryptographic hash of the buyer token + optional company\n * location. Stable across runs, fast, and good enough to discriminate\n * cache entries per buyer without leaking the raw token. Don't use this\n * for security purposes — it's purely for cache keying.\n */\nfunction buyerFingerprint(buyer: BuyerInput): string {\n const input = `${buyer.customerAccessToken}:${buyer.companyLocationId ?? \"\"}`;\n // FNV-1a 32-bit\n let hash = 2166136261;\n for (let i = 0; i < input.length; i++) {\n hash ^= input.charCodeAt(i);\n hash = Math.imul(hash, 16777619);\n }\n // Unsigned hex, 8 chars\n return (hash >>> 0).toString(16).padStart(8, \"0\");\n}\n","/**\n * Bundle types for headless rendering.\n *\n * `ParsedBundle` is a discriminated union keyed on `bundleType` so consumers\n * can narrow the type via a switch statement or conditional check:\n *\n * function render(bundle: ParsedBundle) {\n * switch (bundle.bundleType) {\n * case \"fixed\": // bundle.products is load-bearing\n * case \"volume\": // bundle.volumeTiers is load-bearing\n * case \"mix_match\": // bundle.minQuantity / maxQuantity are load-bearing\n * case \"bogo\": // bundle.buyProductId / getProductId / quantities are load-bearing\n * case \"multi_step\": // bundle.steps / productRules are load-bearing\n * }\n * }\n *\n * Each variant carries ONLY the fields it actually uses. Fields that are\n * always present (id, title, status, schedule, widget config, A/B test\n * metadata) live on the shared `BundleBase` type.\n *\n * Locked for v3.0.0 — widening or narrowing these discriminants is a\n * breaking change. v3.0.0 widened the union with the \"bogo\" and\n * \"multi_step\" variants. `WidgetConfig` widened in 2.0.0 to expose the full\n * admin-editor shape; see WidgetConfig for the eight sub-section structure.\n */\nimport type { Product } from \"../storefront-api/types\";\n\nexport type BundleType = \"fixed\" | \"mix_match\" | \"volume\" | \"bogo\" | \"multi_step\";\n/**\n * `archived` is a legacy system-owned status that is no longer produced. The\n * SDK parser filters archived bundles upstream — they never reach a rendered\n * component — but the union must list the value so raw metaobject reads\n * round-trip cleanly through `parseMetaobjectBundleStrict`.\n *\n * `sync_failed` represents a bundle whose three-layer Metaobject→Discount→Prisma\n * write didn't complete; the admin form shows a \"didn't save properly\" banner\n * and merchants resave to retry. The SDK skips rendering these too.\n */\nexport type BundleStatus =\n | \"active\"\n | \"draft\"\n | \"inactive\"\n | \"out_of_stock\"\n | \"archived\"\n | \"sync_failed\";\n\nexport interface DiscountConfig {\n discountType: \"percentage\" | \"fixed_amount\";\n discountValue: number;\n allowStacking: boolean;\n}\n\n/**\n * Widget configuration as stored in the bundle metaobject's `widget_config`\n * field. Structured by visual area to mirror the in-app editor's accordion.\n *\n * The web component (`<lime-bundle>`) applies every value as a `--lb-*` CSS\n * custom property so the rendered widget matches what the merchant saw in\n * the editor. The React SDK exposes this object unchanged on\n * `ParsedBundle.widgetConfig`; whether and how to apply it is up to the\n * developer.\n */\n/**\n * Widget header section — countdown timer colours only.\n *\n * The colored/gradient header band and the \"-X%\" save badge were retired in the\n * manifest-and-receipt redesign: the bundle title now reads as a plain section\n * heading in the host theme's own type (no app-chrome band), and savings are\n * stated once on the receipt summary line. The interface key stays `header` for\n * config back-compat; only the countdown timer's colours remain.\n */\nexport interface HeaderConfig {\n countdownBgColor: string;\n countdownTextColor: string;\n}\n\n/**\n * Global corner-rounding preset. One choice sets the base radius (`--lb-radius`);\n * every element derives its own from it (cards + CTA use the base, smaller\n * controls use `--lb-radius-sm`). Mirrors `RadiusPreset` in the admin's\n * `app/lib/bundle-types.ts`.\n */\nexport type RadiusPreset = \"none\" | \"subtle\" | \"rounded\" | \"round\";\n\nexport interface LayoutConfig {\n backgroundColor: string;\n borderColor: string;\n borderWidth: number;\n borderRadius: RadiusPreset;\n}\n\n/**\n * Thumbnail aspect-ratio choice. Drives a derived block in\n * applyWidgetConfigVars that sets three CSS vars\n * (--lb-thumbnail-aspect-ratio, --lb-thumbnail-img-fit,\n * --lb-thumbnail-img-height). \"original\" disables the aspect crop and\n * lets each image render at its intrinsic ratio.\n */\nexport type ThumbnailRatio = \"square\" | \"tall\" | \"wide\" | \"original\";\n\nexport interface ProductListConfig {\n textColor: string;\n thumbnailRatio: ThumbnailRatio;\n showPrice: boolean;\n showCompareAtPrice: boolean;\n /**\n * The per-unit line (\"$4.99/kg\") under a product's price. Distinct from\n * `PricingConfig.showPerUnitPrice`, which governs the bundle summary's\n * per-item figure rather than the product row.\n */\n showUnitPrice: boolean;\n /** The \"×N\" count on a product row, whether chip or inline. */\n showQuantity: boolean;\n}\n\nexport interface PricingConfig {\n showComparePrice: boolean;\n showPerUnitPrice: boolean;\n showItemCount: boolean;\n showCompareAtPrice: boolean;\n}\n\n/**\n * Widget savings line — toggles the quiet \"You save {amount} ({percent}%)\"\n * line on the receipt summary. The line inherits the widget's own text colour\n * (lime is reserved for the CTA), so no colour/border fields remain. The\n * interface key stays `savingsBar` for config back-compat.\n */\nexport interface SavingsBarConfig {\n visible: boolean;\n}\n\nexport interface CtaConfig {\n ctaText: string;\n primaryColor: string;\n buttonTextColor: string;\n borderWidth: number;\n borderColor: string;\n}\n\nexport interface CountdownConfig {\n showCountdown: boolean;\n}\n\nexport interface PopularBadgeConfig {\n visible: boolean;\n text: string;\n /** Optional — merchant may not pick a tier. */\n tierIndex?: number;\n bgColor: string;\n textColor: string;\n /** Keeps its own border width + color; the corner radius follows the global\n * radius preset, so there is no badge radius field. */\n borderWidth: number;\n borderColor: string;\n}\n\nexport interface WidgetConfig {\n header: HeaderConfig;\n layout: LayoutConfig;\n productList: ProductListConfig;\n pricing: PricingConfig;\n cta: CtaConfig;\n savingsBar: SavingsBarConfig;\n countdown: CountdownConfig;\n popularBadge: PopularBadgeConfig;\n /** Show \"Only X left\" badge when remaining stock is at or below\n * `lowStockThreshold`. Hidden silently when the Storefront token\n * lacks `unauthenticated_read_product_inventory`. */\n showLowStockBadge: boolean;\n /** Threshold (1-99) below which the low-stock badge appears. */\n lowStockThreshold: number;\n lowStockBgColor: string;\n lowStockTextColor: string;\n\n // --- Mix-match picker section ---\n showSearch: boolean;\n /** Show product-type filter pills above the picker grid (derived from the\n * eligible products' Shopify product types). */\n mixMatchShowTypeFilters: boolean;\n /**\n * When true (default), the picker modal renders a per-pick qty stepper so\n * the shopper can choose how many of each product to add. When false, the\n * stepper is hidden and Add adds qty = `productRules[productId]?.min ?? 1`.\n */\n mixMatchShowQuantitySelector: boolean;\n pickerBgColor: string;\n pickerTextColor: string;\n // Picker modal \"Borders\" — the single source for every border in the modal\n // (modal, search, product tiles, variant select, quantity stepper). Thumbnails\n // take the radius only; the Add button keeps its own width/color but this radius.\n pickerBorderWidth: number;\n pickerBorderColor: string;\n pickerBorderRadius: RadiusPreset;\n /** Aspect ratio for product thumbnails inside the picker modal —\n * independent of the main widget's `productList.thumbnailRatio`. */\n pickerThumbnailRatio: ThumbnailRatio;\n pickerAddBgColor: string;\n pickerAddLabelColor: string;\n pickerAddBorderWidth: number;\n pickerAddBorderColor: string;\n\n // --- Multi-step wizard section ---\n /**\n * When true, the wizard moves to the next step on its own once the\n * shopper's picks reach the step's maximum. Triggered by the maximum,\n * never the minimum; the last step and steps without a maximum never\n * auto-advance. Off by default.\n */\n multiStepAutoAdvance: boolean;\n\n // --- Volume tier section ---\n // Tier cards inherit the global border (width/color) and radius — no fields.\n defaultTier: \"first\" | \"best_value\" | number;\n}\n\n/**\n * A single volume-bundle tier row. `discountType` lives at the bundle\n * level (`bundle.discountConfig.discountType`) — each tier carries only\n * the magnitude in whichever field matches that type:\n * - `percentage` is a whole-number (e.g. 10 for 10% off)\n * - `amount` is the per-unit fixed amount off, in the shop's currency\n *\n * Exactly one of `percentage` or `amount` is present per tier, determined\n * by the parent bundle's `discountType`. This mirrors the on-metaobject\n * shape — see `app/lib/bundle-types.ts` VolumeTierSchema.\n */\nexport interface VolumeTier {\n minQuantity: number;\n percentage?: number;\n amount?: number;\n}\n\n/** Fields present on every bundle regardless of type. */\ninterface BundleBase {\n id: string;\n title: string;\n /** Customer-facing subtitle rendered under the bundle title. */\n description: string | null;\n status: BundleStatus;\n products: Product[];\n discountConfig: DiscountConfig;\n widgetConfig: WidgetConfig;\n startsAt: string | null;\n endsAt: string | null;\n discountLabel: string | null;\n\n /**\n * Per-product allowed-variant lists, positionally aligned with\n * `products`. When the merchant restricted a product to specific\n * variants, only those GIDs should appear in the picker and dropdown.\n * Inner array empty or whole entry null means \"all available variants\".\n */\n selectedVariantIds: string[][] | null;\n\n /** Per-product bundle quantity, keyed by Shopify Product GID. */\n productQuantities: Record<string, number>;\n /** Per-variant quantity override, keyed by Shopify ProductVariant GID. */\n variantQuantities: Record<string, number>;\n\n /**\n * Market visibility scoping.\n * - \"all\" (default): bundle is visible in every Shopify market.\n * - \"specific\": bundle is visible only in the markets listed in marketIds.\n *\n * Consumers querying with a marketId can use isVisibleInMarket() to filter.\n */\n marketVisibility: \"all\" | \"specific\";\n /**\n * Allowed Shopify Market GIDs when marketVisibility = \"specific\".\n * Empty when marketVisibility = \"all\".\n * Example: [\"gid://shopify/Market/1\", \"gid://shopify/Market/2\"]\n */\n marketIds: string[];\n /**\n * Retail projection of the selected markets: ISO-3166-1 alpha-2 country\n * codes from each market's region condition, written by the admin at save\n * time. May contain the \"*\" wildcard when a selected market targets all\n * regions (\"Worldwide\"). Empty when marketVisibility = \"all\" or for\n * metaobjects written before this field existed.\n *\n * Prefer gating on this (via isVisibleToBuyer) over marketIds: Shopify has\n * deprecated market IDs as buyer-targeting signals, and a B2B buyer's\n * market never surfaces through storefront localization at all.\n */\n countryCodes: string[];\n /**\n * B2B projection of the selected markets: CompanyLocation GIDs from each\n * market's company-location condition, written by the admin at save time.\n * May contain the \"*\" wildcard when a selected market targets all company\n * locations. Empty when marketVisibility = \"all\", when no selected market\n * is a B2B market, or for metaobjects written before this field existed.\n */\n companyLocationIds: string[];\n\n /**\n * Id of the A/B test this bundle is one side of, or null when it is not in\n * one.\n *\n * Two bundles sharing an `abTestId` are alternatives, not a pair to render\n * together: exactly one of them should ever be shown to a given shopper.\n * Pass the fetched list through `resolveAbTests` to make that choice.\n */\n abTestId: string | null;\n\n /**\n * This side's share of shoppers, 0–100. Only meaningful alongside\n * `abTestId`, and 0 when there is no test.\n */\n abWeight: number;\n}\n\nexport interface FixedBundleData extends BundleBase {\n bundleType: \"fixed\";\n}\n\nexport interface VolumeBundleData extends BundleBase {\n bundleType: \"volume\";\n volumeTiers: VolumeTier[];\n}\n\n/** Per-product unit-count rule for mix & match bundles. */\nexport interface ProductRule {\n /** Smallest qty the shopper must add when picking this product. */\n min: number;\n /** Largest qty the shopper may add for this pick. >= min. */\n max: number;\n /**\n * When true the product is always part of the bundle: renderers pre-seed\n * it at `min` units and block any removal that would drop its total below\n * `min` (shoppers may still swap variants and adjust quantity). The\n * checkout discount only applies while the cart holds >= `min` units of\n * the product. Absent = not required.\n */\n required?: boolean;\n}\n\n/** Default rule applied to products without an explicit entry (e.g. collection-sourced). */\nexport const DEFAULT_PRODUCT_RULE: ProductRule = { min: 1, max: 99 };\n\nexport interface MixMatchBundleData extends BundleBase {\n bundleType: \"mix_match\";\n /** Total number of units the shopper must select to qualify for the discount, summed across all chosen products. */\n minQuantity: number | null;\n /**\n * @deprecated removed from the data model — always null on parsed bundles.\n * The bundle no longer has a top-level total cap; per-product max lives in productRules.\n */\n maxQuantity: number | null;\n /** Per-product unit-count rules keyed by product GID. Missing entries default to DEFAULT_PRODUCT_RULE. */\n productRules: Record<string, ProductRule>;\n /**\n * Optional per-variant rules keyed by variant GID, nested inside the owning\n * product's rule: the product rule caps that product's total across every\n * variant, and a variant rule refines one slot inside it. Variants without\n * an entry inherit the product rule. Empty on every bundle saved before\n * variant rules existed.\n */\n variantRules: Record<string, ProductRule>;\n}\n\n/**\n * BOGO (Buy X Get Y) bundle: the shopper buys `buyQuantity` units of the\n * buy product and gets `getQuantity` units of the get product (which may be\n * the same product) at `discountConfig.discountValue` percent off\n * (100 = free). The reward repeats per full multiple in the cart. The\n * shared `products` list holds the deduped [buy, get] pair.\n */\nexport interface BogoBundleData extends BundleBase {\n bundleType: \"bogo\";\n /** Product GID the shopper must buy to qualify. */\n buyProductId: string;\n /** Product GID the shopper gets discounted; may equal buyProductId. */\n getProductId: string;\n /** Units of the buy product required per reward (N). */\n buyQuantity: number;\n /** Units of the get product discounted per reward (M). */\n getQuantity: number;\n /**\n * Variant GIDs the buy side is narrowed to; empty = every variant.\n * Per-side lists exist because the positional `selectedVariantIds`\n * cannot hold two different lists for a same-product BOGO, which\n * silently dropped the get side's choice (issue #436).\n */\n buyVariantIds: string[];\n /** Variant GIDs the get side is narrowed to; see buyVariantIds. */\n getVariantIds: string[];\n}\n\n/**\n * One step of a multi-step bundle (\"Pick a shampoo\" → \"Pick a conditioner\").\n * Parsed from the metaobject's `steps` JSON field. Each step scopes the\n * picker to its own product pool (pinned `productIds` plus the products of\n * its `collectionIds`) and carries its own unit requirement:\n * - `minQuantity` (>= 1) — units the shopper must pick in this step.\n * - `maxQuantity` — step unit ceiling, or null for unlimited.\n */\nexport interface ParsedMultiStep {\n name: string;\n minQuantity: number;\n maxQuantity: number | null;\n /** Product GIDs pinned to this step (subset of the cross-step union in `products`). */\n productIds: string[];\n /** Collection GIDs whose products belong to this step's pool. */\n collectionIds: string[];\n}\n\n/**\n * Multi-step bundle: the shopper walks named steps as a wizard, each with\n * its own product pool and min/max unit bounds; the whole-bundle discount\n * applies once every step's minimum is met. The shared `products` list is\n * the flattened cross-step union (pinned products in `selectedVariantIds`\n * order, then collection-resolved products). Use `productsForStep` to\n * recover each step's pool — attribution is exact because the parser\n * records `collectionProductIds` while resolving the `collection` field.\n *\n * `productRules` are step-agnostic: `rule.max` caps the product's TOTAL\n * units across every slot in every step.\n */\nexport interface MultiStepBundleData extends BundleBase {\n bundleType: \"multi_step\";\n steps: ParsedMultiStep[];\n /** Per-product unit-count rules keyed by product GID. Missing entries default to DEFAULT_PRODUCT_RULE. */\n productRules: Record<string, ProductRule>;\n /**\n * Optional per-variant rules keyed by variant GID, nested inside the owning\n * product's rule and step-agnostic for the same reason `productRules` are:\n * one rule per variant, however many steps its product appears in.\n */\n variantRules: Record<string, ProductRule>;\n /**\n * Exact per-collection membership recorded at parse time:\n * collection GID → the product GIDs that collection resolved to. Lets\n * `productsForStep` attribute collection-sourced products to the right\n * step without a separate collection-membership query.\n */\n collectionProductIds: Record<string, string[]>;\n}\n\nexport type ParsedBundle =\n | FixedBundleData\n | VolumeBundleData\n | MixMatchBundleData\n | BogoBundleData\n | MultiStepBundleData;\n\n/**\n * Thrown when metaobject fields cannot be parsed into a valid ParsedBundle.\n * Distinct from StorefrontApiError so callers can handle \"bundle not\n * renderable\" (inactive, expired, schema mismatch) differently from\n * \"network failure\".\n */\nexport class BundleParseError extends Error {\n constructor(message: string, public readonly reason: BundleParseFailReason) {\n super(message);\n this.name = \"BundleParseError\";\n }\n}\n\nexport type BundleParseFailReason =\n | \"not_found\"\n | \"invalid_type\"\n | \"inactive\"\n | \"not_started\"\n | \"expired\";\n","/**\n * Default widget config + CSS-variable flattener.\n *\n * Mirrors the Zod defaults and `flattenWidgetConfig` in the admin package's\n * `app/lib/bundle-types.ts`. Kept in plain TypeScript so the SDK packages\n * stay Zod-free; merchant-authored config is validated at the admin\n * boundary before it reaches the metaobject, so consumers of this module\n * can trust the JSON shape and just merge against these defaults.\n *\n * If a future merchant-editor field is added:\n * 1. Update `WidgetConfig` in types.ts\n * 2. Add the default here\n * 3. Add the flattened CSS-var key below\n * 4. Consume the var in packages/widget/src/styles/*.css\n */\nimport type { LayoutConfig, RadiusPreset, WidgetConfig } from \"./types\";\n\n/**\n * Pixel value each global radius preset resolves to for the base `--lb-radius`.\n * Mirrors `RADIUS_PRESET_PX` in the admin's `app/lib/bundle-types.ts`.\n */\nexport const RADIUS_PRESET_PX: Record<RadiusPreset, number> = {\n none: 0,\n subtle: 6,\n rounded: 12,\n round: 18,\n};\n\nexport const WIDGET_CONFIG_DEFAULTS: WidgetConfig = {\n header: {\n countdownBgColor: \"#B4DC7F47\",\n countdownTextColor: \"#555555\",\n },\n layout: {\n backgroundColor: \"#FCFCFC\",\n borderColor: \"#E5E5E5\",\n borderWidth: 1,\n borderRadius: \"subtle\",\n },\n productList: {\n textColor: \"#555555\",\n thumbnailRatio: \"square\",\n showPrice: true,\n showCompareAtPrice: true,\n showUnitPrice: true,\n showQuantity: true,\n },\n pricing: {\n showComparePrice: true,\n showPerUnitPrice: true,\n showItemCount: true,\n showCompareAtPrice: true,\n },\n cta: {\n ctaText: \"Add to cart\",\n primaryColor: \"#B4DC7F\",\n buttonTextColor: \"#555555\",\n borderWidth: 0,\n borderColor: \"#B4DC7F\",\n },\n savingsBar: {\n visible: true,\n },\n countdown: {\n showCountdown: true,\n },\n popularBadge: {\n visible: true,\n text: \"Most Popular\",\n bgColor: \"#1A1A1A\",\n textColor: \"#FFFFFF\",\n borderWidth: 0,\n borderColor: \"#1A1A1A\",\n },\n showLowStockBadge: true,\n lowStockThreshold: 10,\n lowStockBgColor: \"#FEF3C7\",\n lowStockTextColor: \"#92400E\",\n\n showSearch: true,\n mixMatchShowTypeFilters: true,\n mixMatchShowQuantitySelector: true,\n pickerBgColor: \"#FCFCFC\",\n pickerTextColor: \"#555555\",\n pickerBorderWidth: 1,\n pickerBorderColor: \"#E5E5E5\",\n pickerBorderRadius: \"subtle\",\n pickerThumbnailRatio: \"square\",\n pickerAddBgColor: \"#B4DC7F\",\n pickerAddLabelColor: \"#555555\",\n pickerAddBorderWidth: 0,\n pickerAddBorderColor: \"#B4DC7F\",\n\n multiStepAutoAdvance: false,\n\n defaultTier: \"first\",\n};\n\n/**\n * Merge a partial (possibly stale-schema) widget config against the\n * defaults, so downstream renderers can trust every field. Top-level and\n * nested-section fields both merge shallow; unknown keys on nested sections\n * are preserved (but ignored by the renderer). This is what the parser\n * calls after pulling `widget_config` JSON from the metaobject.\n */\nexport function mergeWidgetConfig(raw: unknown): WidgetConfig {\n if (!raw || typeof raw !== \"object\") {\n return WIDGET_CONFIG_DEFAULTS;\n }\n const input = raw as Partial<WidgetConfig> & Record<string, unknown>;\n return {\n ...WIDGET_CONFIG_DEFAULTS,\n ...input,\n defaultTier: sanitizeDefaultTier(input.defaultTier),\n // Coerce a legacy stored numeric picker radius (pre-preset configs) to a preset.\n pickerBorderRadius: sanitizeRadiusPreset(input.pickerBorderRadius),\n header: { ...WIDGET_CONFIG_DEFAULTS.header, ...(input.header ?? {}) },\n layout: {\n ...WIDGET_CONFIG_DEFAULTS.layout,\n ...(input.layout ?? {}),\n // Coerce a legacy stored numeric radius (pre-preset configs) to a preset.\n borderRadius: sanitizeRadiusPreset(\n (input.layout as Partial<LayoutConfig> | undefined)?.borderRadius,\n ),\n },\n productList: {\n ...WIDGET_CONFIG_DEFAULTS.productList,\n ...(input.productList ?? {}),\n },\n pricing: {\n ...WIDGET_CONFIG_DEFAULTS.pricing,\n ...(input.pricing ?? {}),\n },\n cta: { ...WIDGET_CONFIG_DEFAULTS.cta, ...(input.cta ?? {}) },\n savingsBar: {\n ...WIDGET_CONFIG_DEFAULTS.savingsBar,\n ...(input.savingsBar ?? {}),\n },\n countdown: {\n ...WIDGET_CONFIG_DEFAULTS.countdown,\n ...(input.countdown ?? {}),\n },\n popularBadge: {\n ...WIDGET_CONFIG_DEFAULTS.popularBadge,\n ...(input.popularBadge ?? {}),\n },\n };\n}\n\nfunction sanitizeDefaultTier(\n raw: unknown,\n): \"first\" | \"best_value\" | number {\n if (raw === \"first\" || raw === \"best_value\") return raw;\n if (typeof raw === \"number\" && Number.isInteger(raw) && raw >= 0) return raw;\n return WIDGET_CONFIG_DEFAULTS.defaultTier;\n}\n\nfunction sanitizeRadiusPreset(raw: unknown): RadiusPreset {\n if (raw === \"none\" || raw === \"subtle\" || raw === \"rounded\" || raw === \"round\") {\n return raw;\n }\n return WIDGET_CONFIG_DEFAULTS.layout.borderRadius;\n}\n\n/**\n * Flatten the nested WidgetConfig to the CSS-custom-property keys consumed\n * by `packages/widget/src/styles/*.css`. Keys match the `--lb-*` variable\n * names (without the `--lb-` prefix). Keep in lockstep with the matching\n * function in `app/lib/bundle-types.ts` — the admin's in-app preview and\n * the shipped web component both read the same variable names, so drift\n * between the two maps is a merchant-visible rendering bug.\n */\nexport function flattenWidgetConfig(\n config: WidgetConfig,\n): Record<string, string | number | boolean> {\n return {\n primaryColor: config.cta.primaryColor,\n backgroundColor: config.layout.backgroundColor,\n textColor: config.productList.textColor,\n borderColor: config.layout.borderColor,\n borderWidth: config.layout.borderWidth,\n buttonTextColor: config.cta.buttonTextColor,\n ctaBorderWidth: config.cta.borderWidth,\n ctaBorderColor: config.cta.borderColor,\n // Global radius preset → px for the base --lb-radius; every element derives\n // its own radius from this (smaller controls via --lb-radius-sm in CSS).\n borderRadius: RADIUS_PRESET_PX[config.layout.borderRadius],\n countdownBgColor: config.header.countdownBgColor,\n countdownTextColor: config.header.countdownTextColor,\n thumbnailRatio: config.productList.thumbnailRatio,\n showProductPrice: config.productList.showPrice,\n showProductCompareAtPrice: config.productList.showCompareAtPrice,\n showProductUnitPrice: config.productList.showUnitPrice,\n showProductQuantity: config.productList.showQuantity,\n showCountdown: config.countdown.showCountdown,\n ctaText: config.cta.ctaText,\n showLowStockBadge: config.showLowStockBadge,\n lowStockThreshold: config.lowStockThreshold,\n lowStockBgColor: config.lowStockBgColor,\n lowStockTextColor: config.lowStockTextColor,\n showComparePrice: config.pricing.showComparePrice,\n showPerUnitPrice: config.pricing.showPerUnitPrice,\n showItemCount: config.pricing.showItemCount,\n showCompareAtPrice: config.pricing.showCompareAtPrice,\n showMostPopular: config.popularBadge.visible,\n popularBadgeText: config.popularBadge.text,\n popularBadgeBgColor: config.popularBadge.bgColor,\n popularBadgeTextColor: config.popularBadge.textColor,\n popularBadgeBorderWidth: config.popularBadge.borderWidth,\n popularBadgeBorderColor: config.popularBadge.borderColor,\n showSearch: config.showSearch,\n mixMatchShowTypeFilters: config.mixMatchShowTypeFilters,\n pickerBgColor: config.pickerBgColor,\n pickerTextColor: config.pickerTextColor,\n pickerBorderWidth: config.pickerBorderWidth,\n pickerBorderColor: config.pickerBorderColor,\n // Picker radius preset → px, same mapping as the global radius.\n pickerBorderRadius: RADIUS_PRESET_PX[config.pickerBorderRadius],\n pickerThumbnailRatio: config.pickerThumbnailRatio,\n pickerAddBgColor: config.pickerAddBgColor,\n pickerAddLabelColor: config.pickerAddLabelColor,\n pickerAddBorderWidth: config.pickerAddBorderWidth,\n pickerAddBorderColor: config.pickerAddBorderColor,\n defaultTier: config.defaultTier,\n ...(config.popularBadge.tierIndex !== undefined && {\n mostPopularTierIndex: config.popularBadge.tierIndex,\n }),\n };\n}\n\n/**\n * Map from `flattenWidgetConfig` keys to the actual CSS custom property\n * names consumed by the `bundle-*.css` stylesheets. The names are bespoke,\n * not mechanical, so we can't derive them with a naive camel-to-kebab\n * transform. Mirror of `CSS_VAR_MAP` in\n * `app/components/WidgetEditorPreview.tsx`.\n */\nexport const CSS_VAR_MAP: Record<string, string> = {\n primaryColor: \"--lb-primary-color\",\n backgroundColor: \"--lb-bg\",\n textColor: \"--lb-text\",\n borderColor: \"--lb-border\",\n borderWidth: \"--lb-border-width\",\n buttonTextColor: \"--lb-btn-text\",\n borderRadius: \"--lb-radius\",\n countdownBgColor: \"--lb-countdown-bg\",\n countdownTextColor: \"--lb-countdown-text\",\n ctaBorderWidth: \"--lb-cta-border-width\",\n ctaBorderColor: \"--lb-cta-border-color\",\n popularBadgeBgColor: \"--lb-popular-badge-bg\",\n popularBadgeTextColor: \"--lb-popular-badge-text\",\n popularBadgeBorderWidth: \"--lb-popular-badge-border-width\",\n popularBadgeBorderColor: \"--lb-popular-badge-border-color\",\n pickerBgColor: \"--lb-picker-bg\",\n pickerTextColor: \"--lb-picker-text\",\n pickerBorderWidth: \"--lb-picker-border-width\",\n pickerBorderColor: \"--lb-picker-border-color\",\n pickerBorderRadius: \"--lb-picker-radius\",\n pickerAddBgColor: \"--lb-picker-add-bg\",\n pickerAddLabelColor: \"--lb-picker-add-label\",\n pickerAddBorderWidth: \"--lb-picker-add-border-width\",\n pickerAddBorderColor: \"--lb-picker-add-border-color\",\n};\n\n/** Flat keys whose numeric value should be serialized with a `px` unit. */\nexport const PX_KEYS: ReadonlySet<string> = new Set([\n \"borderRadius\",\n \"borderWidth\",\n \"ctaBorderWidth\",\n \"popularBadgeBorderWidth\",\n \"pickerBorderWidth\",\n \"pickerBorderRadius\",\n \"pickerAddBorderWidth\",\n]);\n\n/**\n * Apply a WidgetConfig to a DOM element as CSS custom properties.\n *\n * Sets one `--lb-*` property per entry in CSS_VAR_MAP, px-suffixing numeric\n * dimensions, plus the boolean toggles that the Liquid template emits\n * as display values (`block`, `inline`, `flex`, or `none`):\n *\n * - `--lb-product-price-display` from productList.showPrice\n * - `--lb-product-compare-display` from productList.showCompareAtPrice\n * - `--lb-product-unit-price-display` from productList.showUnitPrice\n * - `--lb-product-qty-chip-display` from productList.showQuantity\n * - `--lb-product-qty-inline-display` from productList.showQuantity\n *\n * Anything the merchant didn't explicitly configure falls through to the\n * CSS file's own fallback value, so this call is safe on any element.\n */\nexport function applyWidgetConfigVars(\n el: { style: CSSStyleDeclaration },\n config: WidgetConfig,\n): void {\n const flat = flattenWidgetConfig(config);\n\n for (const [flatKey, cssVar] of Object.entries(CSS_VAR_MAP)) {\n const value = flat[flatKey];\n if (value === undefined || value === null) continue;\n const serialized = PX_KEYS.has(flatKey) ? `${value}px` : String(value);\n el.style.setProperty(cssVar, serialized);\n }\n\n // Conditional display toggles — merchant-configurable booleans that the\n // Liquid template emits as explicit display-mode CSS vars. We mirror that\n // exactly so the shipped bundle-*.css rules (which read these vars) hide\n // or show the right elements.\n el.style.setProperty(\n \"--lb-product-price-display\",\n config.productList.showPrice ? \"block\" : \"none\",\n );\n el.style.setProperty(\n \"--lb-product-compare-display\",\n config.productList.showCompareAtPrice ? \"inline\" : \"none\",\n );\n el.style.setProperty(\n \"--lb-product-unit-price-display\",\n config.productList.showUnitPrice ? \"block\" : \"none\",\n );\n // One merchant toggle, two elements: the fixed bundle's right-slot chip is a\n // flex box, every other row's count is inline text. A single var cannot carry\n // both \"on\" values, so the boolean drives one per element.\n el.style.setProperty(\n \"--lb-product-qty-chip-display\",\n config.productList.showQuantity ? \"inline-flex\" : \"none\",\n );\n el.style.setProperty(\n \"--lb-product-qty-inline-display\",\n config.productList.showQuantity ? \"inline\" : \"none\",\n );\n\n // Variant dropdown chevrons — embed the border color into the SVG stroke\n // so the arrow auto-matches. Mirrors the Liquid template in\n // `extensions/bundle-theme/blocks/bundle-widget.liquid` and the admin\n // preview helper in `app/components/WidgetEditorPreview.tsx`.\n el.style.setProperty(\n \"--lb-variant-chevron\",\n variantChevronUrl(config.layout.borderColor),\n );\n el.style.setProperty(\n \"--lb-picker-variant-chevron\",\n variantChevronUrl(config.pickerBorderColor),\n );\n\n // Thumbnail ratio — one merchant enum drives three CSS vars. \"original\"\n // disables aspect-ratio cropping (img renders at intrinsic ratio); the\n // other three values force a fixed aspect with object-fit: cover.\n const ratioVars = thumbnailRatioVars(config.productList.thumbnailRatio);\n el.style.setProperty(\"--lb-thumbnail-aspect-ratio\", ratioVars.aspectRatio);\n el.style.setProperty(\"--lb-thumbnail-img-fit\", ratioVars.imgFit);\n el.style.setProperty(\"--lb-thumbnail-img-height\", ratioVars.imgHeight);\n\n // Picker-modal thumbnail ratio — independent from the main widget so\n // merchants can e.g. show tall picker thumbs while keeping square main\n // thumbs. Reuses `thumbnailRatioVars` with a different CSS-var prefix.\n const pickerRatioVars = thumbnailRatioVars(config.pickerThumbnailRatio);\n el.style.setProperty(\n \"--lb-picker-thumbnail-aspect-ratio\",\n pickerRatioVars.aspectRatio,\n );\n el.style.setProperty(\n \"--lb-picker-thumbnail-img-fit\",\n pickerRatioVars.imgFit,\n );\n el.style.setProperty(\n \"--lb-picker-thumbnail-img-height\",\n pickerRatioVars.imgHeight,\n );\n}\n\n/**\n * Build an inline-SVG `url(...)` value for the variant dropdown chevron,\n * with the given hex color embedded as the stroke. `#` must be URL-encoded\n * inside a data URI. Kept in lockstep with the Liquid template and admin\n * preview helper — any change to the path/size/stroke-width must update\n * all three.\n */\nfunction variantChevronUrl(strokeHex: string): string {\n const encoded = strokeHex.replace(/#/g, \"%23\");\n return `url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath d='M3 4.5l3 3 3-3' fill='none' stroke='${encoded}' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E\")`;\n}\n\n/**\n * Map the merchant-facing thumbnail-ratio choice to the three CSS vars\n * the bundle stylesheets read. Exported so the Liquid template's mirror\n * of this logic stays in lockstep — any change to the value strings here\n * must be reflected in `bundle-widget.liquid` and the admin preview.\n */\nexport function thumbnailRatioVars(ratio: WidgetConfig[\"productList\"][\"thumbnailRatio\"]): {\n aspectRatio: string;\n imgFit: string;\n imgHeight: string;\n} {\n switch (ratio) {\n case \"tall\":\n return { aspectRatio: \"3 / 4\", imgFit: \"cover\", imgHeight: \"100%\" };\n case \"wide\":\n return { aspectRatio: \"4 / 3\", imgFit: \"cover\", imgHeight: \"100%\" };\n case \"original\":\n return { aspectRatio: \"auto\", imgFit: \"contain\", imgHeight: \"auto\" };\n case \"square\":\n default:\n return { aspectRatio: \"1 / 1\", imgFit: \"cover\", imgHeight: \"100%\" };\n }\n}\n","/**\n * Parse metaobject fields into a typed ParsedBundle (discriminated union).\n * Returns null if the bundle is not renderable (invalid type, inactive,\n * outside schedule) — callers who want structured reasons for \"not\n * renderable\" should use parseMetaobjectBundleStrict, which throws a\n * BundleParseError with a reason code.\n */\nimport type { MetaobjectField, Product } from \"../storefront-api/types\";\nimport {\n BundleParseError,\n type ParsedBundle,\n type BundleType,\n type BundleStatus,\n type DiscountConfig,\n type WidgetConfig,\n type VolumeTier,\n type FixedBundleData,\n type VolumeBundleData,\n type MixMatchBundleData,\n type BogoBundleData,\n type MultiStepBundleData,\n type ParsedMultiStep,\n} from \"./types\";\nimport { mergeWidgetConfig } from \"./widget-config\";\n\nconst VALID_BUNDLE_TYPES = new Set<BundleType>([\n \"fixed\",\n \"mix_match\",\n \"volume\",\n \"bogo\",\n \"multi_step\",\n]);\nconst ACTIVE_STATUSES = new Set<BundleStatus>([\"active\"]);\n\n/**\n * Parse a metaobject's fields array into a structured ParsedBundle.\n * Returns null if the bundle is not renderable.\n */\nexport function parseMetaobjectBundle(\n metaobjectId: string,\n fields: MetaobjectField[],\n): ParsedBundle | null {\n try {\n return parseMetaobjectBundleStrict(metaobjectId, fields);\n } catch (err) {\n if (err instanceof BundleParseError) return null;\n throw err;\n }\n}\n\n/**\n * Same as parseMetaobjectBundle but throws BundleParseError with a\n * structured reason instead of returning null.\n */\nexport function parseMetaobjectBundleStrict(\n metaobjectId: string,\n fields: MetaobjectField[],\n): ParsedBundle {\n const fieldMap = new Map(fields.map((f) => [f.key, f]));\n\n const title = fieldMap.get(\"title\")?.value ?? \"Bundle\";\n const rawBundleType = fieldMap.get(\"bundle_type\")?.value;\n const rawStatus = fieldMap.get(\"status\")?.value;\n\n if (!rawBundleType || !VALID_BUNDLE_TYPES.has(rawBundleType as BundleType)) {\n throw new BundleParseError(\n `Invalid or missing bundle_type: ${rawBundleType ?? \"null\"}`,\n \"invalid_type\",\n );\n }\n const bundleType = rawBundleType as BundleType;\n\n if (!rawStatus || !ACTIVE_STATUSES.has(rawStatus as BundleStatus)) {\n throw new BundleParseError(\n `Bundle is not active: status=${rawStatus ?? \"null\"}`,\n \"inactive\",\n );\n }\n const status = rawStatus as BundleStatus;\n\n const startsAt = fieldMap.get(\"starts_at\")?.value ?? null;\n const endsAt = fieldMap.get(\"ends_at\")?.value ?? null;\n const now = new Date();\n\n if (startsAt) {\n const start = new Date(startsAt);\n if (Number.isNaN(start.getTime())) {\n throw new BundleParseError(\n `Invalid starts_at: ${startsAt}`,\n \"invalid_type\",\n );\n }\n if (start > now) {\n throw new BundleParseError(\n `Bundle not yet started: starts_at=${startsAt}`,\n \"not_started\",\n );\n }\n }\n if (endsAt) {\n const end = new Date(endsAt);\n if (Number.isNaN(end.getTime())) {\n throw new BundleParseError(\n `Invalid ends_at: ${endsAt}`,\n \"invalid_type\",\n );\n }\n if (end < now) {\n throw new BundleParseError(\n `Bundle has expired: ends_at=${endsAt}`,\n \"expired\",\n );\n }\n }\n\n const products = resolveProducts(fieldMap);\n const discountConfig = parseDiscountConfig(fieldMap);\n const widgetConfig = parseWidgetConfig(fieldMap);\n\n const base = {\n id: metaobjectId,\n title,\n description: fieldMap.get(\"description\")?.value || null,\n status,\n products,\n discountConfig,\n widgetConfig,\n startsAt,\n endsAt,\n discountLabel: fieldMap.get(\"discount_label\")?.value ?? null,\n selectedVariantIds: parseSelectedVariantIds(fieldMap),\n productQuantities: parseNumericRecord(fieldMap, \"product_quantities\"),\n variantQuantities: parseNumericRecord(fieldMap, \"variant_quantities\"),\n marketVisibility: parseMarketVisibility(fieldMap),\n marketIds: parseMarketIds(fieldMap),\n countryCodes: parseStringArrayField(fieldMap, \"country_codes\"),\n companyLocationIds: parseStringArrayField(fieldMap, \"company_location_ids\"),\n abTestId: fieldMap.get(\"ab_test_id\")?.value || null,\n abWeight: parseAbWeight(fieldMap),\n };\n\n switch (bundleType) {\n case \"fixed\": {\n const result: FixedBundleData = { ...base, bundleType: \"fixed\" };\n return result;\n }\n case \"volume\": {\n const result: VolumeBundleData = {\n ...base,\n bundleType: \"volume\",\n volumeTiers: parseVolumeTiers(fieldMap),\n };\n return result;\n }\n case \"mix_match\": {\n const result: MixMatchBundleData = {\n ...base,\n bundleType: \"mix_match\",\n minQuantity: parseIntField(fieldMap, \"min_quantity\", { min: 1 }),\n maxQuantity: null,\n productRules: parseRuleMap(fieldMap, \"product_rules\"),\n variantRules: parseRuleMap(fieldMap, \"variant_rules\"),\n };\n return result;\n }\n case \"bogo\": {\n const result: BogoBundleData = {\n ...base,\n bundleType: \"bogo\",\n ...parseBogoConfig(fieldMap),\n };\n return result;\n }\n case \"multi_step\": {\n const result: MultiStepBundleData = {\n ...base,\n bundleType: \"multi_step\",\n steps: parseMultiSteps(fieldMap),\n productRules: parseRuleMap(fieldMap, \"product_rules\"),\n variantRules: parseRuleMap(fieldMap, \"variant_rules\"),\n collectionProductIds: parseCollectionProductIds(fieldMap),\n };\n return result;\n }\n }\n}\n\n/**\n * Parse the `steps` JSON metaobject field into `ParsedMultiStep[]`.\n * Fail-closed like `parseBogoConfig`: a missing / non-array / empty `steps`\n * field makes the bundle unrenderable (`invalid_type`) — a multi-step\n * bundle without steps has no picker to walk. Per-step hygiene:\n * - entries that aren't objects, or whose `name` isn't a string, are\n * dropped as malformed;\n * - `minQuantity` clamps to >= 1 (default 1);\n * - `maxQuantity` is null (unlimited) unless a finite number, and never\n * drops below the clamped minimum;\n * - `productIds` / `collectionIds` keep string entries only.\n */\nfunction parseMultiSteps(\n fieldMap: Map<string, MetaobjectField>,\n): ParsedMultiStep[] {\n const raw = parseJsonField(fieldMap, \"steps\");\n if (!Array.isArray(raw)) {\n throw new BundleParseError(\"Invalid or missing steps\", \"invalid_type\");\n }\n const stringList = (v: unknown): string[] =>\n Array.isArray(v) ? v.filter((x): x is string => typeof x === \"string\") : [];\n const steps: ParsedMultiStep[] = [];\n for (const entry of raw) {\n if (!entry || typeof entry !== \"object\" || Array.isArray(entry)) continue;\n const s = entry as Record<string, unknown>;\n if (typeof s.name !== \"string\") continue;\n const minRaw = typeof s.minQuantity === \"number\" ? s.minQuantity : Number(s.minQuantity);\n const minQuantity =\n Number.isFinite(minRaw) && minRaw >= 1 ? Math.floor(minRaw) : 1;\n let maxQuantity: number | null = null;\n if (typeof s.maxQuantity === \"number\" && Number.isFinite(s.maxQuantity)) {\n maxQuantity = Math.max(minQuantity, Math.floor(s.maxQuantity));\n }\n steps.push({\n name: s.name,\n minQuantity,\n maxQuantity,\n productIds: stringList(s.productIds),\n collectionIds: stringList(s.collectionIds),\n });\n }\n if (steps.length === 0) {\n throw new BundleParseError(\n \"multi_step bundle has no valid steps\",\n \"invalid_type\",\n );\n }\n return steps;\n}\n\n/**\n * Record exact per-collection membership while the `collection` field's\n * references are in hand: each collection node carries its own `products`\n * connection, so collection GID → product GIDs attribution is lossless.\n * Consumed by `productsForStep` to scope each wizard step's product pool.\n */\nfunction parseCollectionProductIds(\n fieldMap: Map<string, MetaobjectField>,\n): Record<string, string[]> {\n const out: Record<string, string[]> = {};\n const collectionField = fieldMap.get(\"collection\");\n if (!collectionField?.references?.nodes) return out;\n for (const node of collectionField.references.nodes) {\n if (!(\"products\" in node) || !node.products?.nodes) continue;\n out[node.id] = node.products.nodes.map((p) => p.id);\n }\n return out;\n}\n\n/**\n * Parse the `bogo_config` JSON metaobject field. Both product GIDs are\n * required — a malformed or missing config makes the bundle unrenderable\n * (invalid_type), matching the fail-closed posture of the discount\n * function. Quantities clamp to 1..99 with a default of 1.\n */\nfunction parseBogoConfig(fieldMap: Map<string, MetaobjectField>): {\n buyProductId: string;\n getProductId: string;\n buyQuantity: number;\n getQuantity: number;\n buyVariantIds: string[];\n getVariantIds: string[];\n} {\n const raw = parseJsonField(fieldMap, \"bogo_config\");\n if (!raw || typeof raw !== \"object\" || Array.isArray(raw)) {\n throw new BundleParseError(\"Invalid or missing bogo_config\", \"invalid_type\");\n }\n const cfg = raw as Record<string, unknown>;\n const buyProductId = typeof cfg.buyProductId === \"string\" ? cfg.buyProductId : \"\";\n const getProductId = typeof cfg.getProductId === \"string\" ? cfg.getProductId : \"\";\n if (!buyProductId || !getProductId) {\n throw new BundleParseError(\"bogo_config is missing buy/get product ids\", \"invalid_type\");\n }\n const clampQty = (v: unknown): number => {\n const n = typeof v === \"number\" ? v : Number(v);\n if (!Number.isFinite(n) || n < 1) return 1;\n return Math.min(99, Math.floor(n));\n };\n // Per-side variant narrowing (issue #436): positional selected_variant_ids\n // holds the union for a same-product bundle, so each side carries its own\n // list. Empty (or malformed) = that side is not narrowed.\n const sideVariants = (v: unknown): string[] =>\n Array.isArray(v) ? v.filter((id): id is string => typeof id === \"string\") : [];\n return {\n buyProductId,\n getProductId,\n buyQuantity: clampQty(cfg.buyQuantity),\n getQuantity: clampQty(cfg.getQuantity),\n buyVariantIds: sideVariants(cfg.buyVariantIds),\n getVariantIds: sideVariants(cfg.getVariantIds),\n };\n}\n\nfunction resolveProducts(fieldMap: Map<string, MetaobjectField>): Product[] {\n const products: Product[] = [];\n\n const productsField = fieldMap.get(\"products\");\n if (productsField?.reference && \"variants\" in productsField.reference) {\n products.push(productsField.reference as Product);\n }\n\n if (productsField?.references?.nodes) {\n for (const node of productsField.references.nodes) {\n if (\"variants\" in node) {\n products.push(node as Product);\n } else if (\"products\" in node && node.products?.nodes) {\n products.push(...node.products.nodes);\n }\n }\n }\n\n const collectionField = fieldMap.get(\"collection\");\n if (collectionField?.references?.nodes) {\n for (const node of collectionField.references.nodes) {\n if (\"products\" in node && node.products?.nodes) {\n products.push(...node.products.nodes);\n }\n }\n }\n\n return products;\n}\n\nfunction parseDiscountConfig(\n fieldMap: Map<string, MetaobjectField>,\n): DiscountConfig {\n return {\n discountType:\n (fieldMap.get(\"discount_type\")?.value as \"percentage\" | \"fixed_amount\") ??\n \"percentage\",\n discountValue: parseFloat(fieldMap.get(\"discount_value\")?.value ?? \"0\"),\n allowStacking: fieldMap.get(\"allow_stacking\")?.value === \"true\",\n };\n}\n\n/**\n * Parse the merchant's widget_config metaobject field. The JSON is authored\n * by the Zod schema in app/lib/bundle-types.ts, so consumers can trust the\n * shape — but old bundles may be missing fields added after they were\n * created. mergeWidgetConfig fills those gaps from WIDGET_CONFIG_DEFAULTS,\n * so every renderer downstream gets a fully populated config.\n */\nfunction parseWidgetConfig(\n fieldMap: Map<string, MetaobjectField>,\n): WidgetConfig {\n return mergeWidgetConfig(parseJsonField(fieldMap, \"widget_config\"));\n}\n\n/**\n * Parse `selected_variant_ids`: a list of per-product variant-GID arrays,\n * positionally aligned with `products`. Returns null when the merchant\n * didn't restrict variants (any entry means full list allowed).\n */\nfunction parseSelectedVariantIds(\n fieldMap: Map<string, MetaobjectField>,\n): string[][] | null {\n const raw = parseJsonField(fieldMap, \"selected_variant_ids\");\n if (!Array.isArray(raw)) return null;\n const result: string[][] = [];\n for (const entry of raw) {\n if (Array.isArray(entry)) {\n result.push(\n entry.filter((x): x is string => typeof x === \"string\"),\n );\n } else {\n result.push([]);\n }\n }\n return result;\n}\n\n/**\n * Parse a JSON record of `{ [GID]: number }`. Used for\n * `product_quantities` and `variant_quantities`. Non-numeric values are\n * dropped; returns an empty object when the field is absent or malformed.\n */\nfunction parseNumericRecord(\n fieldMap: Map<string, MetaobjectField>,\n key: string,\n): Record<string, number> {\n const raw = parseJsonField(fieldMap, key);\n // Arrays pass `typeof === \"object\"`. Reject them explicitly so a\n // malformed metaobject field (e.g. `[1, 2, 3]` where an object was\n // expected) doesn't produce a `{ \"0\": 1, \"1\": 2, ... }` record.\n if (!raw || typeof raw !== \"object\" || Array.isArray(raw)) return {};\n const record: Record<string, number> = {};\n for (const [k, v] of Object.entries(raw)) {\n const num = typeof v === \"number\" ? v : Number(v);\n // Defense-in-depth: Zod enforces 0/1–99 at the write path. The parser\n // also clamps at read time so a metafield that bypasses Zod (seed script,\n // direct Admin API edit, manual Shopify CLI) can't feed `Number.MAX_SAFE_INTEGER`\n // downstream into `CartLineInput.quantity` — which would crash Hydrogen\n // or Storefront Cart API with an opaque error. Zero is preserved\n // (merchant-set opt-out); renderers filter zero-qty rows. Negative and\n // out-of-range values are dropped as malformed.\n if (Number.isFinite(num) && num >= 0 && num <= 99) {\n record[k] = Math.floor(num);\n }\n }\n return record;\n}\n\n/**\n * Parse a JSON rule-map metaobject field into a record of\n * `{ [gid]: { min, max, required? } }`. Drops malformed entries.\n * Clamps min/max to 1..99 and ensures `min <= max`. `required` is kept only\n * when strictly `true` (absent otherwise — no `false` noise). Returns an\n * empty object when the field is absent or unparseable.\n *\n * Shared by `product_rules` (keyed by product GID) and `variant_rules` (keyed\n * by variant GID). The two levels are the same shape by design — a variant\n * rule refines a product rule rather than being a different kind of thing —\n * so the parse, the clamping and the malformed-entry handling are written\n * once.\n */\nfunction parseRuleMap(\n fieldMap: Map<string, MetaobjectField>,\n key: \"product_rules\" | \"variant_rules\",\n): Record<string, { min: number; max: number; required?: boolean }> {\n const raw = parseJsonField(fieldMap, key);\n if (!raw || typeof raw !== \"object\" || Array.isArray(raw)) return {};\n const out: Record<string, { min: number; max: number; required?: boolean }> =\n {};\n for (const [pid, rule] of Object.entries(raw)) {\n if (!rule || typeof rule !== \"object\" || Array.isArray(rule)) continue;\n const { min, max, required } = rule as {\n min?: unknown;\n max?: unknown;\n required?: unknown;\n };\n const minN = typeof min === \"number\" ? min : Number(min);\n const maxN = typeof max === \"number\" ? max : Number(max);\n if (!Number.isFinite(minN) || !Number.isFinite(maxN)) continue;\n const minClamped = Math.max(1, Math.min(99, Math.floor(minN)));\n const maxClamped = Math.max(minClamped, Math.min(99, Math.floor(maxN)));\n out[pid] =\n required === true\n ? { min: minClamped, max: maxClamped, required: true }\n : { min: minClamped, max: maxClamped };\n }\n return out;\n}\n\nfunction parseMarketVisibility(\n fieldMap: Map<string, MetaobjectField>,\n): \"all\" | \"specific\" {\n const raw = fieldMap.get(\"market_visibility\")?.value;\n return raw === \"specific\" ? \"specific\" : \"all\";\n}\n\nfunction parseMarketIds(fieldMap: Map<string, MetaobjectField>): string[] {\n return parseStringArrayField(fieldMap, \"markets\");\n}\n\n/** Shared shape for the three market-scoping list fields: a json field\n * holding an array of strings, absent on pre-projection metaobjects. */\nfunction parseStringArrayField(\n fieldMap: Map<string, MetaobjectField>,\n key: string,\n): string[] {\n const raw = fieldMap.get(key)?.value;\n if (!raw) return [];\n try {\n const parsed = JSON.parse(raw);\n if (!Array.isArray(parsed)) return [];\n return parsed.filter((v): v is string => typeof v === \"string\");\n } catch {\n return [];\n }\n}\n\n/**\n * This side's share of traffic, 0–100.\n *\n * Clamped rather than trusted: the field is merchant-adjacent data coming back\n * from a metaobject, and a weight outside the range would make `isInBucket`\n * silently always- or never-true.\n */\nfunction parseAbWeight(fieldMap: Map<string, MetaobjectField>): number {\n const raw = fieldMap.get(\"ab_weight\")?.value;\n if (!raw) return 0;\n const parsed = Number.parseInt(raw, 10);\n if (Number.isNaN(parsed)) return 0;\n return Math.min(100, Math.max(0, parsed));\n}\n\nfunction parseVolumeTiers(\n fieldMap: Map<string, MetaobjectField>,\n): VolumeTier[] {\n const raw = parseJsonField(fieldMap, \"volume_tiers\");\n if (!Array.isArray(raw)) return [];\n return raw\n .filter(\n (t): t is Record<string, unknown> => typeof t === \"object\" && t !== null,\n )\n .map(parseOneVolumeTier);\n}\n\n// Volume tiers are stored as `{ minQuantity, percentage?, amount? }` on the\n// metaobject (see admin VolumeTierSchema in app/lib/bundle-types.ts). The\n// parent bundle's `discountConfig.discountType` tells renderers which of the\n// two magnitude fields to use for each tier.\nfunction parseOneVolumeTier(t: Record<string, unknown>): VolumeTier {\n const minQty = Number(t.minQuantity ?? 0);\n const tier: VolumeTier = {\n // Reject NaN / Infinity / negatives — same discipline as\n // percentage/amount below. Malformed tiers collapse to 0 so the\n // renderer's bounds checks can drop them deterministically.\n minQuantity: Number.isFinite(minQty) && minQty >= 0 ? minQty : 0,\n };\n if (typeof t.percentage === \"number\" && Number.isFinite(t.percentage)) {\n tier.percentage = t.percentage;\n }\n if (typeof t.amount === \"number\" && Number.isFinite(t.amount)) {\n tier.amount = t.amount;\n }\n return tier;\n}\n\nfunction parseJsonField(\n fieldMap: Map<string, MetaobjectField>,\n key: string,\n): unknown {\n const value = fieldMap.get(key)?.value;\n if (!value) return null;\n try {\n return JSON.parse(value);\n } catch {\n return null;\n }\n}\n\nfunction parseIntField(\n fieldMap: Map<string, MetaobjectField>,\n key: string,\n options: { min?: number } = {},\n): number | null {\n const value = fieldMap.get(key)?.value;\n if (!value) return null;\n const num = parseInt(value, 10);\n if (isNaN(num)) return null;\n if (options.min !== undefined && num < options.min) return null;\n return num;\n}\n","/**\n * Store→presentment currency conversion for merchant-configured amounts.\n *\n * Merchant-configured money (fixed_amount values, flat_price targets,\n * volume tier amounts) is stored in the shop's STORE currency, while every\n * price the SDK renders comes from the Storefront API in the buyer's\n * presentment currency. The checkout-side Rust discount function converts\n * configured amounts by its input's `presentmentCurrencyRate`; a headless\n * preview that skips the same conversion quotes a sale price the checkout\n * won't charge on any \"local currencies\" market.\n *\n * The rate itself is consumer-supplied — a headless storefront knows its\n * own currency context (Hydrogen's `localization`, a cart's\n * `presentmentCurrencyRate`, or a custom FX source); the SDK cannot guess\n * it. Percentages are currency-agnostic and never converted.\n */\nimport type { ParsedBundle } from \"./types\";\n\n/**\n * Clamp a consumer-supplied rate to something safe to multiply by.\n *\n * Mirrors the Rust function's `presentment_rate`: anything non-finite or\n * not strictly positive falls back to 1 — no conversion is the safest\n * failure mode. Accepts strings because Shopify surfaces the rate as one\n * (`window.Shopify.currency.rate`, Money scalar fields).\n */\nexport function normalizeCurrencyRate(\n rate: number | string | null | undefined,\n): number {\n const num = typeof rate === \"string\" ? parseFloat(rate) : rate;\n return typeof num === \"number\" && Number.isFinite(num) && num > 0 ? num : 1;\n}\n\n/**\n * Return a copy of `bundle` whose merchant-configured store-currency\n * amounts are converted to presentment currency at `rate`.\n *\n * Converts `discountConfig.discountValue` for every non-percentage\n * discount type (fixed_amount today, plus flat_price which reaches\n * runtime through the parser's pass-through) and each volume tier's\n * `amount`. Amounts stay in unrounded major units — rendering code\n * rounds to cents at the same point it always has, matching the Rust\n * function, which multiplies unrounded f64 majors.\n *\n * A rate of 1 (or anything `normalizeCurrencyRate` rejects) returns the\n * input object unchanged, so store-currency callers pay nothing.\n */\nexport function convertBundleToPresentment<T extends ParsedBundle>(\n bundle: T,\n rate: number | string | null | undefined,\n): T {\n const r = normalizeCurrencyRate(rate);\n if (r === 1) return bundle;\n\n const converted = { ...bundle };\n\n if (bundle.discountConfig.discountType !== \"percentage\") {\n converted.discountConfig = {\n ...bundle.discountConfig,\n discountValue: bundle.discountConfig.discountValue * r,\n };\n }\n\n if (\"volumeTiers\" in bundle && Array.isArray(bundle.volumeTiers)) {\n (converted as { volumeTiers: typeof bundle.volumeTiers }).volumeTiers =\n bundle.volumeTiers.map((tier) =>\n typeof tier.amount === \"number\"\n ? { ...tier, amount: tier.amount * r }\n : tier,\n );\n }\n\n return converted;\n}\n","/**\n * Product-aware bundle fetch — the headless analogue of the classic Liquid\n * theme block.\n *\n * Classic-theme widget reads `product.metafields[\"$app\"].active_bundle_ids`\n * inside Liquid and renders every active bundle that applies. Headless\n * storefronts get the same behaviour via this helper: pass a product\n * handle, receive `ParsedBundle[]` for every active bundle configured\n * against that product.\n *\n * The caller is responsible for knowing the handle — either because their\n * storefront template already knows it (Hydrogen loader, Next.js param),\n * or because the widget auto-detects from `/products/<handle>` URLs.\n *\n * Non-active bundles (draft / inactive / scheduled / expired) are silently\n * skipped rather than throwing — the calling surface is a list, not a\n * single lookup. Surface errors only for genuine failures (network /\n * GraphQL / invalid schema).\n */\nimport { createStorefrontClient } from \"../storefront-api/client\";\nimport {\n BUNDLES_FOR_PRODUCT_QUERY,\n type BundlesForProductResponse,\n} from \"../storefront-api/queries\";\nimport { parseMetaobjectBundleStrict } from \"./parser\";\nimport { convertBundleToPresentment } from \"./presentment\";\nimport { BundleParseError, type ParsedBundle } from \"./types\";\nimport type { MetaobjectField } from \"../storefront-api/types\";\n\nexport interface FetchBundlesForProductOptions {\n shopDomain: string;\n storefrontAccessToken: string;\n productHandle: string;\n /** Forwarded as `Shopify-Storefront-Buyer-IP` — required for SSR. */\n buyerIp?: string;\n signal?: AbortSignal;\n apiVersion?: string;\n /**\n * Store→presentment currency rate. Merchant-configured amounts\n * (fixed_amount / flat_price values, volume tier amounts) are stored in\n * the shop's store currency; pass the rate for the buyer's currency so\n * quoted sale prices match what the checkout's discount function\n * charges. Omit (or pass 1) on store-currency storefronts. See\n * `convertBundleToPresentment`.\n */\n currencyRate?: number;\n}\n\n/**\n * Reasons a bundle is expected-not-renderable (skipped silently, not\n * surfaced as an error). Matches `BundleParseFailReason` minus\n * `not_found`, which indicates a genuine data problem worth propagating.\n *\n * `invalid_type` is skippable for forward compatibility: when the app\n * ships a bundle type this SDK version doesn't know yet (as happened when\n * \"bogo\" was added), the unknown bundle is skipped rather than failing the\n * whole product's bundle list.\n */\nconst SKIPPABLE_REASONS = new Set([\"inactive\", \"not_started\", \"expired\", \"invalid_type\"]);\n\nexport async function fetchBundlesForProduct(\n options: FetchBundlesForProductOptions,\n): Promise<ParsedBundle[]> {\n const client = createStorefrontClient({\n shopDomain: options.shopDomain,\n accessToken: options.storefrontAccessToken,\n buyerIp: options.buyerIp,\n apiVersion: options.apiVersion,\n });\n\n const data = await client.query<BundlesForProductResponse>(\n BUNDLES_FOR_PRODUCT_QUERY,\n { handle: options.productHandle },\n { signal: options.signal },\n );\n\n if (!data.product) {\n // Product not found for this handle. The widget's caller (merchant's\n // storefront) is presumably on a valid product page, so this likely\n // means the handle is wrong or the product is unpublished. Return\n // empty rather than throwing — the widget renders nothing in that\n // case, which is the right UX (bundle section invisible).\n return [];\n }\n\n const refs = data.product.metafield?.references?.nodes ?? [];\n const bundles: ParsedBundle[] = [];\n\n for (const ref of refs) {\n try {\n // The nested references query resolves products + collections to the\n // same shape BUNDLE_METAOBJECT_QUERY returns, so parser.ts doesn't\n // need a new code path.\n const bundle = parseMetaobjectBundleStrict(\n ref.id,\n ref.fields as MetaobjectField[],\n );\n bundles.push(convertBundleToPresentment(bundle, options.currencyRate));\n } catch (err) {\n if (err instanceof BundleParseError && SKIPPABLE_REASONS.has(err.reason)) {\n continue;\n }\n throw err;\n }\n }\n\n return bundles;\n}\n","// Volume tier savings calculation. `discountType` lives on the parent\n// bundle (bundle.discountConfig.discountType); each tier carries the\n// magnitude in either `percentage` (whole-number, e.g. 10 = 10%) or\n// `amount` (currency units, e.g. 5.00 = $5 off per unit).\nimport type { DiscountConfig, VolumeTier } from \"./types\";\n\nexport interface TierSavings {\n tier: VolumeTier;\n unitPrice: number;\n savings: number;\n savingsPercent: number;\n isActive: boolean;\n}\n\n/**\n * Calculate savings for each tier at a given base price and quantity.\n *\n * Output order matches input order. Tier order is merchant-controlled in the\n * admin and is the storefront display order, so renderers can map the result\n * positionally onto `bundle.volumeTiers`.\n */\nexport function calculateTierSavings(\n tiers: VolumeTier[],\n basePrice: number,\n currentQuantity: number,\n discountType: DiscountConfig[\"discountType\"],\n): TierSavings[] {\n // Work in integer cents with the discount floored per unit — the same\n // arithmetic the Liquid widget and the checkout apply. Float-dollar math\n // here drifted a cent from the theme ($902.45 vs $902.46 on a 5% tier of\n // $949.95), and two surfaces showing different unit prices for the same\n // tier reads as a pricing bug to the shopper.\n const baseCents = Math.round(basePrice * 100);\n return tiers.map((tier) => {\n const discountCents =\n discountType === \"percentage\"\n ? Math.floor((baseCents * (tier.percentage ?? 0)) / 100)\n : Math.round((tier.amount ?? 0) * 100);\n\n const unitCents = Math.max(0, baseCents - discountCents);\n const unitPrice = unitCents / 100;\n const savings = (baseCents - unitCents) / 100;\n const savingsPercent =\n baseCents > 0 ? ((baseCents - unitCents) / baseCents) * 100 : 0;\n const isActive = currentQuantity >= tier.minQuantity;\n\n return { tier, unitPrice, savings, savingsPercent, isActive };\n });\n}\n\n/**\n * Get the active tier for a given quantity.\n * Returns the highest tier where quantity >= minQuantity.\n * Order-independent: matches how the discount function picks a tier.\n */\nexport function getActiveTier(\n tiers: VolumeTier[],\n quantity: number,\n): VolumeTier | null {\n const sorted = [...tiers].sort((a, b) => b.minQuantity - a.minQuantity);\n return sorted.find((t) => quantity >= t.minQuantity) ?? null;\n}\n\n/**\n * Smallest `minQuantity` across tiers — the floor below which no tier can be\n * bought. Renderers use it to decide whether a variant has enough stock to be\n * worth showing. Derived with a min rather than read off tiers[0] because tier\n * order is the merchant's display order, not a quantity ranking.\n */\nexport function getMinTierQuantity(tiers: VolumeTier[]): number {\n if (tiers.length === 0) return 1;\n return Math.min(...tiers.map((t) => t.minQuantity));\n}\n\n/** The tier fields the index-resolution helpers need. */\nexport interface TierDiscountValue {\n percentage?: number | null;\n amount?: number | null;\n}\n\n/**\n * Index of the tier with the largest savings value, first one winning ties.\n * Mirrors the `best_index` loop in `lb-volume.liquid` (strict `>` — the\n * earliest maximum keeps the crown), including its rule that fixed-amount\n * bundles rank by `amount` and percentage bundles by `percentage`.\n *\n * Shared by the \"best_value\" default tier and the popular badge's fallback\n * placement, so every surface lands on the same tier.\n */\nexport function bestValueTierIndex(\n tiers: TierDiscountValue[],\n discountType: string,\n): number {\n let best = 0;\n let bestIdx = 0;\n for (let i = 0; i < tiers.length; i++) {\n const savings =\n discountType === \"fixed_amount\"\n ? tiers[i].amount ?? 0\n : tiers[i].percentage ?? 0;\n if (savings > best) {\n best = savings;\n bestIdx = i;\n }\n }\n return bestIdx;\n}\n\n/**\n * Resolve the merchant's `defaultTier` setting to a tier index, exactly as\n * `lb-volume.liquid` does: `\"best_value\"` takes the best-savings index,\n * `\"first\"` (and anything non-numeric, which Liquid's `plus: 0` coerces to 0)\n * takes 0, and a number is clamped into `[0, tierCount - 1]`.\n */\nexport function resolveDefaultTierIndex(\n defaultTier: unknown,\n tierCount: number,\n bestIndex: number,\n): number {\n if (tierCount <= 0) return 0;\n if (defaultTier === \"best_value\") return Math.min(bestIndex, tierCount - 1);\n const n = typeof defaultTier === \"number\" ? defaultTier : 0;\n return Math.max(0, Math.min(Math.floor(n), tierCount - 1));\n}\n\n/**\n * Resolve where the \"Most popular\" badge sits: the pinned `tierIndex` when the\n * merchant set one, else the best-savings tier. Returns -1 when the badge is\n * hidden. A pinned index is deliberately NOT clamped — Liquid compares it\n * against each row's index, so an out-of-range pin renders no badge rather\n * than a wrong one, and every surface must fail the same way.\n */\nexport function resolvePopularTierIndex(\n tierIndex: unknown,\n visible: boolean,\n bestIndex: number,\n): number {\n if (!visible) return -1;\n if (typeof tierIndex === \"number\" && Number.isFinite(tierIndex)) {\n return Math.floor(tierIndex);\n }\n return bestIndex;\n}\n","// Market visibility gate, shared by every storefront consumer.\n//\n// Mirrors the checkout-side gate in the Rust discount function: a bundle\n// scoped to specific markets is shown when the buyer's country matches the\n// projected `countryCodes` (retail markets) OR the buyer is purchasing for\n// a company location in the projected `companyLocationIds` (B2B markets).\n// Either list may carry the \"*\" wildcard, projected from markets whose\n// condition targets all regions / all company locations.\n//\n// Market GIDs are matched too, for callers that still pass one — but they\n// are a legacy signal: Shopify deprecated market IDs for buyer targeting\n// (nested markets return only the most specific match), and a B2B buyer's\n// company-location market never surfaces through storefront localization\n// at all. Prefer supplying `countryCode` / `companyLocationId`.\n//\n// Mirrors the market gate in bundle-widget.liquid.\n\n/** The market scoping any bundle carries. Structural so it accepts a\n * `ParsedBundle` without widening the package's exported types. The two\n * projection lists are optional so pre-projection callers still compile. */\nexport interface MarketScoped {\n marketVisibility: \"all\" | \"specific\";\n marketIds: readonly string[];\n countryCodes?: readonly string[];\n companyLocationIds?: readonly string[];\n}\n\n/** Who is asking. All fields optional; supply what the host knows. */\nexport interface BuyerMarketContext {\n /** Legacy: Shopify Market id (GID or bare id) of the storefront context. */\n marketId?: string | null;\n /** ISO-3166-1 alpha-2 country code of the buyer, e.g. \"CA\". */\n countryCode?: string | null;\n /** CompanyLocation the buyer purchases for (GID or bare id), B2B only. */\n companyLocationId?: string | null;\n}\n\n/** Wildcard entry meaning \"any country\" / \"any company location\". */\nconst WILDCARD = \"*\";\n\n/** Trailing id of a GID-shaped identifier, accepting a GID or a bare id. */\nfunction idKey(id: string): string {\n return id.split(\"/\").pop() ?? id;\n}\n\nfunction listMatches(\n list: readonly string[] | undefined,\n candidate: string | null | undefined,\n compareByIdSuffix: boolean,\n): boolean {\n if (!list || list.length === 0) return false;\n if (list.includes(WILDCARD)) return true;\n if (!candidate) return false;\n if (!compareByIdSuffix) return list.includes(candidate);\n const key = idKey(candidate);\n return list.some((id) => idKey(id) === key);\n}\n\n/**\n * Whether a bundle should be shown to the buyer described by `context`.\n *\n * A bundle scoped to specific markets stays hidden when the caller supplies\n * no matching signal at all, since showing it would advertise a discount the\n * checkout-side gate will refuse to apply.\n *\n * The company-location wildcard requires the buyer to actually be purchasing\n * for a company location — it scopes the bundle to B2B buyers, not everyone.\n */\nexport function isVisibleToBuyer(\n bundle: MarketScoped,\n context: BuyerMarketContext,\n): boolean {\n if (bundle.marketVisibility !== \"specific\") return true;\n // Retail: buyer country vs projected country codes. The wildcard matches\n // even when the caller supplied no country — \"Worldwide\" means everyone.\n if (listMatches(bundle.countryCodes, context.countryCode, false)) {\n return true;\n }\n // B2B: purchasing company location vs projected locations. The wildcard\n // still requires a company location on the buyer.\n if (\n context.companyLocationId &&\n listMatches(bundle.companyLocationIds, context.companyLocationId, true)\n ) {\n return true;\n }\n // Legacy: direct market-id match for callers that only know the market.\n if (context.marketId) {\n const key = idKey(context.marketId);\n if (bundle.marketIds.some((id) => idKey(id) === key)) return true;\n }\n return false;\n}\n\n/**\n * Whether a bundle should be shown to a shopper in `marketId`.\n *\n * Legacy entry point — cannot see B2B buyers or country codes; prefer\n * `isVisibleToBuyer`. Kept because it is public API.\n */\nexport function isVisibleInMarket(\n bundle: MarketScoped,\n marketId: string | null | undefined,\n): boolean {\n return isVisibleToBuyer(bundle, { marketId });\n}\n","/**\n * Which of a bundle's products is the shopper actually looking at?\n *\n * A volume bundle can span several products, but the surface rendering it\n * sits on one product page and must price and sell that product — the theme\n * widget gets this for free from Liquid's `product` object. The headless\n * surfaces receive the same context explicitly (a `product-id` attribute on\n * `<lime-bundle>`, a `productId` prop on `<VolumeBundle>`) and resolve it\n * here, falling back to the bundle's first product only when the host never\n * said which product the shopper is on.\n */\n\nexport interface ProductContext {\n /**\n * The current product's id — either the full GID\n * (`gid://shopify/Product/123`) or the bare numeric id (`\"123\"`).\n */\n productId?: string | null;\n /** The current product's handle, e.g. from the page URL. */\n productHandle?: string | null;\n}\n\n/** True when `ref` (a full GID or bare numeric id) names the product `gid`. */\nfunction productIdMatches(gid: string, ref: string): boolean {\n if (gid === ref) return true;\n return /^\\d+$/.test(ref) && gid.endsWith(`/${ref}`);\n}\n\n/**\n * Resolve the product a bundle surface should price and sell.\n *\n * Precedence: explicit `productId` → `productHandle` → the bundle's first\n * product. A context that names a product outside the bundle falls through\n * to the next source rather than rendering nothing — the merchant may have\n * embedded the widget on an unrelated page, and the first-product fallback\n * is the documented pre-context behaviour.\n *\n * Returns `undefined` only when the bundle has no products at all.\n */\nexport function resolveContextProduct<\n P extends { id: string; handle: string },\n>(products: readonly P[], context?: ProductContext): P | undefined {\n const id = context?.productId?.trim();\n if (id) {\n const byId = products.find((p) => productIdMatches(p.id, id));\n if (byId) return byId;\n }\n\n const handle = context?.productHandle?.trim();\n if (handle) {\n const byHandle = products.find((p) => p.handle === handle);\n if (byHandle) return byHandle;\n }\n\n return products[0];\n}\n","/**\n * Quantity constraint validation for mix-and-match bundles.\n */\n\nexport interface QuantityValidation {\n valid: boolean;\n totalQuantity: number;\n message: string | null;\n}\n\nexport function validateQuantity(\n totalQuantity: number,\n minQuantity: number | null,\n maxQuantity: number | null,\n): QuantityValidation {\n if (minQuantity !== null && totalQuantity < minQuantity) {\n return {\n valid: false,\n totalQuantity,\n message: `Select at least ${minQuantity} item${minQuantity !== 1 ? \"s\" : \"\"}`,\n };\n }\n if (maxQuantity !== null && totalQuantity > maxQuantity) {\n return {\n valid: false,\n totalQuantity,\n message: `Select at most ${maxQuantity} item${maxQuantity !== 1 ? \"s\" : \"\"}`,\n };\n }\n return { valid: true, totalQuantity, message: null };\n}\n","/**\n * Canonical resolver for a bundle's per-line quantity.\n *\n * Bundle configs carry two qty fields: `productQuantities` (keyed by product\n * GID) and `variantQuantities` (keyed by variant GID, overrides product-level\n * when the customer picks that variant). The admin form strips product-level\n * entries that equal the default 1, so downstream consumers must consult\n * variantQuantities first and fall back to productQuantities then to 1.\n *\n * Before this helper existed, the fallback chain was reimplemented in five\n * places (React FixedBundle, React MixMatchBundle, headless widget fixed\n * renderer, Liquid mix-match IIFE, and the Rust discount function). They\n * drifted — PR #147 fixed the resulting cart-line split. Use this helper\n * everywhere the config is available to keep the TS/React/widget runtimes\n * in lockstep. The Liquid IIFE and Rust function have their own\n * implementations pinned by parity tests.\n */\n\nexport interface BundleQtySource {\n variantQuantities: Record<string, number>;\n productQuantities: Record<string, number>;\n}\n\n/**\n * Returns the configured qty for a cart line: `variantQuantities[variantId]`\n * if present, else `productQuantities[productId]`, else 1.\n *\n * Using `!== undefined` (not `??`) for the variant check so that a merchant\n * explicitly setting `0` is honoured — caller is expected to filter zero-qty\n * lines out, matching the headless widget's opt-out semantics.\n */\nexport function resolveBundleQty(\n bundle: BundleQtySource,\n productId: string,\n variantId: string,\n): number {\n const vq = bundle.variantQuantities[variantId];\n if (vq !== undefined) return vq;\n return bundle.productQuantities[productId] ?? 1;\n}\n","/**\n * Pure helpers for multi-step bundles, shared by the widget renderer and\n * the React components so the wizard math lives in one place.\n */\nimport type { Product } from \"../storefront-api/types\";\nimport type { MultiStepBundleData } from \"./types\";\n\n/**\n * Open units left in a step: `Infinity` when the step has no maximum,\n * otherwise `max - unitsInStep` floored at 0. Mirrors `stepHeadroom` in\n * `extensions/bundle-theme/assets/bundle-multi-step.js`.\n */\nexport function stepHeadroom(\n maxQuantity: number | null,\n unitsInStep: number,\n): number {\n if (maxQuantity == null) return Number.POSITIVE_INFINITY;\n return Math.max(0, maxQuantity - unitsInStep);\n}\n\n/**\n * The products belonging to one wizard step, in the flat `bundle.products`\n * order (pinned union first, then collection-resolved products — the order\n * `resolveProducts` emits).\n *\n * A product belongs to the step when its GID is in the step's pinned\n * `productIds`, or when it was resolved from one of the step's\n * `collectionIds`. Collection attribution is EXACT: the parser records\n * `bundle.collectionProductIds` (collection GID → product GIDs) while\n * resolving the `collection` metaobject field, because each collection\n * reference node carries its own `products` connection. No client-side\n * collection-membership query is needed.\n *\n * Duplicates (a product both pinned to the step and present in one of its\n * collections, or repeated across the flat array) are deduped by product\n * id, keeping the first occurrence.\n */\nexport function productsForStep(\n bundle: MultiStepBundleData,\n stepIndex: number,\n): Product[] {\n const step = bundle.steps[stepIndex];\n if (!step) return [];\n\n const pinned = new Set(step.productIds);\n const fromCollections = new Set<string>();\n for (const collectionId of step.collectionIds) {\n for (const productId of bundle.collectionProductIds[collectionId] ?? []) {\n fromCollections.add(productId);\n }\n }\n\n const seen = new Set<string>();\n const result: Product[] = [];\n for (const product of bundle.products) {\n if (seen.has(product.id)) continue;\n if (!pinned.has(product.id) && !fromCollections.has(product.id)) continue;\n seen.add(product.id);\n result.push(product);\n }\n return result;\n}\n","/**\n * Analytics event reporter for headless widgets.\n * Fires events to /api/analytics with single retry on failure.\n */\n\nexport interface AnalyticsConfig {\n shopDomain: string;\n appUrl: string; // Base URL of the Lime Bundles app\n}\n\nexport async function reportImpression(\n config: AnalyticsConfig,\n event: {\n bundleGid: string;\n bundleType: string;\n productId?: string;\n },\n): Promise<void> {\n await sendEvent(config, {\n shopDomain: config.shopDomain,\n eventType: \"bundle_impression\",\n bundleGid: event.bundleGid,\n bundleType: event.bundleType,\n productId: event.productId,\n occurredAt: new Date().toISOString(),\n });\n}\n\nexport async function reportAddToCart(\n config: AnalyticsConfig,\n event: {\n bundleGid: string;\n bundleType: string;\n /**\n * Product GID the widget is rendered on. Optional because multi-product\n * bundles (fixed, mix-match) have no single \"page product\" — the event\n * is already attributed to the bundle via `bundleGid`. Volume bundles\n * typically pass the anchor product's GID. Previously this was required\n * and consumers defaulted to `products[0]?.id ?? \"\"` for multi-product\n * bundles, which was misleading.\n */\n productId?: string;\n quantity: number;\n totalPrice: number;\n },\n): Promise<void> {\n await sendEvent(config, {\n shopDomain: config.shopDomain,\n eventType: \"bundle_add_to_cart\",\n ...event,\n occurredAt: new Date().toISOString(),\n });\n}\n\n/**\n * IntersectionObserver-based impression tracking.\n * Fires once when element is 50% visible, then unobserves.\n */\nexport function observeImpression(\n element: Element,\n callback: () => void,\n): () => void {\n if (typeof IntersectionObserver === \"undefined\") {\n // Fallback: fire immediately if IntersectionObserver not available\n callback();\n return () => {};\n }\n\n const observer = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (entry.isIntersecting) {\n callback();\n observer.unobserve(entry.target);\n }\n }\n },\n { threshold: 0.5 },\n );\n\n observer.observe(element);\n return () => observer.disconnect();\n}\n\n// --- Internal ---\n\nasync function sendEvent(\n config: AnalyticsConfig,\n payload: Record<string, unknown>,\n): Promise<void> {\n const url = `${config.appUrl}/api/analytics`;\n const body = JSON.stringify(payload);\n\n try {\n const response = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body,\n });\n\n if (!response.ok) {\n // Single retry after 2 seconds\n await new Promise((r) => setTimeout(r, 2000));\n await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body,\n });\n }\n } catch {\n // Fire-and-forget — never block UX\n try {\n // Use sendBeacon as last resort\n if (typeof navigator !== \"undefined\" && navigator.sendBeacon) {\n navigator.sendBeacon(url, body);\n }\n } catch {\n // Silently drop\n }\n }\n}\n","/**\n * CSS sanitization for merchant-authored custom CSS.\n *\n * Kept in lockstep with `app/lib/sanitize-css.ts`. When updating one,\n * update the other — the app-side sanitizer runs on merchant INPUT when\n * CSS is saved to the shop metafield, and this SDK-side sanitizer runs\n * on OUTPUT before the CSS is injected into a <style> tag on the\n * storefront. Both must enforce the same rules; a drift means either\n * the merchant's saved CSS fails to render on headless, or malicious\n * CSS sneaks through one side but not the other.\n *\n * 5-step pipeline:\n * 1. Strip comments (prevents keyword obfuscation like @im/**\\/port)\n * 2. Decode unicode escapes (prevents \\0040import → @import bypass)\n * 3. Strip HTML angle brackets (prevents </style><script> breakout)\n * 4. Reject blocked CSS features (@import, expression(), behavior, ...)\n * 5. Allowlist url() values (https://, relative, fragment only)\n */\n\nexport const MAX_CSS_LENGTH = 10_000;\n\nconst BLOCKED_PATTERNS: ReadonlyArray<[RegExp, string]> = [\n [/@import/i, \"@import rules\"],\n [/@charset/i, \"@charset declarations\"],\n [/expression\\s*\\(/i, \"CSS expressions\"],\n [/-moz-binding/i, \"-moz-binding\"],\n [/-webkit-binding/i, \"-webkit-binding\"],\n [/behavior\\s*:/i, \"behavior property\"],\n];\n\nconst SAFE_URL_VALUE = /^(https:|\\/[^/]|\\.\\/|\\.\\.\\/|#)/;\n\nexport type SanitizeResult =\n | { ok: true; css: string }\n | { ok: false; error: string };\n\nexport function sanitizeCustomCss(raw: string): SanitizeResult {\n if (raw.length > MAX_CSS_LENGTH) {\n return {\n ok: false,\n error: `CSS exceeds ${MAX_CSS_LENGTH.toLocaleString(\"en-US\")} character limit`,\n };\n }\n\n let css = raw.replace(/\\/\\*[\\s\\S]*?\\*\\//g, \"\");\n\n try {\n // 1. Decode hex unicode escapes: \\XXXXXX → codepoint. CSS lets authors\n // obfuscate keywords this way (e.g. `\\0040import` → `@import`).\n css = css.replace(/\\\\([0-9a-fA-F]{1,6})[ \\t\\n\\f]?/g, (_, hex) => {\n const codePoint = parseInt(hex, 16);\n if (codePoint < 0 || codePoint > 0x10ffff) return \"\\uFFFD\";\n return String.fromCodePoint(codePoint);\n });\n // 2. Fold backslash + newline (line continuation per CSS spec).\n css = css.replace(/\\\\\\r?\\n/g, \"\");\n // 3. Decode character escapes: \\c → c (backslash + non-hex non-newline).\n // Without this, `@\\i\\mport` would slip past our keyword blocklist.\n css = css.replace(/\\\\([^\\n\\r\\f0-9a-fA-F])/g, \"$1\");\n } catch {\n return {\n ok: false,\n error: \"CSS contains invalid unicode escape sequences\",\n };\n }\n\n css = css.replace(/</g, \"\").replace(/>/g, \"\");\n\n for (const [pattern, label] of BLOCKED_PATTERNS) {\n if (pattern.test(css)) {\n return { ok: false, error: `CSS contains blocked pattern: ${label}` };\n }\n }\n\n // url() extraction: match ANY opening paren → closing paren content,\n // regardless of whether quotes match. Previous regex used `\\1` backref\n // which allowed malformed inputs like `url('javascript:alert(1))` to\n // slip past (mismatched quote → no match → no validation). Now every\n // url(...) is extracted and validated.\n const urlPattern = /url\\s*\\(\\s*([\\s\\S]*?)\\s*\\)/gi;\n let urlMatch: RegExpExecArray | null;\n while ((urlMatch = urlPattern.exec(css)) !== null) {\n let urlValue = urlMatch[1].trim();\n // Strip a single matching or unmatched quote from either end.\n if (urlValue.startsWith(\"'\") || urlValue.startsWith('\"')) {\n urlValue = urlValue.slice(1);\n }\n if (urlValue.endsWith(\"'\") || urlValue.endsWith('\"')) {\n urlValue = urlValue.slice(0, -1);\n }\n urlValue = urlValue.trim();\n if (urlValue && !SAFE_URL_VALUE.test(urlValue)) {\n return {\n ok: false,\n error: \"CSS url() values must use https:// or relative paths\",\n };\n }\n }\n\n return { ok: true, css };\n}\n","/**\n * Merchant custom-CSS injection into the storefront DOM.\n *\n * The headless SDK fetches `$app:custom_css` from the shop metafield\n * alongside bundle data, runs the sanitizer, and injects the output as\n * a <style> tag scoped to a deterministic id. Dedup is by id — if a\n * merchant has two <lime-bundle> elements on the same page, only one\n * <style> tag appears.\n *\n * SSR-safe: no-op when `document` is undefined.\n */\nimport { sanitizeCustomCss } from \"./sanitize\";\n\nconst STYLE_ID_PREFIX = \"lb-custom-css-\";\n\n/**\n * Inject merchant CSS into the document head. Idempotent — calling\n * repeatedly with the same shop updates the existing <style> tag in\n * place rather than appending new ones.\n *\n * Returns true if CSS was injected (or updated), false if skipped\n * (SSR, empty CSS, or sanitization rejected the input).\n */\nexport function injectCustomCss(\n shopDomain: string,\n rawCss: string | null | undefined,\n): boolean {\n if (typeof document === \"undefined\") return false;\n if (!rawCss) return false;\n\n const sanitized = sanitizeCustomCss(rawCss);\n if (!sanitized.ok) return false;\n if (!sanitized.css.trim()) return false;\n\n const id = STYLE_ID_PREFIX + simpleHash(shopDomain);\n\n let style = document.getElementById(id) as HTMLStyleElement | null;\n if (!style) {\n style = document.createElement(\"style\");\n style.id = id;\n style.setAttribute(\"data-lime-bundles\", \"custom-css\");\n document.head.appendChild(style);\n }\n\n // Only touch textContent when it actually changed — avoids triggering\n // layout or style-recalc cycles on repeat invocations.\n if (style.textContent !== sanitized.css) {\n style.textContent = sanitized.css;\n }\n return true;\n}\n\n/** FNV-1a — short, fast, DOM-id-safe hash. Not cryptographic. */\nfunction simpleHash(input: string): string {\n let hash = 0x811c9dc5;\n for (let i = 0; i < input.length; i++) {\n hash ^= input.charCodeAt(i);\n hash = Math.imul(hash, 0x01000193);\n }\n return (hash >>> 0).toString(16);\n}\n","/**\n * Price formatting utilities.\n */\nimport type {\n Money,\n UnitPriceMeasurement,\n UnitPriceMeasurementUnit,\n} from \"../storefront-api/types\";\n\nexport function formatMoney(amount: string | number, currencyCode: string): string {\n const num = typeof amount === \"string\" ? parseFloat(amount) : amount;\n\n try {\n return new Intl.NumberFormat(undefined, {\n style: \"currency\",\n currency: currencyCode,\n }).format(num);\n } catch {\n return `${currencyCode} ${num.toFixed(2)}`;\n }\n}\n\nexport function calculateDiscount(\n price: number,\n discountType: \"percentage\" | \"fixed_amount\",\n discountValue: number,\n): number {\n if (discountType === \"percentage\") {\n return Math.max(0, price * (1 - discountValue / 100));\n }\n return Math.max(0, price - discountValue);\n}\n\nconst UNIT_LABEL: Record<UnitPriceMeasurementUnit, string> = {\n CL: \"cl\",\n CM: \"cm\",\n FLOZ: \"fl oz\",\n FT: \"ft\",\n FT2: \"ft²\",\n G: \"g\",\n GAL: \"gal\",\n IN: \"in\",\n ITEM: \"each\",\n KG: \"kg\",\n L: \"l\",\n LB: \"lb\",\n M: \"m\",\n M2: \"m²\",\n M3: \"m³\",\n MG: \"mg\",\n ML: \"ml\",\n MM: \"mm\",\n OZ: \"oz\",\n PT: \"pt\",\n QT: \"qt\",\n UNKNOWN: \"\",\n YD: \"yd\",\n};\n\n/**\n * Format a Storefront `unitPrice` + `unitPriceMeasurement` pair as a\n * compact display string (\"$0.50/100ml\", \"$12.99/kg\", \"$2.00/each\").\n *\n * Returns `null` when either input is absent — the merchant hasn't\n * configured unit pricing in the Shopify admin and no UI should render.\n *\n * The reference value is omitted when it equals 1 (\"$X.XX/kg\" rather\n * than \"$X.XX/1kg\") to match the format Shopify's `money` filter +\n * `unit_price_with_measurement` produce in Liquid themes.\n */\nexport function formatUnitPrice(\n unitPrice: Money | null | undefined,\n measurement: UnitPriceMeasurement | null | undefined,\n fallbackCurrency?: string,\n): string | null {\n if (!unitPrice || !measurement || measurement.referenceUnit === \"UNKNOWN\") {\n return null;\n }\n\n // Guard against malformed Money.amount strings — Storefront responses\n // are always valid decimals, but a non-finite number would silently\n // render \"$NaN\" via Intl.NumberFormat.format(NaN).\n const amount = parseFloat(unitPrice.amount);\n if (!Number.isFinite(amount)) return null;\n\n // `?? \"\"` + `if (!label)` is defence against Shopify adding enum values\n // we don't yet know about — the type-level lookup would still succeed\n // but the runtime value would be undefined.\n const label = UNIT_LABEL[measurement.referenceUnit] ?? \"\";\n if (!label) return null;\n\n const currency = unitPrice.currencyCode || fallbackCurrency || \"USD\";\n const formatted = formatMoney(amount, currency);\n const value = measurement.referenceValue;\n const measurementText = value === 1 ? label : `${value}${label}`;\n return `${formatted}/${measurementText}`;\n}\n","// Integer-cent arithmetic that matches Shopify's Discount Function\n// per-unit floor rounding — naive `total * (1 - discount)` drifts a\n// cent or two from what the customer actually pays.\nimport type { FixedBundleData, DiscountConfig } from \"../bundle/types\";\n\nexport interface PricingRow {\n /** Pre-discount unit price in cents. */\n unitCents: number;\n /** Quantity of this product in the bundle. */\n qty: number;\n /** Pre-discount line subtotal in cents (`unitCents * qty`). */\n lineCents: number;\n /** Variant's compare-at price in cents, if different from unit. */\n compareCents: number | null;\n}\n\nexport interface FixedBundlePricing {\n rows: PricingRow[];\n /** Pre-discount bundle total in cents. */\n totalCents: number;\n /** Post-discount bundle total in cents. */\n saleCents: number;\n /** Savings in cents — always `max(0, totalCents - saleCents)`. */\n savingsCents: number;\n currency: string;\n}\n\n/**\n * Parse a money amount (Storefront API returns major-unit strings like\n * \"749.95\") into integer cents. Safe on numbers, null, undefined.\n * Rounds to the nearest cent to absorb float-representation drift.\n */\nexport function parseCents(\n amount: string | number | null | undefined,\n): number {\n if (amount === null || amount === undefined) return 0;\n const num = typeof amount === \"string\" ? parseFloat(amount) : amount;\n if (!Number.isFinite(num)) return 0;\n return Math.round(num * 100);\n}\n\n/**\n * Format integer cents as localized currency. Falls back to\n * `CUR 12.34` when the runtime's Intl.NumberFormat rejects the code.\n */\nexport function formatCents(cents: number, currencyCode: string): string {\n try {\n return new Intl.NumberFormat(undefined, {\n style: \"currency\",\n currency: currencyCode,\n }).format(cents / 100);\n } catch {\n return `${currencyCode} ${(cents / 100).toFixed(2)}`;\n }\n}\n\n/**\n * Apply a percentage discount to a single unit with floor rounding —\n * matches the rounding Shopify's Discount Function performs at checkout.\n * For a bundle total, apply this per-unit and sum, rather than computing\n * a single discount on the totalled line — those paths can differ by a\n * cent or two when the per-unit price has odd cents.\n */\nexport function percentageDiscountUnit(\n unitCents: number,\n percent: number,\n): number {\n return Math.max(0, unitCents - Math.floor((unitCents * percent) / 100));\n}\n\n/**\n * Compute the full pricing snapshot for a fixed bundle. Handles\n * percentage and fixed-amount discount types and per-product quantities.\n *\n * `productQuantities` defaults to `bundle.productQuantities` (the\n * merchant-configured map); product IDs missing from that map fall back\n * to qty 1 via the `?? 1` in the row loop.\n *\n * The savings percentage that used to render as a colored header badge was\n * retired in the manifest-and-receipt redesign — savings are now stated once\n * on the receipt summary line (derive it from `savingsCents` / `totalCents`).\n */\nexport function computeFixedPricing(\n bundle: FixedBundleData,\n productQuantities: Record<string, number> = bundle.productQuantities,\n): FixedBundlePricing {\n const { discountType, discountValue } = bundle.discountConfig;\n const rows: PricingRow[] = [];\n let totalCents = 0;\n let saleCents = 0;\n\n for (const product of bundle.products) {\n const variant =\n product.variants.nodes.find((v) => v.availableForSale) ??\n product.variants.nodes[0];\n if (!variant) continue;\n\n const unitCents = parseCents(variant.price.amount);\n const qty = productQuantities[product.id] ?? 1;\n const lineCents = unitCents * qty;\n const compareCents = variant.compareAtPrice\n ? parseCents(variant.compareAtPrice.amount)\n : null;\n\n rows.push({ unitCents, qty, lineCents, compareCents });\n totalCents += lineCents;\n\n if (discountType === \"percentage\") {\n const perUnit = percentageDiscountUnit(unitCents, discountValue);\n saleCents += perUnit * qty;\n } else {\n saleCents += lineCents;\n }\n }\n\n if (discountType === \"fixed_amount\") {\n saleCents = Math.max(0, totalCents - Math.round(discountValue * 100));\n }\n\n const savingsCents = Math.max(0, totalCents - saleCents);\n const currency =\n bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? \"USD\";\n\n return { rows, totalCents, saleCents, savingsCents, currency };\n}\n\n/**\n * Pure helper: compute the sale-price total for an arbitrary set of\n * cart lines under a discount config. Useful for mix-match bundles\n * where the lines are user-selected rather than bundle-configured.\n * Applies percentage discount on the aggregated total (mix-match uses\n * `applies_to_each_item: false`), matching the Liquid block's behaviour.\n */\nexport function computeBundleSaleCents(\n totalCents: number,\n discount: DiscountConfig,\n): number {\n if (discount.discountType === \"percentage\") {\n const off = Math.floor((totalCents * discount.discountValue) / 100);\n return Math.max(0, totalCents - off);\n }\n return Math.max(0, totalCents - Math.round(discount.discountValue * 100));\n}\n","/**\n * Append Shopify CDN image-transform params to a product-image URL so the\n * browser downloads a correctly-sized asset. Mirrors what Liquid's\n * `image_url: width:..., height:..., crop:...` filter chain produces.\n * Non-Shopify CDNs silently ignore the extra params — graceful fallback.\n */\nexport interface ImageTransform {\n width?: number;\n height?: number;\n crop?: \"center\" | \"top\" | \"bottom\" | \"left\" | \"right\";\n}\n\n/**\n * Standard thumbnail size for bundle product rows and picker tiles.\n * Matches the Liquid theme's `image_url: width: 152, height: 152,\n * crop: 'center'`. CSS caps display size around 76px; 152 gives\n * retina-quality rendering on 2x displays.\n */\nexport const THUMB_PX = 152;\n\nexport function transformImageUrl(\n url: string | null | undefined,\n t: ImageTransform = {},\n): string {\n if (!url) return \"\";\n try {\n const u = new URL(url);\n if (t.width !== undefined) u.searchParams.set(\"width\", String(t.width));\n if (t.height !== undefined) u.searchParams.set(\"height\", String(t.height));\n if (t.crop) u.searchParams.set(\"crop\", t.crop);\n return u.toString();\n } catch {\n return url;\n }\n}\n\n/**\n * Responsive `srcset` widths. The smallest variant is ~2× the element's CSS\n * display size — never 1×. A 1× variant is a trap: when the browser resolves\n * device-pixel-ratio to 1 (Chrome device mode, some embedded contexts) it\n * picks that 1× image, which is upscaled and soft on a Retina screen. Starting\n * at 2× guarantees ≥2× density everywhere. Shopify's CDN caps each request at\n * the master's resolution, so the larger variants never upscale.\n *\n * - `THUMB_WIDTHS` — product thumbnails (fixed rows ~48px, mix & match filled\n * slots ~60px). Min 120 = 2× the ~60px slot.\n * - `PICKER_WIDTHS` — the mix & match picker modal (~228px card). Min 480 ≈ 2×\n * the card; up to 1024 (master) for high-DPR / larger displays.\n */\nexport const THUMB_WIDTHS = [120, 180, 240];\nexport const PICKER_WIDTHS = [480, 600, 768, 1024];\n\n/** Display widths for the `sizes` attribute. The modal scales on mobile. */\nexport const THUMB_SIZES = \"60px\";\nexport const PICKER_SIZES = \"(max-width: 767px) 45vw, 228px\";\n\n/** Build a Shopify CDN `srcset` string from a base image URL and widths. */\nexport function imageSrcset(url: string, widths: number[]): string {\n return widths\n .map((w) => `${transformImageUrl(url, { width: w })} ${w}w`)\n .join(\", \");\n}\n","/**\n * Pure countdown formatter — returns the `Nd HHh MMm SSs` / `HHh MMm SSs`\n * string given a millisecond remaining. SSR-safe, no DOM, no timers.\n *\n * A developer building their own widget wires their own `setInterval`\n * + React state and calls this to format the label. The web component's\n * `renderCountdown` uses this internally.\n */\nexport function formatCountdown(msRemaining: number): string {\n if (!Number.isFinite(msRemaining) || msRemaining <= 0) return \"\";\n const totalSeconds = Math.floor(msRemaining / 1000);\n const days = Math.floor(totalSeconds / 86400);\n const hours = Math.floor((totalSeconds % 86400) / 3600);\n const minutes = Math.floor((totalSeconds % 3600) / 60);\n const seconds = totalSeconds % 60;\n const pad = (n: number) => String(n).padStart(2, \"0\");\n if (days > 0) {\n return `${days}d ${pad(hours)}h ${pad(minutes)}m ${pad(seconds)}s`;\n }\n return `${pad(hours)}h ${pad(minutes)}m ${pad(seconds)}s`;\n}\n","// Public surface — only the algorithm and its types. Layout-tuning\n// constants (item height, list padding, max visible items) live inline\n// in each consumer so the SDK contract isn't locked into specific\n// numeric values that might change.\nexport {\n computePosition,\n type DropdownPlacement,\n type ComputePositionArgs,\n type ComputePositionResult,\n} from \"./compute-position\";\n\nexport {\n handleKey,\n firstEnabled,\n type DropdownKeyEvent,\n type DropdownKeyState,\n type DropdownAction,\n} from \"./keyboard\";\n\nexport {\n pushTypeAheadChar,\n emptyTypeAheadState,\n type TypeAheadState,\n type TypeAheadOption,\n type PushCharResult,\n} from \"./type-ahead\";\n","/**\n * Pure positioning helper for an accessible dropdown popover.\n *\n * Given a trigger rect and the viewport height, decide whether the panel\n * should open above or below the trigger and how tall it can be. Used by\n * the Liquid theme dropdown, the `<lime-bundle>` web component, and the\n * React `<VariantDropdown>` so all three surfaces agree.\n *\n * The function is intentionally framework-agnostic — caller passes plain\n * numbers (typically from `getBoundingClientRect()` and `window.innerHeight`)\n * and applies the returned `position: fixed` coordinates itself.\n */\n\nexport const DEFAULT_TRIGGER_MARGIN = 4;\n\nexport type DropdownPlacement = \"up\" | \"down\";\n\nexport interface ComputePositionArgs {\n trigger: { top: number; bottom: number; left: number; width: number };\n viewportHeight: number;\n desiredHeight: number;\n margin?: number;\n /**\n * Optional viewport-coordinate bounds of the nearest clipping ancestor\n * (e.g. a `overflow:auto` product list). When provided, placement is\n * decided against these bounds instead of the full viewport so a panel\n * inside a short scroll container flips upward before it would clip.\n *\n * Bounds are intersected with the viewport (`[0, viewportHeight]`) so a\n * caller that passes the raw `getBoundingClientRect()` of an ancestor\n * still gets a sane result when the ancestor itself extends off-screen.\n */\n clip?: { top: number; bottom: number };\n}\n\nexport interface ComputePositionResult {\n placement: DropdownPlacement;\n maxHeight: number;\n offsetTop: number;\n offsetLeft: number;\n width: number;\n}\n\nexport function computePosition({\n trigger,\n viewportHeight,\n desiredHeight,\n margin = DEFAULT_TRIGGER_MARGIN,\n clip,\n}: ComputePositionArgs): ComputePositionResult {\n const upperBound = clip\n ? Math.min(viewportHeight, clip.bottom)\n : viewportHeight;\n const lowerBound = clip ? Math.max(0, clip.top) : 0;\n const spaceBelow = Math.max(0, upperBound - trigger.bottom - margin);\n const spaceAbove = Math.max(0, trigger.top - lowerBound - margin);\n\n // Prefer downward unless the panel would clip AND upward has more room.\n // Matches native <select> behaviour on most platforms.\n const placement: DropdownPlacement =\n desiredHeight <= spaceBelow\n ? \"down\"\n : spaceAbove > spaceBelow\n ? \"up\"\n : \"down\";\n\n const available = placement === \"down\" ? spaceBelow : spaceAbove;\n // Cap maxHeight at the actual available space so the panel never spills\n // outside the viewport. When `available` is less than the desired height\n // the listbox shrinks and its `overflow-y: auto` scrolls — better than\n // overflowing the viewport with a fixed-minimum tall panel.\n const maxHeight = Math.min(desiredHeight, available);\n\n const offsetTop =\n placement === \"down\"\n ? trigger.bottom + margin\n : trigger.top - margin - maxHeight;\n\n return {\n placement,\n maxHeight,\n offsetTop,\n offsetLeft: trigger.left,\n width: trigger.width,\n };\n}\n","/**\n * Keyboard event → action discriminator for an accessible dropdown.\n *\n * The DOM glue layer (Liquid asset, web component, React component) calls\n * `handleKey(event, state)` with the event details and current state, then\n * applies the returned action. This keeps WAI-ARIA combobox semantics in a\n * single, unit-testable place.\n *\n * Pattern follows the WAI-ARIA Authoring Practices \"Combobox with Listbox\n * Popup, manual selection\" pattern.\n */\n\nexport interface DropdownKeyEvent {\n key: string;\n ctrlKey?: boolean;\n metaKey?: boolean;\n altKey?: boolean;\n shiftKey?: boolean;\n}\n\nexport interface DropdownKeyState {\n isOpen: boolean;\n activeIndex: number;\n selectedIndex: number;\n options: ReadonlyArray<{ disabled: boolean }>;\n}\n\nexport type DropdownAction =\n | {\n type: \"open\";\n activeIndex: number;\n preventDefault: true;\n }\n | {\n type: \"close\";\n commit: false;\n restoreFocus: boolean;\n preventDefault: boolean;\n }\n | {\n type: \"move-active\";\n activeIndex: number;\n preventDefault: true;\n }\n | {\n type: \"commit\";\n index: number;\n preventDefault: true;\n }\n | {\n type: \"type-ahead\";\n char: string;\n preventDefault: false;\n }\n | {\n type: \"passthrough\";\n preventDefault: boolean;\n };\n\nconst PASSTHROUGH: DropdownAction = {\n type: \"passthrough\",\n preventDefault: false,\n};\n\n/**\n * Index of the first selectable option, or -1 when every option is\n * disabled. Exported because consumers need the same \"where does focus\n * land\" answer when opening a listbox with no valid selection.\n */\nexport function firstEnabled(\n options: ReadonlyArray<{ disabled: boolean }>,\n): number {\n for (let i = 0; i < options.length; i++) {\n if (!options[i].disabled) return i;\n }\n return -1;\n}\n\nfunction lastEnabled(options: ReadonlyArray<{ disabled: boolean }>): number {\n for (let i = options.length - 1; i >= 0; i--) {\n if (!options[i].disabled) return i;\n }\n return -1;\n}\n\nfunction nextEnabled(\n options: ReadonlyArray<{ disabled: boolean }>,\n from: number,\n): number {\n if (options.length === 0) return -1;\n for (let step = 1; step <= options.length; step++) {\n const idx = (from + step) % options.length;\n if (!options[idx].disabled) return idx;\n }\n return from; // all disabled — stay put\n}\n\nfunction prevEnabled(\n options: ReadonlyArray<{ disabled: boolean }>,\n from: number,\n): number {\n if (options.length === 0) return -1;\n for (let step = 1; step <= options.length; step++) {\n const idx = (from - step + options.length) % options.length;\n if (!options[idx].disabled) return idx;\n }\n return from;\n}\n\nfunction isPrintable(key: string): boolean {\n return key.length === 1 && key !== \" \" && /\\S/.test(key);\n}\n\nexport function handleKey(\n event: DropdownKeyEvent,\n state: DropdownKeyState,\n): DropdownAction {\n // Ignore modifier-key combos (browser shortcuts, etc).\n if (event.ctrlKey || event.metaKey || event.altKey) return PASSTHROUGH;\n\n const { key } = event;\n const { isOpen, activeIndex, selectedIndex, options } = state;\n\n // Closed → key opens (or noop)\n if (!isOpen) {\n switch (key) {\n case \"Enter\":\n case \" \":\n case \"ArrowDown\":\n return {\n type: \"open\",\n activeIndex:\n selectedIndex >= 0 && !options[selectedIndex]?.disabled\n ? selectedIndex\n : firstEnabled(options),\n preventDefault: true,\n };\n case \"ArrowUp\":\n return {\n type: \"open\",\n activeIndex:\n selectedIndex >= 0 && !options[selectedIndex]?.disabled\n ? selectedIndex\n : lastEnabled(options),\n preventDefault: true,\n };\n default:\n if (isPrintable(key)) {\n return {\n type: \"open\",\n activeIndex:\n selectedIndex >= 0 && !options[selectedIndex]?.disabled\n ? selectedIndex\n : firstEnabled(options),\n preventDefault: true,\n };\n }\n return PASSTHROUGH;\n }\n }\n\n // Open → navigation, commit, close\n switch (key) {\n case \"ArrowDown\":\n return {\n type: \"move-active\",\n activeIndex: nextEnabled(options, activeIndex),\n preventDefault: true,\n };\n case \"ArrowUp\":\n return {\n type: \"move-active\",\n activeIndex: prevEnabled(options, activeIndex),\n preventDefault: true,\n };\n case \"Home\":\n return {\n type: \"move-active\",\n activeIndex: firstEnabled(options),\n preventDefault: true,\n };\n case \"End\":\n return {\n type: \"move-active\",\n activeIndex: lastEnabled(options),\n preventDefault: true,\n };\n case \"Enter\":\n case \" \":\n if (activeIndex >= 0 && !options[activeIndex]?.disabled) {\n return {\n type: \"commit\",\n index: activeIndex,\n preventDefault: true,\n };\n }\n // Enter while open with no committable target — swallow it. The\n // dropdown lives inside a Shopify product-page <form>, so a\n // bubbling Enter would submit the page form. WAI-ARIA combobox\n // pattern says the popup should absorb Enter regardless.\n return { type: \"passthrough\", preventDefault: true };\n case \"Escape\":\n return {\n type: \"close\",\n commit: false,\n restoreFocus: true,\n preventDefault: true,\n };\n case \"Tab\":\n // Don't preventDefault — let the browser advance focus naturally.\n // Don't restoreFocus either: stealing focus back to the trigger\n // would interfere with the browser's tab-advance to the next\n // focusable element.\n return {\n type: \"close\",\n commit: false,\n restoreFocus: false,\n preventDefault: false,\n };\n default:\n if (isPrintable(key)) {\n return { type: \"type-ahead\", char: key, preventDefault: false };\n }\n return PASSTHROUGH;\n }\n}\n","/**\n * Type-ahead matching for dropdown listboxes.\n *\n * The caller drives `now` (typically `Date.now()`) so the function stays\n * pure and deterministic in tests. After `RESET_MS` of inactivity the\n * buffer clears; otherwise additional chars accumulate so the user can\n * type \"me\" to jump from \"Mango\" to \"Medium\" without overshoot.\n */\n\nexport const TYPE_AHEAD_RESET_MS = 500;\n\nexport interface TypeAheadState {\n buffer: string;\n lastTime: number;\n}\n\nexport interface TypeAheadOption {\n disabled: boolean;\n label: string;\n}\n\nexport function emptyTypeAheadState(): TypeAheadState {\n return { buffer: \"\", lastTime: 0 };\n}\n\nexport interface PushCharResult {\n newState: TypeAheadState;\n matchedIndex: number | null;\n}\n\nexport function pushTypeAheadChar(\n state: TypeAheadState,\n char: string,\n now: number,\n options: ReadonlyArray<TypeAheadOption>,\n resetMs: number = TYPE_AHEAD_RESET_MS,\n): PushCharResult {\n if (char.length !== 1) {\n return { newState: state, matchedIndex: null };\n }\n\n const expired = now - state.lastTime > resetMs;\n const buffer = (expired ? \"\" : state.buffer) + char.toLowerCase();\n const newState: TypeAheadState = { buffer, lastTime: now };\n\n for (let i = 0; i < options.length; i++) {\n const opt = options[i];\n if (opt.disabled) continue;\n if (opt.label.toLowerCase().startsWith(buffer)) {\n return { newState, matchedIndex: i };\n }\n }\n\n return { newState, matchedIndex: null };\n}\n","/**\n * Variant resolution + availability cascade for per-option pickers.\n *\n * Mirrors the algorithms in `extensions/bundle-theme/assets/bundle-fixed.js`\n * (`findVariantByOptions`, `isOptionValueAvailable`) so the React component\n * and web component renderer share the same source of truth as the Liquid\n * widget. Liquid keeps its inlined copy because theme assets cannot import\n * npm — a CI parity guard prevents drift.\n *\n * Inputs are normalized to positional option-value arrays. Storefront API\n * consumers can derive this with `v.selectedOptions.map(o => o.value)`.\n */\n\nimport { isFulfillable } from \"../inventory/predicate\";\n\nexport interface PickerVariant {\n id: string;\n /** Positional option values: index i matches the product's option-position i+1. */\n optionValues: string[];\n /** True when the variant has stock and is purchasable. */\n available: boolean;\n}\n\n/**\n * Returns the variant whose positional options exactly match the selected\n * values, or `null` if none. Caller decides what to do on miss (typically\n * fall back to the first available variant).\n */\nexport function findVariantByOptions<V extends PickerVariant>(\n variants: ReadonlyArray<V>,\n optionValues: ReadonlyArray<string>,\n): V | null {\n if (variants.length === 0) return null;\n for (const v of variants) {\n if (v.optionValues.length !== optionValues.length) continue;\n let match = true;\n for (let j = 0; j < v.optionValues.length; j++) {\n if (v.optionValues[j] !== optionValues[j]) {\n match = false;\n break;\n }\n }\n if (match) return v;\n }\n return null;\n}\n\n/**\n * True iff some in-stock variant pairs `value` at `optionIndex` with the\n * currently selected values at every other option index. Drives the\n * disabled state of options in cascading pickers (e.g. selecting Color=Red\n * may disable Size=XL if no Red XL variant exists or it's sold out).\n */\nexport function isOptionValueAvailable(\n variants: ReadonlyArray<PickerVariant>,\n optionIndex: number,\n value: string,\n selected: ReadonlyArray<string>,\n): boolean {\n for (const v of variants) {\n if (!v.available) continue;\n if (v.optionValues[optionIndex] !== value) continue;\n let ok = true;\n for (let j = 0; j < v.optionValues.length; j++) {\n if (j === optionIndex) continue;\n if (v.optionValues[j] !== selected[j]) {\n ok = false;\n break;\n }\n }\n if (ok) return true;\n }\n return false;\n}\n\n/**\n * Convenience helper for Storefront API consumers. Converts a Storefront\n * `ProductVariant` (with `selectedOptions: [{name, value}]`) into the\n * normalized `PickerVariant` shape.\n *\n * Pass `requiredQty` to make availability quantity-aware: a variant with 2\n * units in stock cannot cover a 6-unit bundle slot, and offering it in the\n * dropdown walks the shopper into a cart Shopify caps at add time. Without\n * it, availability falls back to the plain `availableForSale` boolean.\n */\nexport function toPickerVariant<\n T extends {\n id: string;\n selectedOptions: ReadonlyArray<{ value: string }>;\n availableForSale: boolean;\n currentlyNotInStock?: boolean;\n quantityAvailable?: number | null;\n },\n>(variant: T, requiredQty?: number): PickerVariant {\n return {\n id: variant.id,\n optionValues: variant.selectedOptions.map((o) => o.value),\n available:\n requiredQty !== undefined\n ? isFulfillable(variant, requiredQty)\n : variant.availableForSale,\n };\n}\n","/**\n * Storefront-side fulfillability predicate.\n *\n * Mirrors `app/lib/inventory.server.ts:isFulfillable` but reads from a\n * Storefront-shaped variant (no Admin types, no Node imports). Same\n * semantics: a tracked + DENY variant with insufficient stock fails;\n * everything else succeeds.\n *\n * Used by the widget renderers and React components to gate add-to-cart\n * eligibility, badge rendering, and stepper caps. Lives in `core` so the\n * Liquid widget (via `@lime-bundles/widget`) and React consumers (via\n * `@lime-bundles/react`) draw from one source of truth.\n *\n * Storefront API limitations:\n * - `quantityAvailable` is `null` when the merchant has not granted\n * `unauthenticated_read_product_inventory` to the Storefront token.\n * In that case, treat the variant as fulfillable (boolean fallback)\n * rather than blocking the cart on a permission gap.\n * - `quantityAvailable` is `0` (not null) for variants whose inventory\n * is not tracked. Untracked is distinguishable from a genuine zero:\n * a tracked DENY variant at zero is not `availableForSale`, and a\n * tracked CONTINUE variant at zero is flagged `currentlyNotInStock`.\n * Purchasable + not backordered + zero-or-negative quantity therefore\n * means \"untracked — stock unknown, defer to `availableForSale`\",\n * which is exactly how Liquid's `variant.available` treats it.\n * - `currentlyNotInStock` is true for backordered variants (zero\n * stock + `inventoryPolicy = CONTINUE`). Fulfillable for cart\n * purposes — Shopify's checkout still accepts them.\n */\n\n/**\n * Minimal variant shape the predicate needs. Wider Storefront variants\n * (with title, price, etc.) are accepted via structural typing.\n */\nexport interface StorefrontVariantStock {\n availableForSale: boolean;\n /** Optional — Storefront API field, may be undefined on hand-built mocks. */\n currentlyNotInStock?: boolean;\n /** Null when the Storefront token lacks the inventory scope, or when\n * the variant is untracked. Treat as \"unknown stock — defer to\n * availableForSale\". */\n quantityAvailable?: number | null;\n}\n\n/**\n * Purchasable, not backordered, yet reporting zero-or-negative stock:\n * that combination only exists for variants whose inventory is not\n * tracked (tracked DENY at zero loses `availableForSale`; tracked\n * CONTINUE at zero gains `currentlyNotInStock`). Stock is unknown for\n * these, so every stock-based cap must stand down and defer to\n * `availableForSale` — the same answer Liquid's `variant.available`\n * gives for them.\n */\nfunction isUntrackedStock(variant: StorefrontVariantStock): boolean {\n return (\n variant.availableForSale &&\n !variant.currentlyNotInStock &&\n variant.quantityAvailable != null &&\n variant.quantityAvailable <= 0\n );\n}\n\n/**\n * True when a customer can add `requiredQty` units of this variant to\n * their cart and pass Shopify's checkout-time stock check.\n *\n * - `availableForSale = false` → never fulfillable (Shopify will reject).\n * - `currentlyNotInStock = true` → backordered (zero stock + CONTINUE\n * inventory policy). Shopify will accept the cart line for any qty,\n * so treat as fulfillable. Mirrors the Admin-side predicate's\n * CONTINUE branch in [app/lib/inventory.server.ts].\n * - `quantityAvailable` is `null` / `undefined` → defer to the boolean\n * (the merchant's Storefront token may be missing\n * `unauthenticated_read_product_inventory`).\n * - `quantityAvailable <= 0` while still purchasable → untracked\n * inventory (the API reports `0`, not `null`, for untracked variants);\n * fulfillable at any quantity.\n * - Otherwise → require `quantityAvailable >= requiredQty`.\n *\n * Known, accepted asymmetry with the Admin-side predicate: a CONTINUE-policy\n * variant with SOME stock but less than `requiredQty` reads as fulfillable\n * on the admin (Shopify will accept the order and backorder the remainder)\n * but unavailable here — the Storefront API only reveals the policy via\n * `currentlyNotInStock`, which is false while any stock remains, and\n * treating every partial-stock variant as fulfillable would wave through\n * DENY-policy variants whose add Shopify then caps. Blocking the rare\n * backorderable partial is the safe direction; the merchant-visible effect\n * is underselling, not a broken cart. See docs/out-of-stock-lifecycle.md.\n */\nexport function isFulfillable(\n variant: StorefrontVariantStock,\n requiredQty: number,\n): boolean {\n if (!variant.availableForSale) return false;\n if (variant.currentlyNotInStock) return true;\n if (variant.quantityAvailable == null) return true;\n if (isUntrackedStock(variant)) return true;\n return variant.quantityAvailable >= requiredQty;\n}\n\n/**\n * The stock number that should cap UI steppers and cross-slot guards for\n * this variant, or `null` when stock never caps: backorder-allowed\n * (`currentlyNotInStock`), untracked inventory, or unknown because the\n * shop's token lacks `unauthenticated_read_product_inventory`. This is the\n * exact contract of the picker layer's `inventoryQuantity` field — the\n * theme host computes the same value in Liquid.\n */\nexport function stockCapForVariant(\n variant: StorefrontVariantStock,\n): number | null {\n if (variant.currentlyNotInStock) return null;\n if (variant.quantityAvailable == null) return null;\n if (isUntrackedStock(variant)) return null;\n return Math.max(0, variant.quantityAvailable);\n}\n\n/**\n * Whether the \"Only X left\" low-stock badge should render for this\n * variant. False when:\n * - The merchant disabled the badge (`enabled = false`).\n * - We don't know the quantity (`quantityAvailable` is null/undefined).\n * - The variant is below `requiredQty` (the renderer treats this as\n * out-of-stock instead — no double signaling).\n * - The variant has more stock than the threshold (no badge needed).\n */\nexport function shouldShowLowStockBadge(\n variant: StorefrontVariantStock,\n requiredQty: number,\n threshold: number,\n enabled: boolean,\n): boolean {\n if (!enabled) return false;\n if (variant.quantityAvailable == null) return false;\n if (variant.quantityAvailable < requiredQty) return false;\n return variant.quantityAvailable <= threshold;\n}\n\n/**\n * Maximum number of bundles the customer can request given a variant's\n * stock and the per-bundle required quantity. Used to cap quantity\n * steppers — without a cap, a customer can repeatedly press \"+\" until\n * the cart-add request fails at Shopify's checkout-time stock check.\n *\n * Returns `Infinity` when stock is unknown or unbounded (the boolean\n * fallback path) — callers should guard with the existing UI ceiling\n * (e.g. `Math.min(maxBundlesPerOrder, maxBundlesByStock)`).\n */\nexport function maxBundlesByStock(\n variant: StorefrontVariantStock,\n requiredQty: number,\n): number {\n if (variant.quantityAvailable == null) return Number.POSITIVE_INFINITY;\n if (isUntrackedStock(variant)) return Number.POSITIVE_INFINITY;\n if (requiredQty <= 0) return Number.POSITIVE_INFINITY;\n return Math.floor(variant.quantityAvailable / requiredQty);\n}\n\n/**\n * Largest qty the shopper may add to a mix-and-match bundle pick for a given\n * variant, after subtracting any units they've already allocated to this\n * bundle. Returns 0 when the variant isn't fulfillable.\n *\n * Bounded by:\n * - the merchant's per-product `productMax` (rule.max)\n * - the variant's `quantityAvailable` (when known)\n * Backordered variants (`currentlyNotInStock` with policy CONTINUE) bypass\n * the stock cap and rely on `productMax`.\n */\nexport function maxAddableQuantity(\n variant: StorefrontVariantStock,\n productMax: number,\n alreadyInBundle: number,\n): number {\n if (!variant.availableForSale) return 0;\n // Backorder: stock cap doesn't apply.\n if (variant.currentlyNotInStock) {\n return Math.max(0, productMax - alreadyInBundle);\n }\n // Untracked (purchasable but reporting <= 0): stock is unknown, so only\n // the merchant's productMax binds.\n const stockCap =\n variant.quantityAvailable == null || isUntrackedStock(variant)\n ? Number.POSITIVE_INFINITY\n : variant.quantityAvailable;\n return Math.max(0, Math.min(productMax, stockCap) - alreadyInBundle);\n}\n","/**\n * A/B test assignment: given a visitor and a test, which side do they see?\n *\n * Deterministic and stateless. The choice is recomputed from `hash(visitorId +\n * testId)` on every page rather than being decided once and stored, which is\n * why one opaque visitor id covers every test the shop will ever run, changing\n * a split needs no stored-state migration, and there is no per-test cookie to\n * set, read or expire.\n *\n * ## Privacy\n *\n * One random first-party id in `localStorage`, no PII, nothing cross-site, and\n * no consent module. The removed A/B feature shipped `setConsent`/`hasConsent`\n * for a design that needed considerably more than this.\n *\n * ## What happens without storage\n *\n * Blocked or full storage degrades to a per-page id, and that is deliberately\n * survivable rather than fatal. Impression and add-to-cart both happen on the\n * product page, so the funnel stays internally consistent within a page view,\n * and purchase attribution rides the `_lime_bundle_gid` line property into the\n * order. What is lost is only the *experience* staying stable across visits,\n * not the correctness of the numbers.\n *\n * ES2017 target — this package is bundled into both hosts, and esbuild\n * downlevels syntax but does not polyfill runtime APIs.\n */\n\n/** localStorage key holding the opaque per-visitor id. */\nexport const VISITOR_ID_KEY = \"_lb_v\";\n\n/** Module-scope fallback used when localStorage is unavailable. */\nlet inMemoryVisitorId: string | null = null;\n\n/**\n * 32-bit FNV-1a. Small, dependency-free, and well distributed for short\n * strings, which is all that is being asked of it. Not a security primitive\n * and does not need to be one: knowing your own bucket lets a shopper see the\n * other offer, which they could do by clearing storage anyway.\n */\nexport function hashString(input: string): number {\n let hash = 0x811c9dc5;\n for (let i = 0; i < input.length; i++) {\n hash ^= input.charCodeAt(i);\n // Multiply by the FNV prime (16777619) using shifts, staying inside 32-bit\n // integer math the whole way.\n hash +=\n (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);\n hash >>>= 0;\n }\n return hash >>> 0;\n}\n\n/**\n * Read the visitor id, creating and persisting one on first sight.\n *\n * Never throws: Safari private mode, disabled storage and quota-exceeded all\n * fall back to a value that lives for the page.\n */\nexport function getVisitorId(): string {\n try {\n const stored = window.localStorage.getItem(VISITOR_ID_KEY);\n if (stored) return stored;\n const fresh = randomId();\n window.localStorage.setItem(VISITOR_ID_KEY, fresh);\n return fresh;\n } catch (_err) {\n if (!inMemoryVisitorId) inMemoryVisitorId = randomId();\n return inMemoryVisitorId;\n }\n}\n\nfunction randomId(): string {\n // Not crypto-grade on purpose: this is a bucketing key, not a token, and\n // crypto.randomUUID is unavailable on older storefront browsers.\n return (\n Math.random().toString(36).slice(2, 10) +\n Math.random().toString(36).slice(2, 10)\n );\n}\n\n/** The bits of a bundle this module needs. Structural so both hosts fit. */\nexport interface AbAssignable {\n /** Stable identity, used to order the sides deterministically. */\n id: string;\n abTestId: string | null;\n abWeight: number;\n}\n\n/**\n * Roll once per visitor per test, then walk the sides accumulating their\n * weights until the roll lands inside one. A 50/50 pair splits the 0–99 space\n * into 0–49 and 50–99; a 90/10 pair into 0–89 and 90–99.\n *\n * The obvious-looking alternative — asking each side in turn \"is this visitor\n * in your bucket?\" — is broken, and was the original bug here. That predicate\n * depends only on the visitor and the test, so it answers the same for every\n * side: a low roll always matched the first side, and a high roll matched\n * nobody and fell back to the first side. **One side could never be shown.**\n * Weights are shares of a single roll, not independent tests, and the code has\n * to say so.\n *\n * Sides are sorted by `id` first so the choice does not depend on the order\n * they arrived in. DOM order in the theme comes from Shopify's metafield\n * reference list and fetch order in the SDK comes from the API; neither is\n * guaranteed stable, and a visitor must not swap sides because a list came\n * back differently.\n */\nexport function pickAbSide<T extends AbAssignable>(\n members: readonly T[],\n visitorId: string,\n testId: string,\n): T {\n const ordered = members\n .slice()\n .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));\n\n const roll = hashString(visitorId + \":\" + testId) % 100;\n\n let acc = 0;\n for (const member of ordered) {\n acc += Math.max(0, member.abWeight);\n if (roll < acc) return member;\n }\n\n // Weights that don't reach 100 (half-written pairing, stale metaobject):\n // give the remainder to the last side rather than showing the shopper\n // nothing.\n return ordered[ordered.length - 1];\n}\n\n/**\n * Reduce a list of bundles to the ones this visitor should actually see:\n * everything not in a test, plus exactly one side of each test.\n *\n * Order is preserved, so a host that renders bundles top to bottom keeps the\n * merchant's ordering.\n *\n * A test whose weights don't add up still yields a side: showing the shopper\n * one offer always beats showing them none. See `pickAbSide`.\n */\nexport function resolveAbTests<T extends AbAssignable>(\n bundles: readonly T[],\n visitorId: string,\n): T[] {\n const groups = new Map<string, T[]>();\n for (const bundle of bundles) {\n if (!bundle.abTestId) continue;\n const existing = groups.get(bundle.abTestId);\n if (existing) existing.push(bundle);\n else groups.set(bundle.abTestId, [bundle]);\n }\n\n // Nothing paired on this page — every bundle stands alone.\n if (groups.size === 0) return bundles.slice();\n\n const chosen = new Map<string, T>();\n groups.forEach((members, testId) => {\n // A test whose other side isn't on this page (different product, or the\n // sibling went inactive) isn't a choice at all. Show what's here.\n if (members.length === 1) {\n chosen.set(testId, members[0]);\n return;\n }\n chosen.set(testId, pickAbSide(members, visitorId, testId));\n });\n\n return bundles.filter(\n (bundle) => !bundle.abTestId || chosen.get(bundle.abTestId) === bundle,\n );\n}\n","/**\n * What a host supplies to the shared renderer.\n *\n * Two hosts use these modules. The **theme app extension** runs inside a\n * merchant's storefront: it formats money through `Shopify.formatMoney`, which\n * honours the shop's configured format, and its copy is already translated by\n * Liquid's `t` filter. The **headless `<lime-bundle>` element** has neither, so\n * it falls back to `Intl.NumberFormat` and the English defaults below.\n *\n * Everything host-specific is passed in rather than imported, which is what\n * lets one implementation serve both. Nothing in `render/` may reach for\n * `window.Shopify` or a global.\n */\n\n/** A line the widget wants in the cart. `priceCents` feeds analytics only. */\nexport interface CartItem {\n variantId: number | string;\n quantity: number;\n priceCents?: number;\n /**\n * Extra line attributes for THIS line only, on top of the bundle\n * attribution every line carries. Used by BOGO to mark which line the\n * shopper chose as the reward — see `BUNDLE_ROLE_ATTRIBUTE`.\n */\n attributes?: Array<{ key: string; value: string }>;\n}\n\n/**\n * The custom variant dropdown, when the host has wired one up.\n *\n * Optional on purpose: the picker modal works with plain `<select>` elements\n * if no dropdown is bound, so a host can skip it.\n */\nexport interface DropdownApi {\n bind: (select: HTMLSelectElement) => unknown;\n bindAll: (root?: ParentNode | null) => void;\n unbindAll: (root?: ParentNode | null) => void;\n observeRoot: (root: HTMLElement | null) => void;\n}\n\n/** Formats a cents amount for display. */\nexport type FormatMoney = (cents: number) => string;\n\n/**\n * Fallback money formatting for hosts without Shopify's.\n *\n * `Intl.NumberFormat` ignores the merchant's configured money format, so this\n * is a last resort — the theme host always supplies `Shopify.formatMoney`.\n */\nexport function intlFormatMoney(currencyCode: string): FormatMoney {\n return (cents) => {\n try {\n return new Intl.NumberFormat(undefined, {\n style: \"currency\",\n currency: currencyCode,\n }).format(cents / 100);\n } catch {\n // An unrecognised currency code throws rather than degrading.\n return currencyCode + \" \" + (cents / 100).toFixed(2);\n }\n };\n}\n\n/**\n * Line-item attribute keys carrying bundle attribution into the order.\n *\n * The `orders/create` webhook reads both to attribute a purchase, so every\n * line every host adds must carry both. Named here because the theme host\n * writes them as `/cart/add.js` properties and the headless host writes them\n * as `CartLineInput.attributes` — different shapes, one contract.\n */\nexport const BUNDLE_GID_ATTRIBUTE = \"_lime_bundle_gid\";\nexport const BUNDLE_TYPE_ATTRIBUTE = \"_lime_bundle_type\";\n\n/**\n * Marks the BOGO line the shopper chose as their reward.\n *\n * The widget lets the shopper pick the \"get\" variant and shows that one\n * struck through, but the discount function allocated the reward\n * cheapest-first across every line of the get product — so on a\n * same-product offer the buy line counted as a get line, and a shopper who\n * picked the pricier variant as their free item watched checkout discount\n * the other one instead (issue #421).\n *\n * Only written when the reward resolves to a line of its own. A shopper who\n * picks the same variant on both sides gets one merged line, where there is\n * nothing to choose between. A cart assembled by hand carries no role at\n * all and the function falls back to cheapest-first, which is what protects\n * the merchant when nobody made a choice.\n */\nexport const BUNDLE_ROLE_ATTRIBUTE = \"_lime_bundle_role\";\nexport const BUNDLE_ROLE_GET = \"get\";\n\nexport function bundleLineAttributes(\n bundleGid: string,\n bundleType: string,\n): Array<{ key: string; value: string }> {\n return [\n { key: BUNDLE_GID_ATTRIBUTE, value: bundleGid },\n { key: BUNDLE_TYPE_ATTRIBUTE, value: bundleType },\n ];\n}\n","import {\n formatUnitPrice,\n isVariantFulfillable,\n resolveBundleQty,\n stockCapForVariant,\n type Product,\n type ProductVariant,\n} from \"@lime-bundles/core\";\nimport type { BundleProduct, BundleVariant } from \"./product/types\";\n\n/**\n * Storefront API shapes → the render layer's shapes.\n *\n * The render layer speaks the theme's dialect: numeric IDs and integer cents,\n * because that is what Liquid emits into the page and converting there would\n * inflate a payload that ships on every product view. The Storefront API\n * speaks GIDs and decimal-string Money, so the headless host converts here\n * instead — once, at the edge.\n */\n\n/** `gid://shopify/Product/123` → `123`. Returns the input if it isn't a GID. */\nexport function numericId(gid: string): number | string {\n const tail = gid.slice(gid.lastIndexOf(\"/\") + 1);\n const n = Number(tail);\n return Number.isFinite(n) && tail !== \"\" ? n : gid;\n}\n\n/** Decimal-string Money → integer cents, rounded the way Shopify rounds. */\nexport function toCents(amount: string | null | undefined): number {\n if (!amount) return 0;\n const n = parseFloat(amount);\n return Number.isFinite(n) ? Math.round(n * 100) : 0;\n}\n\n/**\n * width / height of a Storefront image, or null when either dimension is\n * missing or degenerate — the same value Liquid's `image.aspect_ratio`\n * exposes, which the theme stamps on each row's thumbnail box.\n */\nfunction imageAspectRatio(\n image: { width?: number | null; height?: number | null } | null | undefined,\n): number | null {\n const w = image?.width;\n const h = image?.height;\n if (typeof w !== \"number\" || typeof h !== \"number\" || w <= 0 || h <= 0) {\n return null;\n }\n return w / h;\n}\n\nexport interface AdaptOptions {\n /** Per-product and per-variant quantity overrides from the bundle. */\n quantities: {\n productQuantities: Record<string, number>;\n variantQuantities: Record<string, number>;\n };\n /** Merchant-restricted variant GIDs for this product, or null for all. */\n allowedVariantIds: string[] | null;\n}\n\nfunction adaptVariant(\n v: ProductVariant,\n requiredQty: number,\n fallbackCurrency: string,\n): BundleVariant {\n return {\n id: numericId(v.id),\n title: v.title,\n // Positional, matching how Liquid emits `variant.options`.\n options: (v.selectedOptions ?? []).map((o) => o.value),\n // The same predicate the admin evaluator and the Liquid gate use, so a\n // variant reads as unavailable on every surface at the same moment.\n available: isVariantFulfillable(v, requiredQty),\n price: toCents(v.price?.amount),\n compareAtPrice: toCents(v.compareAtPrice?.amount),\n unitPrice: formatUnitPrice(v.unitPrice, v.unitPriceMeasurement, fallbackCurrency),\n image: v.image?.url ?? null,\n // Carried so the picker's stepper cap and cross-slot stock guard can\n // bind on the headless surface; dropping it here left them capless.\n inventoryQuantity: stockCapForVariant(v),\n };\n}\n\n/**\n * Adapt one product, narrowed to the variants the merchant allowed.\n *\n * `selectedVariantId` lands on the first allowed variant that is actually\n * fulfillable, falling back to the first allowed one — the same order the\n * Liquid uses when it picks a row's initial variant.\n */\nexport function adaptProduct(\n product: Product,\n bundleId: string,\n opts: AdaptOptions,\n): BundleProduct {\n const allowed = opts.allowedVariantIds;\n const nodes = product.variants.nodes.filter(\n (v) => !allowed || allowed.length === 0 || allowed.includes(v.id),\n );\n\n const currency = product.priceRange.minVariantPrice.currencyCode;\n const productQty = resolveBundleQty(\n opts.quantities,\n product.id,\n nodes[0]?.id ?? \"\",\n );\n\n const variants = nodes.map((v) => {\n const qty = resolveBundleQty(opts.quantities, product.id, v.id);\n const adapted = adaptVariant(v, qty, currency);\n // Only carry a per-variant quantity when it actually differs, so the\n // render layer's `variant.quantity || product.quantity` fallback behaves\n // exactly as it does for Liquid-emitted data.\n if (qty !== productQty) adapted.quantity = qty;\n return adapted;\n });\n\n const initial = variants.find((v) => v.available) ?? variants[0];\n\n return {\n productId: numericId(product.id),\n title: product.title,\n url: product.handle ? \"/products/\" + product.handle : null,\n featuredImage: product.featuredImage?.url ?? null,\n featuredImageRatio: imageAspectRatio(product.featuredImage),\n quantity: productQty,\n selectedVariantId: initial ? initial.id : \"\",\n // Option names come off a variant, since the Storefront product query\n // returns them per-variant rather than as a product-level list.\n optionNames: (nodes[0]?.selectedOptions ?? []).map((o) => o.name),\n variants,\n };\n}\n\n/** Adapt every product in a bundle, honouring per-product variant restrictions. */\nexport function adaptProducts(bundle: {\n id: string;\n products: Product[];\n productQuantities: Record<string, number>;\n variantQuantities: Record<string, number>;\n selectedVariantIds: string[][] | null;\n}): BundleProduct[] {\n return bundle.products.map((product, idx) =>\n adaptProduct(product, bundle.id, {\n quantities: {\n productQuantities: bundle.productQuantities,\n variantQuantities: bundle.variantQuantities,\n },\n allowedVariantIds: bundle.selectedVariantIds?.[idx] ?? null,\n }),\n );\n}\n","import type { BundleProduct, BundleVariant } from \"./types\";\n\n/**\n * Resolve a variant by ID, falling back to the product's first.\n *\n * Both sides are coerced to strings because Liquid emits numeric IDs into JSON\n * while DOM attributes come back as strings, and a strict compare between the\n * two silently misses every time.\n */\nexport function findVariant(\n product: BundleProduct,\n variantId: number | string,\n): BundleVariant {\n for (let i = 0; i < product.variants.length; i++) {\n if (String(product.variants[i].id) === String(variantId)) {\n return product.variants[i];\n }\n }\n return product.variants[0];\n}\n\n/**\n * The variant a row's initial selection should land on: the emitted\n * `selectedVariantId` when that variant is sellable at the bundle's quantity,\n * otherwise the first sellable variant, otherwise null — nothing in the slot\n * can cover, and the caller's CTA lock owns that state.\n *\n * Exists because `selectedVariantId` and the per-variant quantity-aware\n * `available` flags are computed separately at emit time. Seeding a greyed\n * option leaves a live Add-to-cart whose click can only fail while a buyable\n * variant sits one option away (#278). Missing `available` is treated as\n * sellable, matching `isOptionValueAvailable`'s default-true contract.\n */\nexport function resolveSellableVariant(\n product: BundleProduct,\n): BundleVariant | null {\n const seeded = findVariant(product, product.selectedVariantId);\n if (seeded && seeded.available !== false) return seeded;\n for (let i = 0; i < product.variants.length; i++) {\n if (product.variants[i].available !== false) return product.variants[i];\n }\n return null;\n}\n\n/**\n * Resolve the variant whose option values match `optionValues` positionally.\n *\n * Returns null rather than guessing when nothing matches, leaving the fallback\n * to the caller.\n */\nexport function findVariantByOptions(\n product: BundleProduct,\n optionValues: string[] | null | undefined,\n): BundleVariant | null {\n if (!product.variants || !optionValues) return null;\n for (let i = 0; i < product.variants.length; i++) {\n const v = product.variants[i];\n if (!v.options || v.options.length !== optionValues.length) continue;\n let match = true;\n for (let j = 0; j < v.options.length; j++) {\n if (v.options[j] !== optionValues[j]) {\n match = false;\n break;\n }\n }\n if (match) return v;\n }\n return null;\n}\n\n/**\n * Whether any in-stock variant pairs `value` at `optionIndex` with the current\n * selection at every other index.\n *\n * Drives per-`<option>` disabled state the way Dawn does, so picking Large\n * greys out the colours Large doesn't come in rather than silently jumping the\n * shopper to a different combination. Sold-out variants are treated as\n * unavailable, which is the same UX.\n */\nexport function isOptionValueAvailable(\n variants: BundleVariant[],\n optionIndex: number,\n value: string,\n selected: string[],\n): boolean {\n for (let i = 0; i < variants.length; i++) {\n const v = variants[i];\n if (v.available === false) continue;\n if (!v.options || v.options[optionIndex] !== value) continue;\n let ok = true;\n for (let j = 0; j < v.options.length; j++) {\n if (j === optionIndex) continue;\n if (v.options[j] !== selected[j]) {\n ok = false;\n break;\n }\n }\n if (ok) return true;\n }\n return false;\n}\n","import { findVariant } from \"../product/variants\";\nimport type { BundleProduct } from \"../product/types\";\nimport type { CartItem } from \"../host\";\n\n/**\n * Build the add-to-cart payload from the current selection.\n *\n * A fixed bundle is all-or-nothing, so every product contributes a line.\n * `priceCents` is the line total (unit × qty) and feeds analytics only.\n */\nexport function buildCartItems(products: BundleProduct[]): CartItem[] {\n const items: CartItem[] = [];\n for (let i = 0; i < products.length; i++) {\n const variant = findVariant(products[i], products[i].selectedVariantId);\n const qty = variant.quantity || products[i].quantity || 1;\n items.push({\n variantId: Number(products[i].selectedVariantId),\n quantity: qty,\n priceCents: (variant.price || 0) * qty,\n });\n }\n return items;\n}\n","import type { FormatMoney } from \"../host\";\nimport { findVariant } from \"../product/variants\";\nimport type { BundleProduct } from \"../product/types\";\n\nexport interface BundlePricing {\n totalPrice: number;\n salePrice: number;\n savings: number;\n}\n\n/**\n * Derive the bundle's totals from the currently selected variants.\n *\n * Pure, so the arithmetic can be tested without a DOM. `recalcPricing` below\n * paints the result.\n *\n * `percentage` floors per unit, matching how Shopify's discount engine rounds\n * at checkout — summing then discounting would drift by a cent on some\n * quantities and quote the shopper a total they don't get charged.\n *\n * Currency: `discountValue` for `fixed_amount` / `flat_price` must be in\n * the same currency as `variant.price` — the currency the host renders in.\n * The merchant configures the amount in the shop's STORE currency, and the\n * checkout (Rust discount function) converts it by the input's\n * `presentmentCurrencyRate`, so each host converts before this code runs:\n * the theme entries rewrite `data-discount-value` at hydration from\n * `Shopify.currency.rate`, and the headless SDK converts the parsed bundle\n * with the consumer-supplied `currencyRate` (`convertBundleToPresentment`).\n * This layer stays currency-blind; percentage bundles need no conversion.\n */\nexport function computeBundlePricing(\n products: BundleProduct[],\n discountType: string,\n discountValue: number,\n): BundlePricing {\n let totalPrice = 0;\n let salePrice = 0;\n\n for (let i = 0; i < products.length; i++) {\n const variant = findVariant(products[i], products[i].selectedVariantId);\n const qty = variant.quantity || products[i].quantity || 1;\n const linePrice = variant.price * qty;\n totalPrice += linePrice;\n\n if (discountType === \"percentage\") {\n const unitDiscount = Math.floor((variant.price * discountValue) / 100);\n salePrice += (variant.price - unitDiscount) * qty;\n } else {\n // fixed_amount accumulates undiscounted and is adjusted after the loop.\n salePrice += linePrice;\n }\n }\n\n // applies_to_each_item: false — one deduction from the bundle total.\n if (discountType === \"fixed_amount\") {\n salePrice = Math.max(0, totalPrice - Math.round(discountValue * 100));\n }\n if (discountType === \"flat_price\") {\n const flatCents = Math.round(discountValue * 100);\n salePrice = flatCents < totalPrice ? flatCents : totalPrice;\n }\n\n return { totalPrice, salePrice, savings: totalPrice - salePrice };\n}\n\n/**\n * Recompute and paint the bundle total after a variant change.\n *\n * Distinct from the shared `updatePricing` in dom.ts: that one takes finished\n * numbers, this one derives them from the current selection first.\n */\nexport function recalcPricing(\n container: HTMLElement,\n products: BundleProduct[],\n formatMoney: FormatMoney,\n): void {\n // `container` is an ancestor when Liquid rendered the widget and the root\n // itself when the headless host built it, so check both.\n const fixed = container.classList?.contains(\"lb-fixed\")\n ? container\n : container.querySelector<HTMLElement>(\".lb-fixed\");\n if (!fixed) return;\n\n const { totalPrice, salePrice, savings } = computeBundlePricing(\n products,\n fixed.getAttribute(\"data-discount-type\") || \"\",\n parseFloat(fixed.getAttribute(\"data-discount-value\") ?? \"\") || 0,\n );\n\n const saleEl = container.querySelector<HTMLElement>(\"[data-sale-price]\");\n const compareEl = container.querySelector<HTMLElement>(\"[data-compare-price]\");\n const savingsBar = container.querySelector<HTMLElement>(\"[data-savings-bar]\");\n const savingsAmountEl =\n container.querySelector<HTMLElement>(\"[data-savings-amount]\");\n const savingsPercentEl = container.querySelector<HTMLElement>(\n \"[data-savings-percent]\",\n );\n\n if (saleEl) saleEl.textContent = formatMoney(salePrice);\n\n if (compareEl) {\n if (savings > 0) {\n compareEl.textContent = formatMoney(totalPrice);\n compareEl.style.display = \"\";\n } else {\n compareEl.style.display = \"none\";\n }\n }\n\n if (savingsBar) {\n if (savings > 0) {\n if (savingsAmountEl) savingsAmountEl.textContent = formatMoney(savings);\n if (savingsPercentEl) {\n const pct =\n totalPrice > 0 ? Math.round((savings * 100) / totalPrice) : 0;\n savingsPercentEl.textContent = \"(\" + pct + \"%)\";\n }\n savingsBar.style.display = \"\";\n } else {\n savingsBar.style.display = \"none\";\n }\n }\n}\n","import {\n findVariant,\n findVariantByOptions,\n isOptionValueAvailable,\n} from \"./variants\";\nimport type { BundleProduct, BundleVariant } from \"./types\";\n\n/**\n * Wire a row's per-option `<select>` elements to variant resolution.\n *\n * One select per product option (Size, Colour, …), each tagged with\n * `data-option-position`. On change the selected value of every select in the\n * row is collected, resolved to a variant, and handed to `onSelected` for\n * painting — the caller owns what a row looks like, this owns how a variant is\n * chosen.\n *\n * Shared by fixed and BOGO, which emit the same per-variant JSON from Liquid\n * and previously carried near-identical copies of this logic.\n *\n * `getProduct` is a callback rather than a value because fixed resolves its\n * product through an index map that must be read at event time, not at bind\n * time.\n */\nexport function bindVariantSelects(\n row: HTMLElement,\n getProduct: () => BundleProduct | null | undefined,\n onSelected: (variant: BundleVariant, product: BundleProduct) => void,\n): void {\n const optionSelects =\n row.querySelectorAll<HTMLSelectElement>(\"[data-variant-option]\");\n if (!optionSelects.length) return;\n\n function positionOf(sel: HTMLSelectElement): number {\n return parseInt(sel.getAttribute(\"data-option-position\") ?? \"\", 10);\n }\n\n function collectOptionValues(): string[] {\n const values: string[] = [];\n for (let i = 0; i < optionSelects.length; i++) {\n const pos = positionOf(optionSelects[i]);\n if (!pos || pos < 1) continue;\n values[pos - 1] = optionSelects[i].value;\n }\n return values;\n }\n\n function syncSelectsToVariant(variant: BundleVariant | null): void {\n if (!variant || !variant.options) return;\n for (let i = 0; i < optionSelects.length; i++) {\n const pos = positionOf(optionSelects[i]);\n if (!pos || pos < 1) continue;\n const expected = variant.options[pos - 1];\n if (optionSelects[i].value !== expected) {\n optionSelects[i].value = expected;\n }\n }\n }\n\n /** Grey out values that don't pair with the current selection (Dawn-style). */\n function recomputeDisabledState(\n product: BundleProduct,\n selected: string[],\n ): void {\n for (let i = 0; i < optionSelects.length; i++) {\n const sel = optionSelects[i];\n const pos = positionOf(sel);\n if (!pos || pos < 1) continue;\n const opts = sel.options;\n for (let o = 0; o < opts.length; o++) {\n opts[o].disabled = !isOptionValueAvailable(\n product.variants,\n pos - 1,\n opts[o].value,\n selected,\n );\n }\n }\n }\n\n const seeded = getProduct();\n if (seeded) {\n const initialVariant = findVariant(seeded, seeded.selectedVariantId);\n if (initialVariant && initialVariant.options) {\n recomputeDisabledState(seeded, initialVariant.options);\n }\n }\n\n for (let s = 0; s < optionSelects.length; s++) {\n optionSelects[s].addEventListener(\"change\", (e) => {\n // Keep the theme's product-page JS out of this. Dawn's\n // MediaGallery.preloadImage throws when it can't find the bundle's\n // variant among the product's own.\n e.stopPropagation();\n\n const product = getProduct();\n if (!product) return;\n\n const variant = findVariantByOptions(product, collectOptionValues());\n if (!variant) {\n // A disabled value got through — some browsers let you\n // keyboard-select a disabled <option>. Revert rather than jump to an\n // unrelated combination.\n const prev = findVariant(product, product.selectedVariantId);\n syncSelectsToVariant(prev);\n if (prev && prev.options) recomputeDisabledState(product, prev.options);\n return;\n }\n\n product.selectedVariantId = variant.id;\n recomputeDisabledState(product, variant.options);\n onSelected(variant, product);\n });\n }\n}\n","import type { BundleProduct, BundleVariant } from \"./types\";\n\n/**\n * Fill the geometry skeleton Liquid rendered.\n *\n * Liquid emits the row's structure and the product title — the only piece\n * whose height it cannot predict, because text wrapping depends on the\n * merchant's font. Every other box is emitted empty at its final height (see\n * the `:empty` reservations in bundle-base.css) and filled here, so the row\n * never changes size between first paint and hydration.\n *\n * Filling rather than replacing is deliberate: replacing the row would remove\n * and re-add layout boxes, which is a shift even when the dimensions match.\n *\n * Only the controls are built here — the badge and the option selects. The\n * caller paints prices, because fixed and BOGO price a row differently (BOGO's\n * reward side shows a struck original beside the discounted unit).\n */\nexport function hydrateRowControls(\n row: HTMLElement,\n product: BundleProduct,\n selected: BundleVariant,\n): void {\n if (!product.variants.length) return;\n\n // A single eligible variant has nothing to choose, so it reads as a label.\n // More than one gets the option selects. Liquid decided which by emitting\n // the matching empty container, so follow what it reserved.\n const badge = row.querySelector<HTMLElement>(\".lb-bundle-variant-badge\");\n if (badge && !badge.textContent) {\n badge.textContent = selected.title;\n }\n\n const groups = row.querySelector<HTMLElement>(\n \".lb-bundle-variant-option-groups\",\n );\n if (groups && !groups.children.length) {\n buildOptionGroups(groups, product, selected);\n }\n}\n\n/** One `<select>` per product option, values de-duplicated in first-seen order. */\nfunction buildOptionGroups(\n container: HTMLElement,\n product: BundleProduct,\n selected: BundleVariant,\n): void {\n const optionNames = product.optionNames || [];\n\n for (let i = 0; i < optionNames.length; i++) {\n const group = document.createElement(\"div\");\n group.className = \"lb-bundle-variant-option-group\";\n\n const label = document.createElement(\"span\");\n label.className = \"lb-bundle-variant-option-label\";\n label.textContent = optionNames[i];\n group.appendChild(label);\n\n const select = document.createElement(\"select\");\n select.className = \"lb-bundle-variant-select\";\n select.setAttribute(\"data-variant-option\", \"\");\n select.setAttribute(\"data-option-position\", String(i + 1));\n select.setAttribute(\"data-product-id\", String(product.productId));\n select.name = \"lb-variant-\" + product.productId + \"-\" + (i + 1);\n select.setAttribute(\"aria-label\", optionNames[i]);\n\n const seen: Record<string, boolean> = Object.create(null);\n for (const v of product.variants) {\n const value = v.options && v.options[i];\n if (!value || seen[value]) continue;\n seen[value] = true;\n\n const opt = document.createElement(\"option\");\n opt.value = value;\n opt.textContent = value;\n if (selected.options && selected.options[i] === value) {\n opt.selected = true;\n }\n select.appendChild(opt);\n }\n\n group.appendChild(select);\n container.appendChild(group);\n }\n}\n","import type { FormatMoney } from \"../host\";\nimport type { BundleProduct, BundleVariant } from \"./types\";\n\n/**\n * Remembers each row's original image so a variant without one can fall back.\n *\n * Seeded from the product data when present, else from whatever `<img>` the\n * row already had — the skeleton rows Liquid emits start with an empty\n * thumbnail, so there is nothing to read off the DOM.\n */\nconst productImgSrc = new WeakMap<HTMLElement, string>();\n\n/**\n * Point the row's thumbnail at the selected variant's image.\n *\n * Falls back to the row's initial `img.src` — the Liquid-rendered product hero\n * — so a variant with no image of its own doesn't leave the thumbnail stuck on\n * the previously selected variant's photo.\n */\nexport function updateThumbnail(\n row: HTMLElement,\n variant: BundleVariant,\n productImage?: string | null,\n): void {\n const thumb = row.querySelector<HTMLElement>(\"[data-thumbnail]\");\n if (!thumb) return;\n\n const img = thumb.querySelector(\"img\");\n if (!productImgSrc.has(row)) {\n const seed = productImage || (img ? img.src : null);\n if (seed) productImgSrc.set(row, seed);\n }\n\n const nextSrc = variant.image || productImgSrc.get(row) || null;\n if (!nextSrc) return;\n\n if (img) {\n img.src = nextSrc;\n // The Liquid srcset describes the original image, so it has to go or the\n // browser may keep serving a candidate from the previous variant.\n img.srcset = \"\";\n } else {\n // The row had only the placeholder SVG. Build an <img> rather than\n // assigning innerHTML, which would make the image URL an injection point.\n const newImg = document.createElement(\"img\");\n newImg.src = nextSrc;\n newImg.loading = \"lazy\";\n newImg.alt = \"\";\n thumb.textContent = \"\";\n thumb.appendChild(newImg);\n }\n}\n\n/**\n * Update the inline \"×N\" beside a row's price.\n *\n * Always rendered, even at ×1, so the quantity reads as a consistent column\n * down the bundle manifest.\n */\nexport function updateInlineQuantity(row: HTMLElement, qty: number): void {\n const el = row.querySelector<HTMLElement>(\"[data-qty-inline]\");\n if (!el) return;\n el.textContent = \"×\" + qty;\n}\n\n/**\n * Paint a row to reflect a newly selected variant.\n *\n * Purely visual — `bindVariantSelects` owns `product.selectedVariantId`.\n */\nexport function applyVariant(\n row: HTMLElement,\n variant: BundleVariant | null,\n product: BundleProduct,\n formatMoney: FormatMoney,\n): void {\n if (!variant) return;\n\n updateThumbnail(row, variant, product.featuredImage);\n updateInlineQuantity(row, variant.quantity || product.quantity || 1);\n\n const priceEl = row.querySelector<HTMLElement>(\"[data-product-price]\");\n if (priceEl && variant.price != null) {\n priceEl.textContent = formatMoney(variant.price);\n }\n\n const compareEl = row.querySelector<HTMLElement>(\n \"[data-product-compare-price]\",\n );\n if (compareEl) {\n const compare = variant.compareAtPrice;\n if (compare && compare > variant.price) {\n compareEl.textContent = formatMoney(compare);\n compareEl.hidden = false;\n } else {\n compareEl.textContent = \"\";\n compareEl.hidden = true;\n }\n }\n\n const unitEl = row.querySelector<HTMLElement>(\"[data-product-unit-price]\");\n if (unitEl) {\n if (variant.unitPrice) {\n unitEl.textContent = variant.unitPrice;\n unitEl.hidden = false;\n } else {\n unitEl.textContent = \"\";\n unitEl.hidden = true;\n }\n }\n}\n","import type { BundleProduct } from \"./product/types\";\nimport type { PickerTranslations } from \"./picker/types\";\n\n/**\n * The DOM twin of the row skeleton `lb-fixed.liquid` emits.\n *\n * The theme host gets this markup from Liquid, server-rendered, and hydrates\n * it. The headless host has no Liquid, so it builds the same thing here and\n * hydrates it identically — one hydration path, two ways of getting the boxes\n * onto the page.\n *\n * **These must stay in step.** `app/__tests__/skeleton-liquid-parity.test.ts`\n * compares this output against the structure the Liquid emits; the CSS `:empty`\n * reservations are keyed to these exact class names, so a box added here\n * without one added there (and vice versa) is a layout shift.\n */\n\n/** Marks an element as an empty box the hydration step will fill. */\nfunction box(tag: string, className: string, attrs?: Record<string, string>) {\n const el = document.createElement(tag);\n el.className = className;\n for (const k in attrs) el.setAttribute(k, attrs[k]);\n return el;\n}\n\nexport interface RowSkeletonOptions {\n /** Greys the row and replaces its controls with an out-of-stock label. */\n isOos: boolean;\n /** Reward chip text for BOGO's get side. */\n badgeText?: string | null;\n /** BOGO only: which side of the offer this row is. */\n bogoRole?: \"buy\" | \"get\";\n /** BOGO renders its fixed quantity inline; fixed uses a separate chip. */\n inlineQuantity?: number | null;\n /** Product page link. Omitted when the host has no URL for the product. */\n href?: string | null;\n /**\n * Merchant `productList.thumbnailRatio`. Only \"original\" has any effect\n * here: that setting sizes each box to its image's own proportions, which\n * nothing knows before the image loads, so the box gets the ratio stamped\n * inline as `--lb-row-thumb-ratio` — the same stamp the Liquid rows emit.\n * The fixed ratios (square, tall, wide) reserve via the widget-level CSS\n * var, and stamping them here would override the merchant's choice with\n * the image's own (see widget-thumbnail-ratio-override.test.ts).\n */\n thumbnailRatio?: string;\n}\n\n/**\n * Build one product row, empty apart from its title.\n *\n * The title is the only content rendered up front, here as in Liquid: its\n * height depends on where the merchant's font wraps it, so no reservation can\n * predict it.\n */\nexport function buildRowSkeleton(\n product: BundleProduct,\n t: PickerTranslations & { outOfStock?: string },\n opts: RowSkeletonOptions,\n): HTMLElement {\n const eligible = product.variants.length;\n\n const row = box(\"div\", \"lb-bundle-product-row\");\n if (opts.isOos) {\n row.classList.add(\"lb-bundle-product-row--oos\");\n row.setAttribute(\"aria-disabled\", \"true\");\n }\n row.setAttribute(\"data-product-id\", String(product.productId));\n if (opts.bogoRole) row.setAttribute(\"data-bogo-role\", opts.bogoRole);\n\n const thumb = box(\"div\", \"lb-bundle-thumbnail\", { \"data-thumbnail\": \"\" });\n // Liquid guards this on `thumbnail_ratio == 'original' and bp.featured_image`;\n // a product without an image keeps the placeholder box, unstamped.\n const ratio = product.featuredImageRatio;\n if (opts.thumbnailRatio === \"original\" && ratio != null && ratio > 0) {\n thumb.style.setProperty(\"--lb-row-thumb-ratio\", String(ratio));\n }\n row.appendChild(thumb);\n\n const info = box(\"div\", \"lb-bundle-product-info\");\n\n const title = document.createElement(opts.href ? \"a\" : \"span\");\n title.className = \"lb-bundle-product-name\";\n if (opts.href) (title as HTMLAnchorElement).href = opts.href;\n title.textContent = product.title ?? \"\";\n info.appendChild(title);\n\n if (opts.isOos) {\n const oos = box(\"span\", \"lb-bundle-oos-label\");\n oos.textContent = t.outOfStock || \"Out of stock\";\n info.appendChild(oos);\n row.appendChild(info);\n return row;\n }\n\n // A single eligible variant has nothing to choose, so it reads as a label.\n if (eligible === 1 && product.variants[0]?.title !== \"Default Title\") {\n info.appendChild(box(\"span\", \"lb-bundle-variant-badge\"));\n }\n\n const prices = box(\"span\", \"lb-bundle-product-prices\");\n prices.appendChild(\n box(\"span\", \"lb-bundle-product-compare-price\", {\n \"data-product-compare-price\": \"\",\n hidden: \"\",\n }),\n );\n prices.appendChild(\n box(\"span\", \"lb-bundle-product-price\", { \"data-product-price\": \"\" }),\n );\n if (opts.inlineQuantity != null) {\n const qty = box(\"span\", \"lb-bundle-qty-inline\", { \"data-qty-inline\": \"\" });\n qty.textContent = \"×\" + opts.inlineQuantity;\n prices.appendChild(qty);\n }\n info.appendChild(prices);\n\n info.appendChild(\n box(\"span\", \"lb-bundle-product-unit-price\", {\n \"data-product-unit-price\": \"\",\n }),\n );\n\n if (eligible > 1) {\n info.appendChild(\n box(\"div\", \"lb-bundle-variant-option-groups\", {\n // Drives the `:empty` height reservation, exactly as the Liquid's\n // inline style does.\n style: \"--lb-row-option-count: \" + (product.optionNames.length || 1),\n }),\n );\n }\n\n row.appendChild(info);\n\n if (opts.inlineQuantity == null) {\n row.appendChild(box(\"span\", \"lb-bundle-qty-chip\", { \"data-qty-inline\": \"\" }));\n }\n if (opts.badgeText) {\n const badge = box(\"span\", \"lb-bogo__badge\");\n badge.textContent = opts.badgeText;\n row.appendChild(badge);\n }\n\n return row;\n}\n\nexport interface ShellOptions {\n title: string;\n /** Volume labels its summary \"Total\"; the others use \"Bundle price\". */\n summaryLabel?: string;\n /** Volume only: a slot for the \"(N items)\" count beside the label. */\n withItemCount?: boolean;\n /** Volume paints its total into [data-total-price] rather than the sale slot. */\n totalPriceSlot?: boolean;\n subtitle?: string | null;\n showSavingsBar: boolean;\n showComparePrice: boolean;\n ctaText: string;\n /** ISO end date; renders the countdown strip when the merchant set one. */\n endsAt?: string | null;\n countdownLabel?: string;\n}\n\nexport interface WidgetShell {\n root: HTMLElement;\n /** Where rows go. */\n products: HTMLElement;\n cta: HTMLButtonElement;\n}\n\n/**\n * Build the widget chrome the theme gets from Liquid: header, product list,\n * divider, price summary and CTA.\n *\n * Mirrors the anatomy contract in WIDGET-DESIGN.md §1 — the section order is\n * load-bearing, because the CSS targets siblings by position in places.\n */\nexport function buildWidgetShell(\n typeClass: string,\n opts: ShellOptions,\n): WidgetShell {\n const root = box(\"div\", typeClass);\n\n const header = box(\"div\", \"lb-bundle-header\");\n const heading = box(\"h3\", \"lb-bundle-title\");\n heading.textContent = opts.title;\n header.appendChild(heading);\n if (opts.subtitle) {\n const sub = box(\"p\", \"lb-bundle-subtitle\");\n sub.textContent = opts.subtitle;\n header.appendChild(sub);\n }\n root.appendChild(header);\n\n if (opts.endsAt) {\n const countdown = box(\"div\", \"lb-bundle-countdown\", {\n \"data-countdown\": \"\",\n });\n const label = box(\"span\", \"lb-bundle-countdown-label\");\n // Matches the theme's `countdown_label` translation default.\n label.textContent = opts.countdownLabel ?? \"Limited time offer\";\n countdown.appendChild(label);\n countdown.appendChild(\n box(\"span\", \"lb-bundle-countdown-timer\", {\n \"data-countdown-timer\": \"\",\n }),\n );\n root.appendChild(countdown);\n }\n\n const products = box(\"div\", typeClass + \"__products lb-edge-fade\");\n root.appendChild(products);\n\n root.appendChild(box(\"div\", \"lb-bundle-divider\"));\n\n // `data-pricing-section` is the hook the mix-and-match and multi-step\n // pricing updaters hide until the bundle qualifies — the same attribute\n // the Liquid summaries carry. The other types never touch it.\n const summary = box(\"div\", \"lb-bundle-summary\", {\n \"data-pricing-section\": \"\",\n });\n const text = box(\"div\", \"lb-bundle-summary__text\");\n const label = box(\"span\", \"lb-bundle-summary__label\");\n label.textContent = opts.summaryLabel ?? \"Bundle total\";\n text.appendChild(label);\n if (opts.withItemCount) {\n // Volume writes \"(3 items)\" beside the label as the tier changes.\n label.appendChild(box(\"span\", \"\", { \"data-item-count\": \"\" }));\n }\n if (opts.showSavingsBar) {\n const savings = box(\"p\", \"lb-bundle-savings-line\", {\n \"data-savings-bar\": \"\",\n style: \"display:none\",\n });\n savings.appendChild(document.createTextNode(\"Save \"));\n savings.appendChild(box(\"span\", \"\", { \"data-savings-amount\": \"\" }));\n savings.appendChild(document.createTextNode(\" \"));\n savings.appendChild(box(\"span\", \"\", { \"data-savings-percent\": \"\" }));\n text.appendChild(savings);\n }\n summary.appendChild(text);\n\n const prices = box(\"span\", \"lb-bundle-summary__prices\");\n if (opts.showComparePrice) {\n prices.appendChild(\n box(\"span\", \"lb-bundle-compare-price\", { \"data-compare-price\": \"\" }),\n );\n }\n prices.appendChild(\n box(\"span\", \"lb-bundle-sale-price\", {\n [opts.totalPriceSlot ? \"data-total-price\" : \"data-sale-price\"]: \"\",\n }),\n );\n summary.appendChild(prices);\n root.appendChild(summary);\n\n // `data-cta-text` mirrors the attribute the Liquid CTA carries: the state\n // repainters (mix-match slots, multi-step body) re-derive the idle label\n // from it, so without the attribute a repaint would wipe the merchant's\n // custom text back to the translation default.\n const cta = box(\"button\", \"lb-bundle-cta\", {\n \"data-cta-text\": opts.ctaText,\n type: \"button\",\n \"data-add-bundle\": \"\",\n }) as HTMLButtonElement;\n const ctaLabel = box(\"span\", \"lb-cta-label\", { \"data-cta-label\": \"\" });\n ctaLabel.textContent = opts.ctaText;\n cta.appendChild(ctaLabel);\n root.appendChild(cta);\n\n root.appendChild(\n box(\"p\", \"lb-bundle-error\", { \"data-error\": \"\", \"aria-live\": \"polite\" }),\n );\n root.appendChild(\n box(\"span\", \"lb-visually-hidden\", {\n \"data-status\": \"\",\n \"aria-live\": \"polite\",\n }),\n );\n\n return { root, products, cta };\n}\n\nexport interface TierSpec {\n /** Units this tier requires. */\n quantity: number;\n /** Percentage off, or 0 for an amount-based tier. */\n percent: number;\n /** Cents off per unit, or 0 for a percentage tier. */\n amountCents: number;\n label: string;\n savingsLabel?: string | null;\n badgeLabel?: string | null;\n selected: boolean;\n}\n\nexport interface VolumeTierOptions {\n /** Mirror of `pricing.showComparePrice` — the struck base price per tier. */\n showCompare?: boolean;\n /** Mirror of `pricing.showPerUnitPrice` — the discounted \"each\" price. */\n showPriceEach?: boolean;\n /** Text of the unit label after the per-unit price (\"each\" on the theme). */\n unitLabel?: string | null;\n}\n\n/**\n * The DOM twin of the tier list `lb-volume.liquid` emits.\n *\n * The `data-tier-*` attributes are the contract: `selectTier` and\n * `updateAllTierPrices` in render/volume/pricing.ts read the quantity and the\n * discount straight off the element, so the same code drives the list whether\n * Liquid or this built it. Like the Liquid, the compare span only exists on\n * tiers that actually discount, and the price nodes are emitted empty — the\n * host fills them via `updateAllTierPrices` once it knows the base price.\n */\nexport function buildVolumeTiers(\n tiers: TierSpec[],\n groupLabel: string,\n opts: VolumeTierOptions = {},\n): HTMLElement {\n const showCompare = opts.showCompare !== false;\n const showPriceEach = opts.showPriceEach !== false;\n const group = box(\"div\", \"lb-volume__tiers lb-edge-fade\", {\n role: \"radiogroup\",\n \"aria-label\": groupLabel,\n \"data-tier-group\": \"\",\n });\n\n tiers.forEach((tier, i) => {\n const el = box(\"div\", \"lb-volume__tier\", {\n role: \"radio\",\n \"aria-checked\": tier.selected ? \"true\" : \"false\",\n // Roving tabindex: the group is one tab stop and arrows move within it.\n tabindex: tier.selected ? \"0\" : \"-1\",\n \"data-tier-index\": String(i),\n \"data-tier-qty\": String(tier.quantity),\n \"data-tier-pct\": String(tier.percent),\n \"data-tier-amt\": String(tier.amountCents),\n });\n\n const radio = box(\"span\", \"lb-volume__radio\");\n radio.appendChild(box(\"span\", \"lb-volume__radio-dot\"));\n el.appendChild(radio);\n\n const info = box(\"span\", \"lb-volume__tier-info\");\n const label = box(\"span\", \"lb-volume__tier-label\");\n label.textContent = tier.label;\n info.appendChild(label);\n\n const price = box(\"span\", \"lb-volume__tier-price\");\n if (showCompare && (tier.percent > 0 || tier.amountCents > 0)) {\n price.appendChild(box(\"span\", \"lb-volume__tier-compare\"));\n }\n if (showPriceEach) {\n price.appendChild(box(\"span\", \"\", { \"data-tier-price-each\": \"\" }));\n if (opts.unitLabel) {\n const unit = box(\"span\", \"lb-volume__tier-unit\");\n unit.textContent = opts.unitLabel;\n price.appendChild(unit);\n }\n }\n info.appendChild(price);\n el.appendChild(info);\n\n if (tier.badgeLabel || tier.savingsLabel) {\n const right = box(\"span\", \"lb-volume__tier-right\");\n if (tier.badgeLabel) {\n const badge = box(\"span\", \"lb-volume__tier-badge\");\n badge.textContent = tier.badgeLabel;\n right.appendChild(badge);\n }\n if (tier.savingsLabel) {\n const savings = box(\"span\", \"lb-volume__tier-savings\");\n savings.textContent = tier.savingsLabel;\n right.appendChild(savings);\n }\n el.appendChild(right);\n }\n\n group.appendChild(el);\n });\n\n return group;\n}\n\n/**\n * The DOM twin of the progress bar in `lb-mix-match.liquid`.\n *\n * One segment per required unit, plus the two live-region labels\n * `updateProgress` writes into. Multi-step passes its step count instead, so\n * the same bar reads \"2 of 3 steps completed\".\n */\nexport function buildProgressBar(segmentCount: number): HTMLElement {\n const wrap = box(\"div\", \"lb-mix-match__progress\", { \"data-progress\": \"\" });\n\n const segments = box(\"div\", \"lb-mix-match__progress-segments\", {\n role: \"progressbar\",\n \"aria-valuenow\": \"0\",\n \"aria-valuemin\": \"0\",\n \"aria-valuemax\": String(segmentCount),\n });\n for (let i = 0; i < segmentCount; i++) {\n segments.appendChild(\n box(\"span\", \"lb-mix-match__progress-segment\", {\n \"data-progress-segment\": \"\",\n }),\n );\n }\n wrap.appendChild(segments);\n\n const labels = box(\"div\", \"lb-mix-match__progress-labels\");\n labels.appendChild(\n box(\"span\", \"lb-mix-match__progress-count\", {\n \"data-progress-count\": \"\",\n \"aria-live\": \"polite\",\n }),\n );\n labels.appendChild(\n box(\"span\", \"lb-mix-match__progress-remaining\", {\n \"data-progress-remaining\": \"\",\n \"aria-live\": \"polite\",\n }),\n );\n wrap.appendChild(labels);\n\n return wrap;\n}\n","declare global {\n interface HTMLElement {\n /** Handle for the ticking timer, so a re-init can clear the previous one. */\n _lbCountdownInterval?: ReturnType<typeof setInterval> | null;\n }\n}\n\nfunction pad(n: number): string {\n return n < 10 ? \"0\" + n : String(n);\n}\n\n/**\n * Drive the \"ends in\" timer from the widget's `data-ends-at`, and hide the\n * whole widget once the window closes.\n *\n * Host-agnostic: it reads the end date off the DOM, so the theme (where Liquid\n * writes the attribute) and the headless element (where the renderer does)\n * both use it unchanged.\n *\n * Clears any prior interval first: the theme editor re-runs init on every\n * section reload, and without this each reload would leave another timer\n * ticking against a detached node.\n */\nexport function initCountdown(container: HTMLElement): void {\n if (container._lbCountdownInterval) {\n clearInterval(container._lbCountdownInterval);\n container._lbCountdownInterval = null;\n }\n\n const countdown = container.querySelector<HTMLElement>(\"[data-countdown]\");\n if (!countdown) return;\n\n const widget = container.closest<HTMLElement>(\".lb-bundle-widget\");\n const endsAt = widget && widget.getAttribute(\"data-ends-at\");\n if (!endsAt) {\n countdown.style.display = \"none\";\n return;\n }\n\n const endTime = new Date(endsAt).getTime();\n if (isNaN(endTime)) {\n countdown.style.display = \"none\";\n return;\n }\n\n const timerEl = countdown.querySelector<HTMLElement>(\"[data-countdown-timer]\");\n if (!timerEl) return;\n\n let interval: ReturnType<typeof setInterval> | undefined;\n\n function update(): void {\n const remaining = endTime - Date.now();\n if (remaining <= 0) {\n if (widget) widget.style.display = \"none\";\n if (interval) clearInterval(interval);\n return;\n }\n const days = Math.floor(remaining / 86400000);\n const hours = Math.floor((remaining % 86400000) / 3600000);\n const mins = Math.floor((remaining % 3600000) / 60000);\n const secs = Math.floor((remaining % 60000) / 1000);\n timerEl!.textContent =\n days > 0\n ? days + \"d \" + pad(hours) + \"h \" + pad(mins) + \"m \" + pad(secs) + \"s\"\n : pad(hours) + \"h \" + pad(mins) + \"m \" + pad(secs) + \"s\";\n }\n\n update();\n if (endTime - Date.now() > 0) {\n interval = setInterval(update, 1000);\n container._lbCountdownInterval = interval;\n }\n}\n","import type { PickerTranslations } from \"./picker/types\";\n\n/**\n * English fallbacks for hosts without a translation system.\n *\n * The theme app extension never uses these — Shopify's `t` filter has already\n * resolved every string into the data block by the time the render layer runs,\n * in the shopper's language. The headless element has no equivalent, so this\n * is what it ships with, and a consumer can override any of it.\n *\n * `__COUNT__`-style placeholders match the Liquid locale files, so the two sets\n * are interchangeable.\n */\nexport interface BundleStrings extends PickerTranslations {\n outOfStock?: string;\n /** \"__COUNT__ items out of stock\". Either one template for every count, or\n * plural forms keyed by Intl.PluralRules category so \"1 items\" can't\n * happen. */\n itemsOutOfStock?: string | Record<string, string>;\n addToCart?: string;\n bundlePrice?: string;\n save?: string;\n complete?: string;\n moreToGo?: string;\n bundleComplete?: string;\n chooseMoreUnlock?: string;\n chooseMoreComplete?: string;\n editSelection?: string;\n stepOf?: string;\n stepsCompleted?: string;\n doorChoose?: string;\n doorContinue?: string;\n doorQuickSteps?: string | Record<string, string>;\n doorStepsToGo?: Record<string, string>;\n each?: string;\n buyQty?: string;\n /** Plural forms for the summary \"(N items)\" label, keyed by\n * Intl.PluralRules category — the shape `formatItemCount` expects and the\n * Liquid `item_count` translation carries. */\n itemCount?: Record<string, string>;\n}\n\nexport const DEFAULT_STRINGS: BundleStrings = {\n locale: \"en\",\n addItem: \"Add\",\n addedItem: \"Added\",\n removeItem: \"Remove\",\n swapItem: \"Swap\",\n required: \"Required\",\n soldOut: \"Sold out\",\n outOfStock: \"Out of stock\",\n itemsOutOfStock: {\n one: \"__COUNT__ item out of stock\",\n other: \"__COUNT__ items out of stock\",\n },\n quantity: \"Quantity\",\n increaseQuantity: \"Increase quantity\",\n decreaseQuantity: \"Decrease quantity\",\n allTypes: \"All\",\n editSelection: \"Edit selection\",\n addToCart: \"Add to cart\",\n bundlePrice: \"Bundle price\",\n save: \"Save\",\n complete: \"Complete\",\n ofSelected: \"__COUNT__ of __TOTAL__ added\",\n moreToGo: \"__COUNT__ more to go\",\n nProductsShown: \"__COUNT__ products shown\",\n bundleComplete: \"Your bundle is complete\",\n chooseMoreUnlock: \"Choose __COUNT__ more to unlock __PCT__% off\",\n chooseMoreComplete: \"Choose __COUNT__ more to complete your bundle\",\n stepOf: \"Step __STEP__ of __TOTAL__\",\n stepsCompleted: \"__COUNT__ of __TOTAL__ steps completed\",\n doorChoose: \"Choose your products\",\n doorContinue: \"Continue building\",\n doorQuickSteps: {\n one: \"__COUNT__ quick step\",\n other: \"__COUNT__ quick steps\",\n },\n doorStepsToGo: {\n one: \"__COUNT__ step to go\",\n other: \"__COUNT__ steps to go\",\n },\n each: \" each\",\n buyQty: \"Buy __COUNT__\",\n itemCount: {\n one: \"__COUNT__ item\",\n other: \"__COUNT__ items\",\n },\n needsSpots: {\n one: \"Needs __COUNT__ spot\",\n other: \"Needs __COUNT__ spots\",\n },\n};\n\n/**\n * Resolve the disabled-CTA \"N items out of stock\" label for a count,\n * accepting either the single-template or the plural-map shape of\n * `itemsOutOfStock`.\n */\nexport function formatItemsOutOfStock(\n t: BundleStrings,\n count: number,\n): string {\n const raw = t.itemsOutOfStock;\n let tpl: string | undefined;\n if (raw && typeof raw === \"object\") {\n const form = new Intl.PluralRules(t.locale || \"en\").select(count);\n tpl = raw[form] || raw.other;\n } else {\n tpl = raw;\n }\n return (tpl || \"__COUNT__ items out of stock\")\n .split(\"__COUNT__\")\n .join(String(count));\n}\n","/**\n * Nearest ancestor that clips overflow on the Y axis.\n *\n * The listbox flips upward before it would be hidden behind a scrollable\n * container such as `.lb-fixed__products`. Stops at `<body>` — past that the\n * viewport bound is the correct constraint.\n */\nexport function findScrollableAncestor(el: HTMLElement): HTMLElement | null {\n const doc = el.ownerDocument;\n const win = doc && doc.defaultView;\n if (!win) return null;\n\n let cur = el.parentElement;\n while (cur && cur !== doc.body) {\n const overflowY = win.getComputedStyle(cur).overflowY;\n if (overflowY === \"auto\" || overflowY === \"scroll\" || overflowY === \"hidden\") {\n return cur;\n }\n cur = cur.parentElement;\n }\n return null;\n}\n","export interface DropdownInstance {\n shell: HTMLElement;\n listbox: HTMLElement;\n select: HTMLSelectElement;\n close: () => void;\n destroy: () => void;\n}\n\n/**\n * Every currently open dropdown, newest last.\n *\n * Module-level so document listeners are attached once for the page rather\n * than once per dropdown, and torn down again when the last one closes.\n */\nconst openInstances: DropdownInstance[] = [];\nlet documentListenersAttached = false;\n\n/**\n * Whether an event originated inside a dropdown.\n *\n * Checks both shell and listbox because the listbox is portaled to the modal\n * overlay in the mix-and-match picker and so is not a descendant of the shell.\n * `composedPath` also handles shadow-DOM retargeting, should a theme ever wrap\n * the widget in a custom element.\n */\nfunction eventInsideDropdown(event: Event, inst: DropdownInstance): boolean {\n if (event.composedPath) {\n const path = event.composedPath();\n for (let p = 0; p < path.length; p++) {\n if (path[p] === inst.shell || path[p] === inst.listbox) return true;\n }\n return false;\n }\n const target = event.target as Node;\n return inst.shell.contains(target) || inst.listbox.contains(target);\n}\n\nfunction closeAllOutside(event: Event): void {\n for (let i = openInstances.length - 1; i >= 0; i--) {\n if (!eventInsideDropdown(event, openInstances[i])) openInstances[i].close();\n }\n}\n\nfunction onWindowResize(): void {\n for (let i = openInstances.length - 1; i >= 0; i--) openInstances[i].close();\n}\n\nfunction attachDocListeners(): void {\n if (documentListenersAttached) return;\n document.addEventListener(\"pointerdown\", closeAllOutside, true);\n window.addEventListener(\"resize\", onWindowResize);\n // Capture phase, because scroll doesn't bubble. Scrolling the listbox\n // itself is excluded by eventInsideDropdown — the panel has overflow-y:auto\n // for long variant lists and must not close itself.\n window.addEventListener(\"scroll\", closeAllOutside, true);\n documentListenersAttached = true;\n}\n\nfunction detachDocListeners(): void {\n if (!documentListenersAttached) return;\n document.removeEventListener(\"pointerdown\", closeAllOutside, true);\n window.removeEventListener(\"resize\", onWindowResize);\n window.removeEventListener(\"scroll\", closeAllOutside, true);\n documentListenersAttached = false;\n}\n\n/** Register a newly opened dropdown, attaching page listeners if it's the first. */\nexport function registerOpen(instance: DropdownInstance): void {\n openInstances.push(instance);\n if (openInstances.length === 1) attachDocListeners();\n}\n\n/** Deregister a closed dropdown, detaching page listeners if it was the last. */\nexport function registerClosed(instance: DropdownInstance): void {\n const idx = openInstances.indexOf(instance);\n if (idx >= 0) openInstances.splice(idx, 1);\n if (openInstances.length === 0) detachDocListeners();\n}\n\n/** Close every open dropdown except one. Only a single dropdown may be open. */\nexport function closeOthers(except: DropdownInstance | undefined): void {\n for (let i = openInstances.length - 1; i >= 0; i--) {\n if (openInstances[i] !== except) openInstances[i].close();\n }\n}\n","// Namespaced in core to keep its top-level surface clean.\nimport { dropdown } from \"@lime-bundles/core\";\nimport { findScrollableAncestor } from \"./scroll\";\nimport {\n closeOthers,\n registerClosed,\n registerOpen,\n type DropdownInstance,\n} from \"./open-registry\";\n\n/**\n * Listbox layout constants.\n *\n * Deliberately local rather than shared from `@lime-bundles/core`: the\n * algorithm is the contract, these numbers are this surface's styling and are\n * expected to differ between the theme widget and the headless renderer.\n * Must match `.lb-dropdown-option` in bundle-dropdown.css.\n */\nconst ITEM_HEIGHT = 32;\nconst LIST_PADDING_Y = 8;\nconst MAX_VISIBLE_ITEMS = 8;\n\n/** Instance per bound `<select>`, so rebinding and teardown can find it. */\nconst instances = new WeakMap<HTMLSelectElement, DropdownInstance>();\n\nexport function instanceFor(\n select: HTMLSelectElement,\n): DropdownInstance | undefined {\n return instances.get(select);\n}\n\ninterface OptionState {\n disabled: boolean;\n label: string;\n}\n\n/**\n * Replace a native `<select>` with an accessible custom listbox.\n *\n * The original element stays in the DOM as the form value and the source of\n * truth — every commit writes back to it and dispatches `change`, so anything\n * listening to the select (the variant pickers, the theme) is unaffected by\n * the swap.\n */\nexport function bindDropdown(\n selectEl: HTMLSelectElement,\n): DropdownInstance | null {\n if (selectEl.classList.contains(\"lb-dropdown-state\")) return null;\n\n const doc = document;\n const labelText = selectEl.getAttribute(\"aria-label\") || \"\";\n const idBase = \"lb-dd-\" + Math.random().toString(36).slice(2, 9);\n\n selectEl.classList.add(\"lb-dropdown-state\");\n selectEl.setAttribute(\"aria-hidden\", \"true\");\n selectEl.setAttribute(\"tabindex\", \"-1\");\n\n const shell = doc.createElement(\"div\");\n shell.className = \"lb-dropdown\";\n shell.setAttribute(\"data-lb-dropdown\", \"\");\n\n const trigger = doc.createElement(\"button\");\n trigger.type = \"button\";\n trigger.className = \"lb-dropdown-trigger\";\n trigger.setAttribute(\"role\", \"combobox\");\n trigger.setAttribute(\"aria-haspopup\", \"listbox\");\n trigger.setAttribute(\"aria-expanded\", \"false\");\n\n const listboxId = idBase + \"-listbox\";\n trigger.setAttribute(\"aria-controls\", listboxId);\n if (labelText) trigger.setAttribute(\"aria-label\", labelText);\n\n const triggerLabel = doc.createElement(\"span\");\n triggerLabel.className = \"lb-dropdown-trigger-value\";\n\n const chevron = doc.createElement(\"span\");\n chevron.className = \"lb-dropdown-chevron\";\n chevron.setAttribute(\"aria-hidden\", \"true\");\n\n trigger.appendChild(triggerLabel);\n trigger.appendChild(chevron);\n\n const listbox = doc.createElement(\"ul\");\n listbox.id = listboxId;\n listbox.className = \"lb-dropdown-listbox\";\n listbox.setAttribute(\"role\", \"listbox\");\n if (labelText) listbox.setAttribute(\"aria-label\", labelText);\n listbox.hidden = true;\n\n shell.appendChild(trigger);\n selectEl.parentNode?.insertBefore(shell, selectEl.nextSibling);\n\n // The mix-and-match modal animates in with translateY, which makes\n // position:fixed resolve against the modal rather than the viewport. Portal\n // the listbox up to the overlay — it carries the per-bundle --lb-*\n // variables and has no transform of its own. Outside a modal there is no\n // transformed ancestor to escape, so leaving it in the shell keeps teardown\n // simple for no loss.\n const modalOverlay = selectEl.closest<HTMLElement>(\"[data-modal-overlay]\");\n if (modalOverlay) {\n modalOverlay.appendChild(listbox);\n listbox.setAttribute(\"data-lb-dropdown-portal\", \"\");\n } else {\n shell.appendChild(listbox);\n }\n\n let isOpen = false;\n let activeIndex = -1;\n let typeAhead: dropdown.TypeAheadState = dropdown.emptyTypeAheadState();\n let optionEls: HTMLLIElement[] = [];\n\n function readOptionsState(): OptionState[] {\n const sel: OptionState[] = [];\n for (let i = 0; i < selectEl.options.length; i++) {\n const o = selectEl.options[i];\n sel.push({ disabled: o.disabled, label: o.textContent || o.value });\n }\n return sel;\n }\n\n function syncFromSelect(): void {\n // Mirror the select's disabled state onto the trigger. Without this a row\n // that disables its select — mix-and-match products already in the bundle\n // — leaves the custom trigger focusable and clickable, so the visible\n // chrome disagrees with the form control.\n trigger.disabled = selectEl.disabled;\n\n const sel = readOptionsState();\n const idx = selectEl.selectedIndex;\n triggerLabel.textContent = idx >= 0 && sel[idx] ? sel[idx].label : \"\";\n\n // Rebuilt via removeChild rather than innerHTML so option labels can\n // never be an injection point.\n while (listbox.firstChild) listbox.removeChild(listbox.firstChild);\n optionEls = [];\n\n for (let i = 0; i < sel.length; i++) {\n const li = doc.createElement(\"li\");\n li.id = idBase + \"-opt-\" + i;\n li.className = \"lb-dropdown-option\";\n li.setAttribute(\"role\", \"option\");\n li.setAttribute(\"aria-selected\", i === idx ? \"true\" : \"false\");\n if (sel[i].disabled) li.setAttribute(\"aria-disabled\", \"true\");\n li.setAttribute(\"data-value\", selectEl.options[i].value);\n li.setAttribute(\"data-index\", String(i));\n li.textContent = sel[i].label;\n listbox.appendChild(li);\n optionEls.push(li);\n }\n }\n\n function updateActive(newIndex: number): void {\n if (activeIndex >= 0 && optionEls[activeIndex]) {\n optionEls[activeIndex].classList.remove(\"is-active\");\n }\n activeIndex = newIndex;\n\n if (newIndex < 0 || !optionEls[newIndex]) {\n trigger.setAttribute(\"aria-activedescendant\", \"\");\n return;\n }\n\n const li = optionEls[newIndex];\n li.classList.add(\"is-active\");\n trigger.setAttribute(\"aria-activedescendant\", li.id);\n\n // Scrolled by hand rather than with scrollIntoView: when the list is\n // shorter than its max-height there is nothing to scroll, and\n // scrollIntoView falls through to the next scrolling ancestor and yanks\n // the whole page.\n const liTop = li.offsetTop;\n const liBottom = liTop + li.offsetHeight;\n const visTop = listbox.scrollTop;\n const visBottom = visTop + listbox.clientHeight;\n if (liTop < visTop) {\n listbox.scrollTop = liTop;\n } else if (liBottom > visBottom) {\n listbox.scrollTop = liBottom - listbox.clientHeight;\n }\n }\n\n /** Returns false when the trigger has no layout yet and positioning must retry. */\n function position(): boolean {\n const rect = trigger.getBoundingClientRect();\n if (rect.width === 0) return false;\n\n const visibleCount = Math.min(optionEls.length || 1, MAX_VISIBLE_ITEMS);\n const desiredHeight = visibleCount * ITEM_HEIGHT + LIST_PADDING_Y;\n\n // Always clip to the trigger's nearest scrollable ancestor. In-shell that\n // is the bundle's product list, so the panel flips up before hiding\n // behind the widget footer. Portaled, it is the modal's scrollable list,\n // so the panel flips up near the modal's bottom edge even when there is\n // viewport room below — without this a fixed-position listbox on the\n // overlay would open downward and overflow the modal.\n const scrollable = findScrollableAncestor(trigger);\n const clipRect = scrollable\n ? {\n top: scrollable.getBoundingClientRect().top,\n bottom: scrollable.getBoundingClientRect().bottom,\n }\n : undefined;\n\n const pos = dropdown.computePosition({\n trigger: {\n top: rect.top,\n bottom: rect.bottom,\n left: rect.left,\n width: rect.width,\n },\n viewportHeight: window.innerHeight,\n desiredHeight,\n clip: clipRect,\n });\n\n listbox.setAttribute(\"data-placement\", pos.placement);\n listbox.style.maxHeight = pos.maxHeight + \"px\";\n\n // In-shell, CSS handles top/left/width via [data-placement] against the\n // position:relative shell. Only the portaled case needs viewport coords.\n if (listbox.hasAttribute(\"data-lb-dropdown-portal\")) {\n listbox.style.top = pos.offsetTop + \"px\";\n listbox.style.left = pos.offsetLeft + \"px\";\n listbox.style.width = pos.width + \"px\";\n }\n return true;\n }\n\n function open(): void {\n if (isOpen) return;\n closeOthers(instance);\n\n isOpen = true;\n listbox.hidden = false;\n trigger.setAttribute(\"aria-expanded\", \"true\");\n\n if (!position()) {\n requestAnimationFrame(() => {\n position();\n });\n }\n\n const sel = readOptionsState();\n const selIdx = selectEl.selectedIndex;\n if (selIdx >= 0 && sel[selIdx] && !sel[selIdx].disabled) {\n updateActive(selIdx);\n } else {\n updateActive(dropdown.firstEnabled(sel));\n }\n\n registerOpen(instance);\n }\n\n function close(restoreFocus: boolean): void {\n if (!isOpen) return;\n isOpen = false;\n listbox.hidden = true;\n trigger.setAttribute(\"aria-expanded\", \"false\");\n trigger.setAttribute(\"aria-activedescendant\", \"\");\n if (activeIndex >= 0 && optionEls[activeIndex]) {\n optionEls[activeIndex].classList.remove(\"is-active\");\n }\n activeIndex = -1;\n\n registerClosed(instance);\n if (restoreFocus) trigger.focus();\n }\n\n function commit(index: number): void {\n const opt = selectEl.options[index];\n if (!opt || opt.disabled) return;\n if (selectEl.value !== opt.value) {\n selectEl.value = opt.value;\n selectEl.dispatchEvent(new Event(\"change\", { bubbles: true }));\n }\n syncFromSelect();\n close(true);\n }\n\n function onKeydown(event: KeyboardEvent): void {\n const optsState = readOptionsState();\n const action = dropdown.handleKey(\n {\n key: event.key,\n ctrlKey: event.ctrlKey,\n metaKey: event.metaKey,\n altKey: event.altKey,\n shiftKey: event.shiftKey,\n },\n {\n isOpen,\n activeIndex,\n selectedIndex: selectEl.selectedIndex,\n options: optsState,\n },\n );\n\n if (action.preventDefault) event.preventDefault();\n\n switch (action.type) {\n case \"open\":\n open();\n if (action.activeIndex >= 0) updateActive(action.activeIndex);\n break;\n case \"close\":\n close(action.restoreFocus);\n break;\n case \"move-active\":\n updateActive(action.activeIndex);\n break;\n case \"commit\":\n commit(action.index);\n break;\n case \"type-ahead\": {\n const r = dropdown.pushTypeAheadChar(\n typeAhead,\n action.char,\n Date.now(),\n optsState,\n );\n typeAhead = r.newState;\n if (r.matchedIndex !== null) {\n if (!isOpen) open();\n updateActive(r.matchedIndex);\n }\n break;\n }\n }\n }\n\n function onTriggerClick(event: MouseEvent): void {\n event.preventDefault();\n if (isOpen) close(false);\n else open();\n }\n\n /** Walk up from the event target to the option element, if any. */\n function optionIndexFrom(event: Event): number | null {\n let target = event.target as HTMLElement | null;\n while (target && target !== listbox) {\n if (target.classList && target.classList.contains(\"lb-dropdown-option\")) {\n const idx = parseInt(target.getAttribute(\"data-index\") ?? \"\", 10);\n return isNaN(idx) ? null : idx;\n }\n target = target.parentNode as HTMLElement | null;\n }\n return null;\n }\n\n function onListboxClick(event: MouseEvent): void {\n const idx = optionIndexFrom(event);\n if (idx !== null) commit(idx);\n }\n\n function onListboxMousemove(event: MouseEvent): void {\n let target = event.target as HTMLElement | null;\n while (target && target !== listbox) {\n if (target.classList && target.classList.contains(\"lb-dropdown-option\")) {\n if (target.getAttribute(\"aria-disabled\") === \"true\") return;\n const idx = parseInt(target.getAttribute(\"data-index\") ?? \"\", 10);\n if (!isNaN(idx) && idx !== activeIndex) updateActive(idx);\n return;\n }\n target = target.parentNode as HTMLElement | null;\n }\n }\n\n function onShellFocusout(): void {\n // Deferred past the focus transition so document.activeElement is settled.\n setTimeout(() => {\n if (!isOpen) return;\n if (!shell.contains(document.activeElement)) close(false);\n }, 0);\n }\n\n /** Something else set select.value — resync the visible chrome. */\n function onSelectChange(): void {\n syncFromSelect();\n }\n\n // Cascading availability changes rewrite the option list, so rebuild.\n const observer = new MutationObserver(() => {\n syncFromSelect();\n });\n observer.observe(selectEl, {\n childList: true,\n subtree: true,\n attributes: true,\n attributeFilter: [\"disabled\", \"value\", \"selected\"],\n });\n\n /**\n * Stop mousedown on a non-focusable <li> from blurring the trigger.\n *\n * Without this, focusout fires on the shell and queues a setTimeout(0) that\n * closes the dropdown. On desktop that close runs before the synthesised\n * click, so onListboxClick never sees the option and the commit is lost.\n * Mobile is unaffected because touchstart doesn't move focus.\n */\n function onListboxMousedown(event: MouseEvent): void {\n event.preventDefault();\n }\n\n trigger.addEventListener(\"click\", onTriggerClick);\n trigger.addEventListener(\"keydown\", onKeydown);\n shell.addEventListener(\"focusout\", onShellFocusout);\n listbox.addEventListener(\"mousedown\", onListboxMousedown);\n listbox.addEventListener(\"click\", onListboxClick);\n listbox.addEventListener(\"mousemove\", onListboxMousemove);\n selectEl.addEventListener(\"change\", onSelectChange);\n\n function destroy(): void {\n if (isOpen) close(false);\n observer.disconnect();\n trigger.removeEventListener(\"click\", onTriggerClick);\n trigger.removeEventListener(\"keydown\", onKeydown);\n shell.removeEventListener(\"focusout\", onShellFocusout);\n listbox.removeEventListener(\"mousedown\", onListboxMousedown);\n listbox.removeEventListener(\"click\", onListboxClick);\n listbox.removeEventListener(\"mousemove\", onListboxMousemove);\n selectEl.removeEventListener(\"change\", onSelectChange);\n shell.parentNode?.removeChild(shell);\n listbox.parentNode?.removeChild(listbox);\n selectEl.classList.remove(\"lb-dropdown-state\");\n selectEl.removeAttribute(\"aria-hidden\");\n selectEl.removeAttribute(\"tabindex\");\n instances.delete(selectEl);\n }\n\n // Declared last but captured by the handlers above; they only run after\n // binding completes, so the closures always see an initialised value.\n const instance: DropdownInstance = {\n shell,\n listbox,\n select: selectEl,\n close: () => close(false),\n destroy,\n };\n instances.set(selectEl, instance);\n\n syncFromSelect();\n return instance;\n}\n","import { bindDropdown, instanceFor } from \"./bind\";\n\n/**\n * The variant `<select>` classes the auto-binder attaches to.\n *\n * Single source of truth — add another bundle type's class here if it wants\n * the custom dropdown.\n */\nexport const BIND_SELECTOR =\n \"select.lb-bundle-variant-select:not(.lb-dropdown-state), \" +\n \"select.lb-mix-match__variant-select:not(.lb-dropdown-state)\";\n\n/** Roots already under observation, so re-init doesn't stack observers. */\nconst observedRoots = new WeakSet<HTMLElement>();\n\nexport function bindAll(root?: ParentNode | null): void {\n const scope = root ?? document;\n const selects = scope.querySelectorAll<HTMLSelectElement>(BIND_SELECTOR);\n for (let i = 0; i < selects.length; i++) bindDropdown(selects[i]);\n}\n\nexport function unbindAll(root?: ParentNode | null): void {\n const scope = root ?? document;\n const bound = scope.querySelectorAll<HTMLSelectElement>(\n \"select.lb-dropdown-state\",\n );\n for (let i = 0; i < bound.length; i++) {\n instanceFor(bound[i])?.destroy();\n }\n}\n\n/**\n * Bind dropdowns to selects added after load — mix-and-match slots, picker\n * modal rows, and theme editor section reloads all inject markup late.\n */\nexport function observeRoot(rootEl: HTMLElement | null): void {\n if (!rootEl || observedRoots.has(rootEl)) return;\n observedRoots.add(rootEl);\n\n new MutationObserver((mutations) => {\n for (let m = 0; m < mutations.length; m++) {\n const added = mutations[m].addedNodes;\n for (let n = 0; n < added.length; n++) {\n if (added[n].nodeType !== 1) continue;\n const node = added[n] as HTMLElement;\n if (node.matches && node.matches(BIND_SELECTOR)) {\n bindDropdown(node as HTMLSelectElement);\n } else if (node.querySelectorAll) {\n const found = node.querySelectorAll<HTMLSelectElement>(BIND_SELECTOR);\n for (let f = 0; f < found.length; f++) bindDropdown(found[f]);\n }\n }\n }\n }).observe(rootEl, { childList: true, subtree: true });\n}\n","import { bindAll, unbindAll } from \"@lime-bundles/render/dropdown/auto-bind\";\n\n/**\n * Bind the custom variant dropdown inside a rendered bundle, and return the\n * teardown.\n *\n * The theme host binds through `window.LimeBundles.Dropdown` because its\n * scripts are separate `<script>` tags with no module graph between them. This\n * host imports the same module directly — one implementation, two ways of\n * reaching it.\n */\nexport function bindDropdowns(root: HTMLElement): () => void {\n bindAll(root);\n return () => unbindAll(root);\n}\n","import type { CartLineInput, FixedBundleData } from \"@lime-bundles/core\";\nimport { adaptProducts } from \"@lime-bundles/render/adapt\";\nimport { buildCartItems } from \"@lime-bundles/render/fixed/cart\";\nimport { recalcPricing } from \"@lime-bundles/render/fixed/pricing\";\nimport { bundleLineAttributes, intlFormatMoney } from \"@lime-bundles/render/host\";\nimport { bindVariantSelects } from \"@lime-bundles/render/product/option-selects\";\nimport { hydrateRowControls } from \"@lime-bundles/render/product/hydrate\";\nimport { applyVariant } from \"@lime-bundles/render/product/row\";\nimport {\n findVariant,\n resolveSellableVariant,\n} from \"@lime-bundles/render/product/variants\";\nimport { buildRowSkeleton, buildWidgetShell } from \"@lime-bundles/render/skeleton\";\nimport { initCountdown } from \"@lime-bundles/render/countdown\";\nimport { DEFAULT_STRINGS, formatItemsOutOfStock } from \"@lime-bundles/render/strings\";\nimport { bindDropdowns } from \"./dropdown-host\";\n\n/**\n * Fixed bundle, headless.\n *\n * This host has no Liquid, so it builds the skeleton the theme's Liquid emits\n * and then runs the identical hydration path — `hydrateRowControls`,\n * `applyVariant`, `bindVariantSelects`, `recalcPricing`. Everything below the\n * skeleton is shared code; what is here is only the difference between having\n * a server template and not.\n */\nexport function renderFixedBundle(\n container: HTMLElement,\n bundle: FixedBundleData,\n onAddToCart: (lines: CartLineInput[]) => void,\n onCleanup?: (fn: () => void) => void,\n): void {\n const wc = bundle.widgetConfig;\n const products = adaptProducts(bundle);\n if (!products.length) return;\n\n // A fixed bundle is all-or-nothing: sold-out rows render greyed with the\n // CTA locked, and the status flip hides a bundle that can't be bought.\n const oosProducts = products.filter((p) => !p.variants.some((v) => v.available));\n\n const currency =\n bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? \"USD\";\n const formatMoney = intlFormatMoney(currency);\n const t = DEFAULT_STRINGS;\n\n const shell = buildWidgetShell(\"lb-fixed\", {\n title: bundle.title,\n subtitle: bundle.description,\n showSavingsBar: wc.savingsBar?.visible !== false,\n showComparePrice: wc.pricing?.showCompareAtPrice !== false,\n ctaText: wc.cta?.ctaText || t.addToCart || \"Add to cart\",\n endsAt: wc.countdown?.showCountdown === false ? null : bundle.endsAt,\n });\n shell.root.setAttribute(\"data-discount-type\", bundle.discountConfig.discountType);\n shell.root.setAttribute(\n \"data-discount-value\",\n String(bundle.discountConfig.discountValue),\n );\n\n for (const product of products) {\n const isOos = !product.variants.some((v) => v.available);\n const row = buildRowSkeleton(product, t, {\n isOos,\n href: product.url,\n thumbnailRatio: wc.productList?.thumbnailRatio,\n });\n shell.products.appendChild(row);\n\n if (isOos) continue;\n\n // adaptProducts already seeds a sellable variant; resolving again keeps\n // this host correct if the seed and the availability flags ever diverge,\n // the same guarantee the theme host makes.\n const sellable = resolveSellableVariant(product);\n if (sellable) product.selectedVariantId = sellable.id;\n const selected = sellable ?? findVariant(product, product.selectedVariantId);\n hydrateRowControls(row, product, selected);\n applyVariant(row, selected, product, formatMoney);\n\n bindVariantSelects(\n row,\n () => product,\n (variant, p) => {\n applyVariant(row, variant, p, formatMoney);\n recalcPricing(shell.root, products, formatMoney);\n },\n );\n }\n\n // A product that can't be fulfilled locks the CTA rather than hiding it, so\n // the shopper is told why instead of finding a button that does nothing.\n if (oosProducts.length > 0) {\n shell.cta.disabled = true;\n const label = shell.cta.querySelector(\"[data-cta-label]\");\n if (label) {\n label.textContent = formatItemsOutOfStock(t, oosProducts.length);\n }\n } else {\n shell.cta.addEventListener(\"click\", () => {\n onAddToCart(\n buildCartItems(products).map((item) => ({\n merchandiseId: \"gid://shopify/ProductVariant/\" + item.variantId,\n quantity: item.quantity,\n attributes: bundleLineAttributes(bundle.id, bundle.bundleType),\n })),\n );\n });\n }\n\n container.appendChild(shell.root);\n initCountdown(shell.root);\n recalcPricing(shell.root, products, formatMoney);\n\n onCleanup?.(bindDropdowns(shell.root));\n}\n","import type { PickerTranslations } from \"./types\";\n\n/** Substitute `__NAME__` placeholders in a translated template. */\nexport function fill(\n template: string,\n values: Record<string, string | number>,\n): string {\n let out = template;\n for (const key in values) {\n out = out.split(\"__\" + key + \"__\").join(String(values[key]));\n }\n return out;\n}\n\n/**\n * \"Needs N spots\" hint, pluralised through Intl.PluralRules against the\n * translations' per-form map — the same mechanism the volume widget's item\n * count uses, so languages with more than two plural forms read correctly.\n */\nexport function formatNeedsSpots(\n min: number,\n t: PickerTranslations,\n): string {\n const map = t.needsSpots;\n if (!map) return \"Needs \" + min + \" spots\";\n const form = new Intl.PluralRules(t.locale || \"en\").select(min);\n return fill(map[form] || map.other || \"__COUNT__\", { COUNT: min });\n}\n","import { fill } from \"../picker/i18n\";\nimport type { MixMatchTranslations } from \"./types\";\n\nexport { formatNeedsSpots } from \"../picker/i18n\";\n\nexport function formatOfSelected(\n count: number,\n total: number,\n t: MixMatchTranslations,\n): string {\n return fill(t.ofSelected || \"__COUNT__ of __TOTAL__ added\", {\n COUNT: count,\n TOTAL: total,\n });\n}\n\nexport function formatMoreToGo(\n remaining: number,\n t: MixMatchTranslations,\n): string {\n return remaining > 0\n ? fill(t.moreToGo || \"__COUNT__ more to go\", { COUNT: remaining })\n : t.complete || \"Complete\";\n}\n\n/**\n * Modal subtitle: names the discount when there is a percentage to advertise,\n * otherwise just counts down the remaining picks.\n */\nexport function formatSubtitle(\n remaining: number,\n discountType: string | undefined,\n discountValue: number | undefined,\n t: MixMatchTranslations,\n): string {\n if (remaining <= 0) return t.bundleComplete || \"Your bundle is complete\";\n\n const pct =\n discountType === \"percentage\" && discountValue\n ? Math.round(discountValue)\n : 0;\n\n return pct > 0\n ? fill(t.chooseMoreUnlock || \"Choose __COUNT__ more to unlock __PCT__% off\", {\n COUNT: remaining,\n PCT: pct,\n })\n : fill(\n t.chooseMoreComplete || \"Choose __COUNT__ more to complete your bundle\",\n { COUNT: remaining },\n );\n}\n","/**\n * Rule primitives shared by every picker-based bundle type.\n *\n * Step-agnostic on purpose: mix & match applies these to one flat selection,\n * multi-step applies them per step. Anything that needs to know about steps\n * or a bundle-wide required quantity lives in the type's own rules module.\n */\n\n/** Per-product unit rule from the bundle's `productRules` map. */\nexport interface ProductRule {\n min: number;\n max: number;\n required?: boolean;\n}\n\n/** Applied to products with no explicit entry (collection-sourced, mostly). */\nexport const DEFAULT_RULE: ProductRule = { min: 1, max: 99 };\n\n/** One committed pick. Slots are variant-scoped: one per (product, variant). */\nexport interface SelectedItem {\n productId: number | string;\n variantId: number | string;\n title: string;\n url: string | null;\n variantTitle: string;\n featuredImage: string | null;\n price: number;\n compareAtPrice: number | null;\n unitPrice: string | null;\n quantity: number;\n}\n\n/**\n * The rule lookup, keyed by GID because that is what the metaobject stores\n * while the picker works in numeric IDs.\n *\n * One map holds both levels. Product GIDs and variant GIDs are different\n * namespaces (`gid://shopify/Product/…` vs `gid://shopify/ProductVariant/…`)\n * so they cannot collide, and merging them at the host boundary — see\n * `mergeRuleMaps` — means every function that already threads `rulesMap`\n * keeps working untouched. Nothing iterates this map; it is only ever read by\n * key.\n */\nexport type RulesMap = Record<string, ProductRule | undefined>;\n\n/**\n * Rule for a pick.\n *\n * Pass `variantId` wherever the caller knows which variant it is talking\n * about, and the variant's own rule wins. Without one — a product row before\n * the shopper has chosen a variant — this resolves the product rule, which is\n * the envelope covering every variant.\n *\n * A malformed entry falls back to the default rather than throwing at either\n * level: the map comes from merchant-authored data via Liquid.\n */\nexport function ruleFor(\n productId: number | string,\n rulesMap: RulesMap,\n variantId?: number | string | null,\n): ProductRule {\n if (variantId != null) {\n const vRule = rulesMap[\"gid://shopify/ProductVariant/\" + variantId];\n if (vRule && typeof vRule.min === \"number\" && typeof vRule.max === \"number\") {\n return vRule;\n }\n }\n const rule = rulesMap[\"gid://shopify/Product/\" + productId];\n if (!rule || typeof rule.min !== \"number\" || typeof rule.max !== \"number\") {\n return DEFAULT_RULE;\n }\n return rule;\n}\n\n/**\n * The variant's OWN rule, or undefined when it inherits the product's.\n *\n * Distinct from `ruleFor(pid, map, vid)`, which falls back to the product\n * rule. Callers that must know which level a rule came from need this one:\n * \"is this variant required\" and \"is this variant's product required\" are\n * different questions with different answers, and conflating them would make\n * every variant of a required product unremovable.\n */\nexport function variantRuleFor(\n variantId: number | string | null | undefined,\n rulesMap: RulesMap,\n): ProductRule | undefined {\n if (variantId == null) return undefined;\n const rule = rulesMap[\"gid://shopify/ProductVariant/\" + variantId];\n if (!rule || typeof rule.min !== \"number\" || typeof rule.max !== \"number\") {\n return undefined;\n }\n return rule;\n}\n\n/**\n * Fold optional per-variant rules into the product rule map the hosts already\n * build, so the picker has one lookup rather than two threaded side by side.\n *\n * Returns the product map untouched when there are no variant rules, which is\n * every bundle saved before they existed.\n */\nexport function mergeRuleMaps(\n productRules: RulesMap | undefined,\n variantRules: RulesMap | undefined,\n): RulesMap {\n const products = productRules ?? {};\n if (!variantRules) return products;\n const keys = Object.keys(variantRules);\n if (keys.length === 0) return products;\n const merged: RulesMap = { ...products };\n for (const key of keys) merged[key] = variantRules[key];\n return merged;\n}\n\n/**\n * Default units a single pick contributes.\n *\n * One of five implementations of the bundle quantity fallback in this repo\n * (canonical: packages/core/src/bundle/qty.ts; also the Rust discount function\n * and the metafield producer). A pick starts at the merchant's `min` — the\n * variant's when it has one, else the product's — and the stepper raises it\n * from there. Named rather than inlined so the parity test in\n * packages/core/__tests__/resolve-bundle-qty.test.ts can assert it directly.\n */\nexport function defaultPickQuantity(\n productId: number | string,\n rulesMap: RulesMap,\n variantId?: number | string | null,\n): number {\n return ruleFor(productId, rulesMap, variantId).min;\n}\n\n/** Total units across every slot. Completion counts units, not products. */\nexport function totalUnits(items: SelectedItem[]): number {\n let qty = 0;\n for (let i = 0; i < items.length; i++) qty += items[i].quantity || 1;\n return qty;\n}\n\n/** Index of the slot holding a variant, or -1. */\nexport function indexOfVariantInBundle(\n items: SelectedItem[],\n variantId: number | string,\n): number {\n for (let i = 0; i < items.length; i++) {\n if (items[i] && items[i].variantId === variantId) return i;\n }\n return -1;\n}\n\n/** Committed units of the slot holding this variant, 0 when absent. */\nexport function committedQtyForVariant(\n items: SelectedItem[],\n variantId: number | string,\n): number {\n const idx = indexOfVariantInBundle(items, variantId);\n return idx === -1 ? 0 : items[idx].quantity || 0;\n}\n\n/**\n * Whether an unselected row can be added at all.\n *\n * Exact-size rule: a product's minimum pick must fit the remaining spots, or\n * adding it would overshoot the bundle size on its own.\n */\nexport function isRowAddable(\n rule: ProductRule,\n remainingSpots: number,\n): boolean {\n return rule.min <= remainingSpots;\n}\n\n/**\n * Ceiling for a row's quantity stepper.\n *\n * The lower of what the product's rule still allows (max minus sibling-variant\n * units) and what the bundle has room for (the row's own committed units plus\n * the open spots — its own units already count toward the total, so a\n * committed row edits live within that headroom). Stock, when known, has the\n * final say.\n *\n * `variantMax` is the nested level: the two bind independently, so it is NOT\n * reduced by `productOtherUnits`. Sibling units eat into the product envelope,\n * never into this variant's own allowance — \"max 4 shirts, no more than 2 of\n * them medium\" still permits 2 mediums once two larges are in the bundle.\n */\nexport function stepperCeiling(\n rule: ProductRule,\n remainingSpots: number,\n stock?: number,\n ownQty = 0,\n productOtherUnits = 0,\n variantMax?: number,\n): number {\n let ceiling = Math.min(rule.max - productOtherUnits, ownQty + remainingSpots);\n if (typeof variantMax === \"number\") ceiling = Math.min(ceiling, variantMax);\n return typeof stock === \"number\" ? Math.min(ceiling, stock) : ceiling;\n}\n","/**\n * Responsive picker imagery. Mirrors packages/widget/src/renderers/image.ts.\n *\n * Shopify's CDN caps each request at the master image's native resolution, so\n * the browser picks the smallest srcset candidate that satisfies\n * `sizes` × devicePixelRatio. The smallest candidate is deliberately ~2× the\n * display size rather than 1×, so a DPR-1 browser still gets a 2× image —\n * otherwise the thumbnail looks soft the moment it's viewed on a Retina panel.\n */\nexport const THUMB_WIDTHS = [120, 180, 240];\nexport const PICKER_WIDTHS = [480, 600, 768, 1024];\nexport const THUMB_SIZES = \"60px\";\nexport const PICKER_SIZES = \"(max-width: 767px) 45vw, 228px\";\n\n/** Set or replace the `width` query parameter on a Shopify CDN image URL. */\nexport function imgWidth(url: string, w: number): string {\n if (!url) return url;\n return /[?&]width=\\d+/.test(url)\n ? url.replace(/([?&])width=\\d+/, \"$1width=\" + w)\n : url + (url.indexOf(\"?\") === -1 ? \"?\" : \"&\") + \"width=\" + w;\n}\n\nexport function srcset(url: string, widths: number[]): string {\n if (!url) return \"\";\n return widths.map((w) => imgWidth(url, w) + \" \" + w + \"w\").join(\", \");\n}\n","/** Trailing-edge debounce, used to keep picker search off every keystroke. */\nexport function debounce<A extends unknown[]>(\n fn: (...args: A) => void,\n delay: number,\n): (...args: A) => void {\n let timer: ReturnType<typeof setTimeout> | null = null;\n return function (this: unknown, ...args: A) {\n if (timer) clearTimeout(timer);\n timer = setTimeout(() => fn.apply(this, args), delay);\n };\n}\n\n/**\n * Fold case and strip diacritics so picker search matches \"creme\" against\n * \"Crème\".\n *\n * The range is written as escapes on purpose. The multi-step copy of this\n * function carried the literal combining marks instead, which are invisible in\n * an editor and trivially mangled by anything that renormalises the file.\n */\nexport function normalizeText(str: string | null | undefined): string {\n if (!str) return \"\";\n return str\n .normalize(\"NFD\")\n .replace(/[\\u0300-\\u036f]/g, \"\")\n .toLowerCase();\n}\n","import {\n isRowAddable,\n ruleFor,\n stepperCeiling,\n variantRuleFor,\n type ProductRule,\n} from \"./rules\";\nimport { imgWidth, srcset, PICKER_SIZES, PICKER_WIDTHS } from \"./images\";\nimport { formatNeedsSpots } from \"./i18n\";\nimport { normalizeText } from \"../utils\";\nimport type {\n EligibleProduct,\n EligibleVariant,\n PickerTranslations,\n} from \"./types\";\n\n/**\n * How a row asks about capacity.\n *\n * Mix & match answers against one flat selection and a bundle-wide required\n * quantity; multi-step answers against the current step. The row's own logic —\n * stepper bounds, Add/Remove/Swap state, the needs-spots hint — is identical\n * either way, which is why it is written once and the scoping is injected.\n */\nexport interface RowCapacity {\n /** Open units the row may still claim. */\n remainingSpots(): number;\n /** Units of this variant already committed to the row's own slot. */\n ownQty(variantId: number | string): number;\n /** Units of this variant held by any other slot; they draw the same stock. */\n variantUnitsElsewhere(variantId: number | string): number;\n /** Units of this product held elsewhere; rule.max caps the product, not the slot. */\n productUnitsElsewhere(\n productId: number | string,\n variantId: number | string,\n ): number;\n /** Units an atomic swap can displace, or 0 when the row isn't in that state. */\n swapUnits(productId: number | string, variantId: number | string): number;\n isInBundle(variantId: number | string): boolean;\n /** Whether the committed pick for this variant may be removed. */\n canRemove(variantId: number | string): boolean;\n /** Whether there is no room left at all. */\n isFull(): boolean;\n /** Write a new quantity onto the committed pick, or return false if absent. */\n commitQty(variantId: number | string, qty: number): boolean;\n}\n\n/** Width the picker card's `src` fallback requests; srcset covers the rest. */\nconst PICKER_BASE_WIDTH = 600;\n\nexport interface PickerRowContext {\n t: PickerTranslations;\n capacity: RowCapacity;\n rulesMap: Record<string, ProductRule | undefined>;\n showQtySelector: boolean;\n formatMoney: (cents: number) => string;\n /** Called after a stepper edit writes through to a committed pick. */\n onCommittedQtyChanged: () => void;\n}\n\nexport interface PickerRow {\n el: HTMLElement;\n /**\n * Recompute this row's stepper bounds and Add state from the live\n * selection. Called on every modal open and after every mutation, so\n * cross-slot stock accounting stays current.\n */\n refresh: () => void;\n}\n\n/**\n * Build one product row for the picker modal.\n *\n * Previously an IIFE inside the build loop, needed so each row's mutable state\n * got its own scope — with `var` alone every row's change handler aliased the\n * last iteration's variables, and picking a variant on one row wrote the\n * selection onto another. A function per row makes that structural rather than\n * a trick to remember.\n */\nexport function buildPickerRow(\n p: EligibleProduct,\n rowIndex: number,\n ctx: PickerRowContext,\n): PickerRow {\n const { t, rulesMap, capacity, formatMoney } = ctx;\n const pRule = ruleFor(p.id, rulesMap);\n\n const row = document.createElement(\"div\");\n row.className = \"lb-mix-match__modal-product\";\n if (!p.available) row.className += \" lb-mix-match__modal-product--sold-out\";\n row.setAttribute(\"data-product-item\", \"\");\n row.setAttribute(\"data-title\", normalizeText(p.title));\n row.setAttribute(\"data-type\", p.type || \"\");\n\n // Sold-out rows lock their variant selects. In-stock rows keep theirs\n // enabled even while a variant is committed — slots are variant-scoped, so\n // the dropdown is how a shopper reaches another variant of the same product.\n let rowDisabled = false;\n // The stepper has its own flag: a committed row keeps it enabled to edit the\n // pick's quantity live, while uncommitted rows lock it together with Add.\n let stepperDisabled = false;\n\n /* ── Thumbnail ─────────────────────────────────────────────── */\n\n const thumbDiv = document.createElement(\"div\");\n thumbDiv.className = \"lb-mix-match__modal-product-thumb\";\n\n // Informational only; the row's button is what acts.\n const addedBadge = document.createElement(\"span\");\n addedBadge.className = \"lb-mix-match__modal-added-badge\";\n addedBadge.textContent = t.addedItem || \"Added\";\n addedBadge.hidden = true;\n thumbDiv.appendChild(addedBadge);\n\n const firstAvailable = p.variants.find((v) => v.available) ?? null;\n const initialThumbSrc =\n (firstAvailable && firstAvailable.image) || p.featuredImage;\n if (initialThumbSrc) {\n const img = document.createElement(\"img\");\n img.src = imgWidth(initialThumbSrc, PICKER_BASE_WIDTH);\n img.srcset = srcset(initialThumbSrc, PICKER_WIDTHS);\n img.sizes = PICKER_SIZES;\n img.alt = p.title;\n img.loading = \"lazy\";\n thumbDiv.appendChild(img);\n }\n row.appendChild(thumbDiv);\n\n /* ── Info column ───────────────────────────────────────────── */\n\n const infoDiv = document.createElement(\"div\");\n infoDiv.className = \"lb-mix-match__modal-product-info\";\n\n const titleEl = document.createElement(\"p\");\n titleEl.className = \"lb-mix-match__modal-product-title\";\n if (p.url) {\n // New tab keeps the in-progress bundle alive; same-tab navigation would\n // wipe the shopper's picks.\n const titleLink = document.createElement(\"a\");\n titleLink.href = p.url;\n titleLink.target = \"_blank\";\n titleLink.rel = \"noopener noreferrer\";\n titleLink.textContent = p.title;\n titleEl.appendChild(titleLink);\n } else {\n titleEl.textContent = p.title;\n }\n infoDiv.appendChild(titleEl);\n\n // The row's currently-picked variant. Declared up here because the stepper\n // and the refresh hook both read it, on single- and multi-variant rows.\n let currentVariant: EligibleVariant | null =\n firstAvailable ?? (p.variants.length > 0 ? p.variants[0] : null);\n\n /**\n * The rule in force for a given variant: its own when the merchant wrote\n * one, else the product's, which is the envelope covering every variant.\n * Resolved per call rather than captured once, because the shopper moves\n * between variants through the dropdowns and the row's min/max must follow.\n * `pRule` stays the envelope and is used wherever the question is about the\n * product's total rather than this variant's allowance.\n */\n const ruleForVariant = (v: EligibleVariant | null): ProductRule =>\n ruleFor(p.id, rulesMap, v ? v.id : null);\n\n const modalQty = currentVariant ? ruleForVariant(currentVariant).min : 1;\n\n const priceEl = document.createElement(\"p\");\n priceEl.className = \"lb-mix-match__modal-product-price\";\n priceEl.setAttribute(\"data-row-price\", \"\");\n const priceSaleEl = document.createElement(\"span\");\n priceSaleEl.setAttribute(\"data-row-price-sale\", \"\");\n if (currentVariant) {\n priceSaleEl.textContent = formatMoney(currentVariant.price * modalQty);\n }\n priceEl.appendChild(priceSaleEl);\n const priceCompareEl = document.createElement(\"s\");\n priceCompareEl.className = \"lb-mix-match__modal-product-compare\";\n priceCompareEl.setAttribute(\"data-row-compare\", \"\");\n priceEl.appendChild(priceCompareEl);\n infoDiv.appendChild(priceEl);\n\n /** Struck compare-at for variant × qty; also CSS-gated by --lb-product-compare-display. */\n function paintRowCompare(\n el: HTMLElement,\n variant: EligibleVariant | null,\n qty: number,\n ): void {\n const cmp = variant && variant.compareAtPrice ? variant.compareAtPrice * qty : 0;\n const price = variant ? variant.price * qty : 0;\n if (cmp > price) {\n el.textContent = formatMoney(cmp);\n el.hidden = false;\n } else {\n el.textContent = \"\";\n el.hidden = true;\n }\n }\n paintRowCompare(priceCompareEl, currentVariant, modalQty);\n\n const unitPriceEl = document.createElement(\"p\");\n unitPriceEl.className =\n \"lb-mix-match__modal-product-unit-price lb-bundle-product-unit-price\";\n unitPriceEl.setAttribute(\"data-row-unit-price\", \"\");\n const initialUnit = currentVariant ? currentVariant.unitPrice : null;\n if (initialUnit) unitPriceEl.textContent = initialUnit;\n else unitPriceEl.hidden = true;\n infoDiv.appendChild(unitPriceEl);\n\n /* ── Quantity stepper ──────────────────────────────────────── */\n\n let qtyGroup: HTMLElement | null = null;\n let qtyValueEl: HTMLElement | null = null;\n let current = ruleForVariant(currentVariant).min;\n let effectiveMax = ruleForVariant(currentVariant).max;\n let paint: (() => void) | null = null;\n let setBoundsForVariant:\n | ((variant: EligibleVariant | null, alreadyInBundle?: number) => void)\n | null = null;\n\n if (p.available && ctx.showQtySelector) {\n // Mirrors renderQtyStepper in packages/widget/src/renderers/mix-match.ts;\n // the bundle-css-parity test pins both stylesheets together.\n qtyGroup = document.createElement(\"div\");\n qtyGroup.className =\n \"lb-bundle-variant-option-group lb-mix-match__qty-stepper-group\";\n\n const stepperWrap = document.createElement(\"div\");\n stepperWrap.className = \"lb-mix-match__qty-stepper\";\n stepperWrap.setAttribute(\"role\", \"group\");\n stepperWrap.setAttribute(\"aria-label\", t.quantity || \"Quantity\");\n\n const qtyMinusBtn = document.createElement(\"button\");\n qtyMinusBtn.type = \"button\";\n qtyMinusBtn.className =\n \"lb-mix-match__qty-stepper-button lb-mix-match__qty-stepper-button--minus\";\n qtyMinusBtn.setAttribute(\n \"aria-label\",\n t.decreaseQuantity || \"Decrease quantity\",\n );\n qtyMinusBtn.textContent = \"−\";\n\n qtyValueEl = document.createElement(\"span\");\n qtyValueEl.className = \"lb-mix-match__qty-stepper-value\";\n qtyValueEl.setAttribute(\"data-row-qty\", \"\");\n qtyValueEl.setAttribute(\"aria-live\", \"polite\");\n qtyValueEl.textContent = String(ruleForVariant(currentVariant).min);\n\n const qtyPlusBtn = document.createElement(\"button\");\n qtyPlusBtn.type = \"button\";\n qtyPlusBtn.className =\n \"lb-mix-match__qty-stepper-button lb-mix-match__qty-stepper-button--plus\";\n qtyPlusBtn.setAttribute(\n \"aria-label\",\n t.increaseQuantity || \"Increase quantity\",\n );\n qtyPlusBtn.textContent = \"+\";\n\n stepperWrap.appendChild(qtyMinusBtn);\n stepperWrap.appendChild(qtyValueEl);\n stepperWrap.appendChild(qtyPlusBtn);\n qtyGroup.appendChild(stepperWrap);\n\n const valueEl = qtyValueEl;\n paint = () => {\n valueEl.textContent = String(current);\n valueEl.setAttribute(\"data-qty\", String(current));\n qtyMinusBtn.disabled =\n stepperDisabled || current <= ruleForVariant(currentVariant).min;\n qtyPlusBtn.disabled = stepperDisabled || current >= effectiveMax;\n };\n\n setBoundsForVariant = (variant, alreadyInBundle = 0) => {\n const variantId = variant ? variant.id : null;\n const ownQty =\n variantId === null ? 0 : capacity.ownQty(variantId);\n const otherUnits =\n variantId === null\n ? 0\n : capacity.productUnitsElsewhere(p.id, variantId);\n\n // A swap displaces sibling units rather than adding, so the bundle's\n // open spots don't constrain it — the total never moves.\n const swapUnits =\n variantId === null\n ? 0\n : capacity.swapUnits(p.id, variantId);\n\n const stock = variant ? variant.inventoryQuantity : null;\n // Own committed units draw from the same stock pool the stepper edits,\n // so only units held by OTHER slots reduce what's available here.\n const othersInBundle = Math.max(0, alreadyInBundle - ownQty);\n const stockCap =\n typeof stock === \"number\"\n ? Math.max(0, stock - othersInBundle)\n : undefined;\n\n // The variant's own cap, when it has one. Passed separately because it\n // binds independently of the envelope: sibling units eat into the\n // product's total, never into this variant's allowance.\n const vMax = variantRuleFor(variantId, rulesMap)?.max;\n const rMin = ruleForVariant(variant).min;\n\n effectiveMax =\n swapUnits > 0\n ? typeof stockCap === \"number\"\n ? Math.min(swapUnits, stockCap)\n : swapUnits\n : stepperCeiling(\n pRule,\n capacity.remainingSpots(),\n stockCap,\n ownQty,\n otherUnits,\n vMax,\n );\n\n if (effectiveMax < rMin) {\n // Nothing can satisfy another pick — pin the display at rule.min so\n // the stepper doesn't flash 0; refresh() disables Add below.\n current = rMin;\n } else {\n if (current > effectiveMax) current = effectiveMax;\n if (current < rMin) current = rMin;\n }\n paint!();\n };\n\n /**\n * Live edit: when the row's variant holds a slot, a stepper click writes\n * straight onto that pick and runs the standard update path, so freeing\n * units immediately re-enables rows the exact-size rule had disabled.\n * Removal stays with the Added toggle — the floor here is rule.min, never 0.\n */\n const commitQtyToBundle = (): void => {\n if (!currentVariant) return;\n if (capacity.commitQty(currentVariant.id, current)) {\n ctx.onCommittedQtyChanged();\n }\n };\n\n qtyMinusBtn.addEventListener(\"click\", () => {\n if (current > ruleForVariant(currentVariant).min) current -= 1;\n paint!();\n commitQtyToBundle();\n });\n qtyPlusBtn.addEventListener(\"click\", () => {\n if (current < effectiveMax) current += 1;\n paint!();\n commitQtyToBundle();\n });\n\n if (currentVariant) setBoundsForVariant(currentVariant);\n else paint();\n }\n\n /* ── Variant option selects ────────────────────────────────── */\n\n // Sold-out variants are included so their option values render disabled,\n // matching how unavailable combinations behave; isValueAvailable filters\n // them out of the selectable set.\n const availVariants = p.variants.slice();\n const optionSelects: HTMLSelectElement[] = [];\n\n if (availVariants.length > 1) {\n const optionNames = p.optionNames || [];\n // Prefer the first purchasable option so the default price and the Add\n // click target something buyable.\n currentVariant =\n availVariants.find((v) => v.available !== false) ?? availVariants[0];\n\n const findMatchingVariant = (): EligibleVariant | null => {\n const values = optionSelects.map((s) => s.value);\n for (const v of availVariants) {\n if (!v.options || v.options.length !== values.length) continue;\n if (v.options.every((o, j) => o === values[j])) return v;\n }\n return null;\n };\n\n const syncSelectsToVariant = (v: EligibleVariant | null): void => {\n if (!v || !v.options) return;\n for (let i = 0; i < optionSelects.length && i < v.options.length; i++) {\n if (optionSelects[i].value !== v.options[i]) {\n optionSelects[i].value = v.options[i];\n }\n }\n };\n\n const isValueAvailable = (\n optionIndex: number,\n value: string,\n selected: string[],\n ): boolean => {\n for (const v of availVariants) {\n if (v.available === false) continue;\n if (!v.options || v.options[optionIndex] !== value) continue;\n let ok = true;\n for (let j = 0; j < v.options.length; j++) {\n if (j === optionIndex) continue;\n if (v.options[j] !== selected[j]) {\n ok = false;\n break;\n }\n }\n if (ok) return true;\n }\n return false;\n };\n\n const recomputeDisabled = (selected: string[]): void => {\n for (let i = 0; i < optionSelects.length; i++) {\n const sel = optionSelects[i];\n for (let o = 0; o < sel.options.length; o++) {\n sel.options[o].disabled = !isValueAvailable(\n i,\n sel.options[o].value,\n selected,\n );\n }\n }\n };\n\n const onOptionChange = (): void => {\n const v = findMatchingVariant();\n if (!v) {\n // Disabled combination reached by keyboard — revert rather than jump\n // to an unrelated variant.\n syncSelectsToVariant(currentVariant);\n if (currentVariant?.options) recomputeDisabled(currentVariant.options);\n return;\n }\n\n currentVariant = v;\n row.setAttribute(\"data-selected-variant-id\", String(v.id));\n\n const resolvedQty = ruleForVariant(v).min;\n priceSaleEl.textContent = formatMoney(v.price * resolvedQty);\n paintRowCompare(priceCompareEl, v, resolvedQty);\n\n if (v.unitPrice) {\n unitPriceEl.textContent = v.unitPrice;\n unitPriceEl.hidden = false;\n } else {\n unitPriceEl.textContent = \"\";\n unitPriceEl.hidden = true;\n }\n\n const rowImg = thumbDiv.querySelector(\"img\");\n const imgSrc = v.image || p.featuredImage;\n if (rowImg && imgSrc) {\n rowImg.src = imgWidth(imgSrc, PICKER_BASE_WIDTH);\n rowImg.srcset = srcset(imgSrc, PICKER_WIDTHS);\n }\n\n // Bounds depend on the new variant's stock and on how many of it other\n // slots already hold.\n setBoundsForVariant?.(v, capacity.variantUnitsElsewhere(v.id));\n if (v.options) recomputeDisabled(v.options);\n refresh();\n };\n\n const groupsContainer = document.createElement(\"div\");\n groupsContainer.className = \"lb-bundle-variant-option-groups\";\n\n for (let on = 0; on < optionNames.length; on++) {\n const optName = optionNames[on];\n const groupEl = document.createElement(\"div\");\n groupEl.className = \"lb-bundle-variant-option-group\";\n\n const labelEl = document.createElement(\"span\");\n labelEl.className = \"lb-bundle-variant-option-label\";\n labelEl.textContent = optName;\n groupEl.appendChild(labelEl);\n\n const sel = document.createElement(\"select\");\n sel.className = \"lb-mix-match__variant-select\";\n sel.setAttribute(\"data-variant-option\", \"\");\n sel.setAttribute(\"data-option-position\", String(on + 1));\n sel.name = \"lb-variant-\" + p.id + \"-\" + (on + 1);\n sel.setAttribute(\"aria-label\", optName);\n\n const seenValues: Record<string, boolean> = Object.create(null);\n const currentVal = currentVariant?.options?.[on];\n for (const av of availVariants) {\n const val = av.options && av.options[on];\n if (!val || seenValues[val]) continue;\n seenValues[val] = true;\n const opt = document.createElement(\"option\");\n opt.value = val;\n opt.textContent = val;\n if (val === currentVal) opt.selected = true;\n sel.appendChild(opt);\n }\n\n sel.addEventListener(\"change\", onOptionChange);\n optionSelects.push(sel);\n groupEl.appendChild(sel);\n groupsContainer.appendChild(groupEl);\n }\n\n infoDiv.appendChild(groupsContainer);\n if (currentVariant) {\n row.setAttribute(\"data-selected-variant-id\", String(currentVariant.id));\n if (currentVariant.options) recomputeDisabled(currentVariant.options);\n }\n } else if (\n availVariants.length === 1 &&\n availVariants[0].title !== \"Default Title\"\n ) {\n const varLabel = document.createElement(\"span\");\n varLabel.className = \"lb-mix-match__filled-variant\";\n varLabel.textContent = availVariants[0].title;\n infoDiv.appendChild(varLabel);\n }\n\n /* ── Needs-spots hint and actions ──────────────────────────── */\n\n let needsSpotsEl: HTMLElement | null = null;\n if (p.available) {\n needsSpotsEl = document.createElement(\"span\");\n needsSpotsEl.className = \"lb-mix-match__modal-needs-spots\";\n needsSpotsEl.hidden = true;\n infoDiv.appendChild(needsSpotsEl);\n }\n\n let addBtn: HTMLButtonElement | null = null;\n if (p.available) {\n const actionsDiv = document.createElement(\"div\");\n actionsDiv.className = \"lb-mix-match__modal-product-actions\";\n if (qtyGroup) actionsDiv.appendChild(qtyGroup);\n\n addBtn = document.createElement(\"button\");\n addBtn.type = \"button\";\n addBtn.className = \"lb-mix-match__modal-add\";\n addBtn.textContent = t.addItem || \"Add\";\n // Per-row accessible name: without it every Add announces as just \"Add,\n // button\" and a screen reader user can't tell which product it belongs to.\n addBtn.setAttribute(\"aria-label\", (t.addItem || \"Add\") + \" \" + p.title);\n addBtn.setAttribute(\"data-add-product\", String(rowIndex));\n actionsDiv.appendChild(addBtn);\n infoDiv.appendChild(actionsDiv);\n } else {\n const soldOutLabel = document.createElement(\"span\");\n soldOutLabel.className = \"lb-mix-match__modal-sold-out-label\";\n soldOutLabel.textContent = t.soldOut || \"Sold out\";\n row.setAttribute(\"aria-disabled\", \"true\");\n infoDiv.appendChild(soldOutLabel);\n }\n\n row.appendChild(infoDiv);\n\n /* ── Refresh hook ──────────────────────────────────────────── */\n\n let lastVariantId: number | null = currentVariant ? currentVariant.id : null;\n\n function refresh(): void {\n if (!currentVariant) return;\n const variant = currentVariant;\n\n const stock = variant.inventoryQuantity;\n const already =\n typeof stock === \"number\"\n ? capacity.variantUnitsElsewhere(variant.id)\n : 0;\n const inBundle = capacity.isInBundle(variant.id);\n\n if (setBoundsForVariant) {\n // A committed row's stepper mirrors and edits the committed quantity.\n // Switching to an uncommitted variant starts fresh at rule.min rather\n // than dragging the previous variant's quantity across.\n const committed = capacity.ownQty(variant.id);\n if (committed > 0) current = committed;\n else if (lastVariantId !== variant.id)\n current = ruleForVariant(variant).min;\n lastVariantId = variant.id;\n setBoundsForVariant(variant, already);\n }\n\n // A hard-capped required product's uncommitted variants displace sibling\n // units, so the exact-size and capacity gates don't apply to them.\n const rowSwapUnits = capacity.swapUnits(p.id, variant.id);\n const vRule = ruleForVariant(variant);\n const inSwapState = rowSwapUnits >= vRule.min;\n\n const remaining = capacity.remainingSpots();\n const needsMoreSpots =\n !inBundle && !inSwapState && !isRowAddable(vRule, remaining);\n\n if (needsSpotsEl) {\n // Hidden at capacity: the modal's complete state already tells that\n // story for every row at once.\n if (needsMoreSpots && remaining > 0) {\n needsSpotsEl.textContent = formatNeedsSpots(vRule.min, t);\n needsSpotsEl.hidden = false;\n } else {\n needsSpotsEl.hidden = true;\n }\n }\n\n if (addBtn) {\n if (inBundle) {\n addBtn.disabled = false;\n if (!capacity.canRemove(variant.id)) {\n // The required product's last covering pick — an inert \"Required\"\n // rather than a Remove that would silently no-op.\n addBtn.disabled = true;\n addBtn.textContent = t.required || \"Required\";\n addBtn.setAttribute(\n \"aria-label\",\n p.title + \" is required and cannot be removed\",\n );\n addBtn.classList.add(\"lb-mix-match__modal-add--required\");\n } else {\n addBtn.textContent = t.removeItem || \"Remove\";\n addBtn.setAttribute(\n \"aria-label\",\n (t.removeItem || \"Remove\") + \" \" + p.title,\n );\n addBtn.classList.remove(\"lb-mix-match__modal-add--required\");\n }\n addBtn.classList.add(\"lb-mix-match__modal-add--added\");\n row.classList.add(\"lb-mix-match__modal-product--in-bundle\");\n addedBadge.hidden = false;\n } else {\n const swapLabel = t.swapItem || \"Swap\";\n const label = inSwapState ? swapLabel : t.addItem || \"Add\";\n addBtn.textContent = label;\n addBtn.setAttribute(\"aria-label\", label + \" \" + p.title);\n addBtn.classList.remove(\"lb-mix-match__modal-add--added\");\n addBtn.classList.remove(\"lb-mix-match__modal-add--required\");\n row.classList.remove(\"lb-mix-match__modal-product--in-bundle\");\n addedBadge.hidden = true;\n\n const noStock =\n typeof stock === \"number\"\n ? Math.max(0, stock - already) < vRule.min\n : false;\n\n if (inSwapState) {\n // A swap frees its own room, so only stock can block it.\n addBtn.disabled = noStock;\n } else {\n // Sibling-variant slots may already claim so much of rule.max that\n // this variant can't take rule.min.\n const productMaxed =\n pRule.max - capacity.productUnitsElsewhere(p.id, variant.id) <\n vRule.min;\n addBtn.disabled =\n noStock ||\n productMaxed ||\n needsMoreSpots ||\n capacity.isFull();\n }\n }\n }\n\n // The custom dropdown trigger follows its select's disabled attribute via\n // a MutationObserver (see dropdown/bind.ts syncFromSelect), so propagating\n // here keeps the picker chrome consistent with the form control.\n rowDisabled = !p.available;\n stepperDisabled = !p.available || (!inBundle && !!addBtn?.disabled);\n paint?.();\n for (const sel of optionSelects) sel.disabled = rowDisabled;\n }\n\n return { el: row, refresh };\n}\n","const FOCUSABLE =\n 'a[href],button:not([disabled]),input:not([disabled]),' +\n 'select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex=\"-1\"])';\n\n/**\n * Focusable descendants that are actually on screen.\n *\n * `offsetParent === null` filters out anything hidden by `display: none` or an\n * ancestor that is — a filter pill in a collapsed step, say. Without it the\n * trap can send focus to an invisible control and the modal appears to swallow\n * Tab.\n */\nexport function getFocusable(container: HTMLElement): HTMLElement[] {\n const nodes = container.querySelectorAll<HTMLElement>(FOCUSABLE);\n const result: HTMLElement[] = [];\n for (let i = 0; i < nodes.length; i++) {\n if (nodes[i].offsetParent !== null) result.push(nodes[i]);\n }\n return result;\n}\n","/**\n * Body scroll lock that survives iOS Safari.\n *\n * `overflow: hidden` alone does not hold on iOS, so the body is pinned with\n * `position: fixed` at a negative offset and the scroll position is restored\n * on unlock. Reference-counted because the mix-and-match picker can open a\n * dropdown over an already-locked modal, and the inner unlock must not release\n * the outer lock.\n */\nlet savedY = 0;\nlet lockCount = 0;\n\nexport function lock(): void {\n lockCount++;\n if (lockCount > 1) return;\n\n savedY = window.pageYOffset || document.documentElement.scrollTop;\n const body = document.body;\n body.style.position = \"fixed\";\n body.style.top = \"-\" + savedY + \"px\";\n body.style.left = \"0\";\n body.style.right = \"0\";\n body.style.overflow = \"hidden\";\n}\n\nexport function unlock(): void {\n if (lockCount <= 0) return;\n lockCount--;\n if (lockCount > 0) return;\n\n const body = document.body;\n body.style.position = \"\";\n body.style.top = \"\";\n body.style.left = \"\";\n body.style.right = \"\";\n body.style.overflow = \"\";\n window.scrollTo(0, savedY);\n}\n","/**\n * Order the picker's product rows so what's left to pick sits at the top.\n *\n * Products the shopper has already dealt with — any variant committed to the\n * bundle (or the current step), required seeds included — move after the\n * products not yet added. The partition is stable: within each half, rows\n * keep the merchant's product order.\n *\n * Runs only when the modal opens or a step loads, never on add/remove: a\n * list that re-sorts under the shopper's finger loses their place, so a\n * product picked mid-session stays put until the next open or step change.\n *\n * Rows are moved in the DOM rather than rebuilt, and always re-appended from\n * the canonical order, so repeated opens are deterministic and the rows'\n * `data-add-product` indices (which point into the products array, not the\n * DOM) stay valid.\n */\nexport function sinkAddedRows(\n list: HTMLElement,\n rowEls: HTMLElement[],\n isAdded: (rowIndex: number) => boolean,\n): void {\n const front: HTMLElement[] = [];\n const back: HTMLElement[] = [];\n for (let i = 0; i < rowEls.length; i++) {\n (isAdded(i) ? back : front).push(rowEls[i]);\n }\n for (const el of front) list.appendChild(el);\n for (const el of back) list.appendChild(el);\n}\n","import { formatOfSelected, formatSubtitle } from \"./i18n\";\nimport { fill } from \"../picker/i18n\";\nimport {\n buildPickerRow,\n type PickerRow,\n type RowCapacity,\n} from \"../picker/row\";\nimport {\n defaultPickQuantity,\n totalUnits,\n type ProductRule,\n type SelectedItem,\n} from \"../picker/rules\";\nimport { getFocusable } from \"../picker/focus-trap\";\nimport { lock, unlock } from \"../picker/scroll-lock\";\nimport { sinkAddedRows } from \"../picker/sort\";\nimport { debounce, normalizeText } from \"../utils\";\nimport type { DropdownApi } from \"../host\";\nimport type {\n EligibleProduct,\n MixMatchData,\n MixMatchTranslations,\n} from \"./types\";\n\nconst ALL_TYPES = \"__all__\";\n/** Matches the overlay's CSS transition so display:none lands after it. */\nconst CLOSE_TRANSITION_MS = 300;\nconst SEARCH_DEBOUNCE_MS = 200;\n\nexport interface ModalDeps {\n container: HTMLElement;\n data: MixMatchData;\n t: MixMatchTranslations;\n rulesMap: Record<string, ProductRule | undefined>;\n eligibleProducts: EligibleProduct[];\n requiredQty: number;\n showQtySelector: boolean;\n items: SelectedItem[];\n formatMoney: (cents: number) => string;\n /** Answers the row's capacity questions against the live selection. */\n rowCapacity: RowCapacity;\n dropdown: DropdownApi | undefined;\n /** Where to portal the overlay, or null to leave it in place. */\n portalTarget: HTMLElement | null;\n /** Element focus returns to when the picker closes. */\n fallbackFocus: HTMLElement | null;\n /** Called when a row's add/remove/swap changed the selection. */\n onSelectionChanged: () => void;\n /** Add a pick, subject to the capacity and product-max gates. */\n addSelection: (item: SelectedItem) => void;\n removeVariant: (variantId: number) => void;\n swapVariant: (item: SelectedItem, qty: number) => void;\n swapUnitsFor: (productId: number, variantId: number) => number;\n isInBundle: (variantId: number) => boolean;\n atCapacity: () => boolean;\n}\n\nexport interface PickerModal {\n open: () => void;\n close: () => void;\n isOpen: () => boolean;\n /** Recompute every row's bounds and the footer, after any mutation. */\n refresh: () => void;\n overlay: HTMLElement | null;\n}\n\nexport function createPickerModal(deps: ModalDeps): PickerModal {\n const { container, data, t, eligibleProducts, items } = deps;\n\n const overlay = container.querySelector<HTMLElement>(\"[data-modal-overlay]\");\n // Themes routinely wrap sections in a transform or `contain`, either of\n // which makes position:fixed resolve against that ancestor instead of the\n // viewport — so the theme host portals the overlay to <body>. A shadow root\n // has no such ancestor, and moving the overlay out of it would strip the\n // scoped styles, so that host passes null and it stays put.\n if (overlay && deps.portalTarget) deps.portalTarget.appendChild(overlay);\n\n const q = <T extends HTMLElement>(sel: string): T | null =>\n overlay ? overlay.querySelector<T>(sel) : null;\n\n const modalDialog = q<HTMLElement>('[role=\"dialog\"]');\n const searchInput = q<HTMLInputElement>(\"[data-modal-search]\");\n const searchClear = q<HTMLElement>(\"[data-modal-search-clear]\");\n const modalList = q<HTMLElement>(\"[data-modal-list]\");\n const modalEmpty = q<HTMLElement>(\"[data-modal-empty]\");\n const modalLive = q<HTMLElement>(\"[data-modal-live]\");\n const closeBtn = q<HTMLElement>(\"[data-modal-close]\");\n const doneBtn = q<HTMLElement>(\"[data-modal-done]\");\n const footerCountEl = q<HTMLElement>(\"[data-modal-footer-count]\");\n const subtitleEl = q<HTMLElement>(\"[data-modal-subtitle]\");\n const filtersContainer = q<HTMLElement>(\"[data-modal-filters]\");\n\n const showTypeFilters = data.showTypeFilters !== false;\n let activeType = ALL_TYPES;\n let productListBuilt = false;\n let filtersBuilt = false;\n let modalOpen = false;\n let rows: PickerRow[] = [];\n\n function refreshAllStepperBounds(): void {\n for (const r of rows) r.refresh();\n }\n\n function updateModalMeta(): void {\n // Capped for display: over-completion (required 3, two picks of 2) still\n // reads \"3 of 3\".\n const count = Math.min(totalUnits(items), deps.requiredQty);\n const remaining = Math.max(0, deps.requiredQty - count);\n\n if (footerCountEl) {\n footerCountEl.textContent = formatOfSelected(count, deps.requiredQty, t);\n }\n if (subtitleEl) {\n subtitleEl.textContent = formatSubtitle(\n remaining,\n data.discountType,\n data.discountValue,\n t,\n );\n }\n }\n\n function refresh(): void {\n refreshAllStepperBounds();\n updateModalMeta();\n }\n\n /* ── Product list ──────────────────────────────────────────── */\n\n function buildProductList(): void {\n if (!modalList) return;\n\n // Tear the custom dropdowns down before wiping, or their MutationObservers\n // are left watching detached selects.\n deps.dropdown?.unbindAll(modalList);\n modalList.innerHTML = \"\";\n rows = [];\n\n for (let i = 0; i < eligibleProducts.length; i++) {\n const row = buildPickerRow(eligibleProducts[i], i, {\n t,\n rulesMap: deps.rulesMap,\n showQtySelector: deps.showQtySelector,\n capacity: deps.rowCapacity,\n formatMoney: deps.formatMoney,\n onCommittedQtyChanged: () => {\n deps.onSelectionChanged();\n refresh();\n },\n });\n rows.push(row);\n modalList.appendChild(row.el);\n }\n\n // The modal lives outside .lb-bundle-widget once reparented, so the global\n // auto-binder can't reach these selects.\n deps.dropdown?.bindAll(modalList);\n\n modalList.addEventListener(\"click\", onListClick);\n }\n\n function onListClick(e: MouseEvent): void {\n const addBtn = (e.target as HTMLElement).closest<HTMLButtonElement>(\n \"[data-add-product]\",\n );\n if (!addBtn || addBtn.disabled) return;\n\n const prodIndex = parseInt(\n addBtn.getAttribute(\"data-add-product\") ?? \"\",\n 10,\n );\n const prod = eligibleProducts[prodIndex];\n if (!prod) return;\n\n // The row carries the id resolved by its option selects; single-variant\n // rows fall back to the first available variant.\n const row = addBtn.closest<HTMLElement>(\".lb-mix-match__modal-product\");\n const selectedId = row && row.getAttribute(\"data-selected-variant-id\");\n let selectedVariant =\n (selectedId\n ? prod.variants.find((v) => v.id === parseInt(selectedId, 10))\n : null) ?? null;\n if (!selectedVariant) {\n selectedVariant = prod.variants.find((v) => v.available) ?? null;\n }\n if (!selectedVariant) return;\n\n // Toggle off. Slots are variant-scoped, so other variants of the same\n // product keep theirs.\n if (deps.isInBundle(selectedVariant.id)) {\n deps.removeVariant(selectedVariant.id);\n refresh();\n return;\n }\n\n // The stepper publishes its live value in data-qty, so this handler\n // doesn't need to share a closure with it.\n const rowQtyEl = row?.querySelector<HTMLElement>(\"[data-row-qty]\");\n const pickedQty = rowQtyEl\n ? Math.max(\n 1,\n parseInt(\n rowQtyEl.getAttribute(\"data-qty\") || rowQtyEl.textContent || \"\",\n 10,\n ) || 1,\n )\n : defaultPickQuantity(prod.id, deps.rulesMap, selectedVariant.id);\n\n const newItem: SelectedItem = {\n productId: prod.id,\n variantId: selectedVariant.id,\n title: prod.title,\n url: prod.url || null,\n variantTitle: selectedVariant.title,\n featuredImage: selectedVariant.image || prod.featuredImage || null,\n price: selectedVariant.price,\n compareAtPrice: selectedVariant.compareAtPrice || null,\n unitPrice: selectedVariant.unitPrice || null,\n quantity: pickedQty,\n };\n\n // An atomic swap displaces sibling units, so the total never moves and\n // this runs instead of the capacity gate.\n const swapUnits = deps.swapUnitsFor(prod.id, selectedVariant.id);\n if (swapUnits > 0) {\n deps.swapVariant(newItem, Math.min(pickedQty, swapUnits));\n refresh();\n return;\n }\n\n if (deps.atCapacity()) return;\n\n deps.addSelection(newItem);\n refresh();\n }\n\n /* ── Filter pills ──────────────────────────────────────────── */\n\n function buildFilters(): void {\n if (!filtersContainer || !showTypeFilters) return;\n\n const types: string[] = [];\n const seen: Record<string, boolean> = Object.create(null);\n for (const p of eligibleProducts) {\n const ty = (p.type || \"\").trim();\n if (ty && !seen[ty]) {\n seen[ty] = true;\n types.push(ty);\n }\n }\n\n // One type filters nothing — hide the row and let search own the gap.\n if (types.length < 2) {\n filtersContainer.style.display = \"none\";\n modalDialog?.classList.add(\"lb-mix-match__modal--filters-hidden\");\n return;\n }\n\n filtersContainer.innerHTML = \"\";\n for (const value of [ALL_TYPES, ...types]) {\n const pill = document.createElement(\"button\");\n pill.type = \"button\";\n pill.className = \"lb-mix-match__filter\";\n pill.setAttribute(\"data-filter\", value);\n pill.setAttribute(\"aria-pressed\", value === activeType ? \"true\" : \"false\");\n if (value === activeType) pill.classList.add(\"lb-mix-match__filter--active\");\n pill.textContent = value === ALL_TYPES ? t.allTypes || \"All\" : value;\n pill.addEventListener(\"click\", () => {\n activeType = value;\n syncActiveFilterPill();\n filterProducts(searchInput ? searchInput.value : \"\");\n });\n filtersContainer.appendChild(pill);\n }\n }\n\n function syncActiveFilterPill(): void {\n if (!filtersContainer) return;\n const pills = filtersContainer.querySelectorAll<HTMLElement>(\"[data-filter]\");\n for (let i = 0; i < pills.length; i++) {\n const on = pills[i].getAttribute(\"data-filter\") === activeType;\n pills[i].classList.toggle(\"lb-mix-match__filter--active\", on);\n pills[i].setAttribute(\"aria-pressed\", on ? \"true\" : \"false\");\n }\n }\n\n /* ── Search ────────────────────────────────────────────────── */\n\n function filterProducts(query: string): void {\n if (!modalList) return;\n const normalizedQuery = normalizeText(query);\n const listItems = modalList.querySelectorAll<HTMLElement>(\n \"[data-product-item]\",\n );\n let visibleCount = 0;\n\n for (let i = 0; i < listItems.length; i++) {\n const title = listItems[i].getAttribute(\"data-title\") ?? \"\";\n const type = listItems[i].getAttribute(\"data-type\") || \"\";\n const matches =\n (!normalizedQuery || title.indexOf(normalizedQuery) !== -1) &&\n (activeType === ALL_TYPES || type === activeType);\n listItems[i].classList.toggle(\"lb-hidden\", !matches);\n if (matches) visibleCount++;\n }\n\n if (modalEmpty) {\n modalEmpty.style.display =\n visibleCount === 0 && normalizedQuery.length > 0 ? \"\" : \"none\";\n }\n if (searchClear) {\n searchClear.style.display = query.length > 0 ? \"\" : \"none\";\n }\n if (modalLive) {\n modalLive.textContent = fill(\n t.nProductsShown || \"__COUNT__ products shown\",\n { COUNT: visibleCount },\n );\n }\n }\n\n if (searchInput) {\n const handleSearch = debounce(\n () => filterProducts(searchInput.value),\n SEARCH_DEBOUNCE_MS,\n );\n searchInput.addEventListener(\"input\", handleSearch);\n }\n if (searchClear) {\n searchClear.addEventListener(\"click\", () => {\n if (!searchInput) return;\n searchInput.value = \"\";\n filterProducts(\"\");\n searchInput.focus();\n });\n }\n\n /* ── Open / close ──────────────────────────────────────────── */\n\n function open(): void {\n if (!overlay || modalOpen) return;\n modalOpen = true;\n\n if (!productListBuilt) {\n buildProductList();\n productListBuilt = true;\n }\n if (!filtersBuilt) {\n buildFilters();\n filtersBuilt = true;\n }\n\n refresh();\n\n activeType = ALL_TYPES;\n syncActiveFilterPill();\n if (searchInput) searchInput.value = \"\";\n filterProducts(searchInput ? searchInput.value : \"\");\n\n // Products already in the bundle (required seeds included) sink below\n // the ones still to pick. Only here, on open — never on add/remove, so\n // the list doesn't jump under the shopper's finger mid-session.\n if (modalList) {\n const selectedProductIds: Record<string, true> = Object.create(null);\n for (const it of items) selectedProductIds[String(it.productId)] = true;\n sinkAddedRows(\n modalList,\n rows.map((r) => r.el),\n (i) => selectedProductIds[String(eligibleProducts[i].id)] === true,\n );\n modalList.scrollTop = 0;\n }\n\n overlay.style.display = \"\";\n // Force reflow so the transition runs from the hidden state.\n void overlay.offsetHeight;\n overlay.classList.add(\"lb-mix-match__modal-overlay--open\");\n\n lock();\n\n // The close icon, not the search input: focusing search on a phone pops\n // the keyboard over half the modal before the shopper asked for it.\n setTimeout(() => {\n if (closeBtn) closeBtn.focus();\n else modalDialog?.focus();\n }, 50);\n }\n\n function close(): void {\n if (!overlay || !modalOpen) return;\n modalOpen = false;\n\n overlay.classList.remove(\"lb-mix-match__modal-overlay--open\");\n unlock();\n\n setTimeout(() => {\n if (!modalOpen) overlay.style.display = \"none\";\n }, CLOSE_TRANSITION_MS);\n\n const elToFocus = deps.fallbackFocus;\n if (elToFocus && typeof elToFocus.focus === \"function\") {\n setTimeout(() => elToFocus.focus(), 0);\n }\n }\n\n closeBtn?.addEventListener(\"click\", close);\n doneBtn?.addEventListener(\"click\", close);\n\n // Backdrop dismissal tracks mousedown and mouseup so a drag that starts\n // inside the dialog and ends on the backdrop doesn't close it.\n if (overlay) {\n let mouseDownTarget: EventTarget | null = null;\n overlay.addEventListener(\"mousedown\", (e) => {\n mouseDownTarget = e.target;\n });\n overlay.addEventListener(\"mouseup\", (e) => {\n if (e.target === overlay && mouseDownTarget === overlay) close();\n mouseDownTarget = null;\n });\n }\n\n document.addEventListener(\"keydown\", (e) => {\n if (!modalOpen) return;\n\n if (e.key === \"Escape\") {\n e.preventDefault();\n close();\n return;\n }\n\n if (e.key === \"Tab\" && modalDialog) {\n const focusable = getFocusable(modalDialog);\n if (focusable.length === 0) {\n e.preventDefault();\n return;\n }\n const first = focusable[0];\n const last = focusable[focusable.length - 1];\n if (e.shiftKey && document.activeElement === first) {\n e.preventDefault();\n last.focus();\n } else if (!e.shiftKey && document.activeElement === last) {\n e.preventDefault();\n first.focus();\n }\n }\n });\n\n return { open, close, isOpen: () => modalOpen, refresh, overlay };\n}\n","/**\n * Mix & match capacity maths, over one flat selection.\n *\n * Takes the selection as an argument rather than closing over it, so the\n * arithmetic that decides what a shopper may pick is testable without building\n * a modal. Step-agnostic primitives live in ../picker/rules.\n */\nimport {\n indexOfVariantInBundle,\n ruleFor,\n variantRuleFor,\n totalUnits,\n type ProductRule,\n type SelectedItem,\n} from \"../picker/rules\";\n\n\n/**\n * Open spots left. `requiredQty` is a hard bundle size — the picker never lets\n * the unit total overshoot it, so a product whose rule.min exceeds what's left\n * cannot be added at all.\n */\nexport function remainingUnits(\n items: SelectedItem[],\n requiredQty: number,\n): number {\n return Math.max(0, requiredQty - totalUnits(items));\n}\n\nexport function atCapacity(items: SelectedItem[], requiredQty: number): boolean {\n return totalUnits(items) >= requiredQty;\n}\n\n/**\n * Units of a variant already committed.\n *\n * Caps the picker stepper and the Add button against stock: without it a\n * shopper could fill several slots with the same low-stock variant and blow\n * past inventory at checkout.\n */\nexport function alreadyInBundleForVariant(\n items: SelectedItem[],\n variantId: number | string,\n): number {\n let qty = 0;\n for (let i = 0; i < items.length; i++) {\n if (items[i].variantId === variantId) qty += items[i].quantity || 0;\n }\n return qty;\n}\n\n/**\n * Units of a product held by slots other than the given variant's own.\n *\n * `rule.max` caps a product's total across all its variant slots, so a row's\n * ceiling is rule.max minus what its sibling variants already claim.\n */\nexport function productUnitsInOtherSlots(\n items: SelectedItem[],\n productId: number | string,\n excludeVariantId: number | string,\n): number {\n let qty = 0;\n for (let i = 0; i < items.length; i++) {\n const it = items[i];\n if (it && it.productId === productId && it.variantId !== excludeVariantId) {\n qty += it.quantity || 0;\n }\n }\n return qty;\n}\n\n/**\n * Whether a pick may be removed.\n *\n * Product-level lock for required products: removal is allowed only while the\n * product's other slots keep it at or above rule.min. Shoppers can swap\n * variants (add Large, remove Small) but never drop a required product out of\n * the bundle.\n *\n * A required VARIANT is stricter and needs no tally. Slots are one per\n * (product, variant), so a required variant has no other slot to fall back on\n * and no sibling may stand in for it — naming the variant is precisely what\n * takes the swap off the table.\n */\nexport function canRemovePick(\n items: SelectedItem[],\n item: SelectedItem,\n rulesMap: Record<string, ProductRule | undefined>,\n): boolean {\n if (variantRuleFor(item.variantId, rulesMap)?.required) return false;\n\n const rule = ruleFor(item.productId, rulesMap);\n if (!rule.required) return true;\n\n let unitsElsewhere = 0;\n for (let ci = 0; ci < items.length; ci++) {\n const cs = items[ci];\n if (cs !== item && cs.productId === item.productId) {\n unitsElsewhere += cs.quantity || 1;\n }\n }\n return unitsElsewhere >= rule.min;\n}\n\n/**\n * Units available for an atomic variant swap, or 0 when the row isn't in that\n * state.\n *\n * A required product with no headroom (rule.min === rule.max) can't do the\n * add-then-remove swap the product-level lock normally allows: adding a second\n * variant would breach the cap and removing the committed one would break the\n * floor. Those rows get a \"Swap\" button instead, where the new variant's units\n * displace sibling units rather than adding on top.\n */\nexport function swapUnitsFor(\n items: SelectedItem[],\n productId: number | string,\n variantId: number | string,\n rulesMap: Record<string, ProductRule | undefined>,\n): number {\n const rule = ruleFor(productId, rulesMap);\n if (!rule.required || rule.max !== rule.min) return 0;\n if (indexOfVariantInBundle(items, variantId) !== -1) return 0;\n return productUnitsInOtherSlots(items, productId, variantId);\n}\n","import { imgWidth, srcset, THUMB_SIZES, THUMB_WIDTHS } from \"./images\";\nimport type { PickerTranslations, SelectedItem } from \"./types\";\n\nfunction removeIcon(): SVGElement {\n const NS = \"http://www.w3.org/2000/svg\";\n const svg = document.createElementNS(NS, \"svg\");\n svg.setAttribute(\"width\", \"16\");\n svg.setAttribute(\"height\", \"16\");\n svg.setAttribute(\"viewBox\", \"0 0 20 20\");\n svg.setAttribute(\"fill\", \"none\");\n for (const [x1, y1, x2, y2] of [\n [\"5\", \"5\", \"15\", \"15\"],\n [\"15\", \"5\", \"5\", \"15\"],\n ]) {\n const line = document.createElementNS(NS, \"line\");\n line.setAttribute(\"x1\", x1);\n line.setAttribute(\"y1\", y1);\n line.setAttribute(\"x2\", x2);\n line.setAttribute(\"y2\", y2);\n line.setAttribute(\"stroke\", \"currentColor\");\n line.setAttribute(\"stroke-width\", \"2\");\n line.setAttribute(\"stroke-linecap\", \"round\");\n svg.appendChild(line);\n }\n return svg;\n}\n\n/**\n * One filled slot in the widget body.\n *\n * Shared by mix & match and multi-step, which render identical cards — the\n * only difference is which selection the callbacks act on.\n */\nexport function buildSlotCard(\n item: SelectedItem,\n trans: PickerTranslations,\n formatMoney: (cents: number) => string,\n /** False for a required pick at its floor: shows an inert chip instead of ×. */\n removable: boolean,\n onRemove: () => void,\n onEdit: () => void,\n): HTMLElement {\n const card = document.createElement(\"div\");\n card.className = \"lb-mix-match__slot lb-mix-match__slot--filled\";\n\n const thumb = document.createElement(\"div\");\n thumb.className = \"lb-bundle-thumbnail\";\n if (item.featuredImage) {\n const img = document.createElement(\"img\");\n img.src = imgWidth(item.featuredImage, 240);\n img.srcset = srcset(item.featuredImage, THUMB_WIDTHS);\n img.sizes = THUMB_SIZES;\n img.alt = item.title;\n img.loading = \"lazy\";\n thumb.appendChild(img);\n }\n card.appendChild(thumb);\n\n const info = document.createElement(\"div\");\n info.className = \"lb-mix-match__filled-info\";\n\n // A button, not a link: navigating away would destroy the in-memory picks.\n // Product-page access lives on the modal row titles instead.\n const title = document.createElement(\"button\");\n title.type = \"button\";\n title.className = \"lb-mix-match__filled-title\";\n title.textContent = item.title;\n title.setAttribute(\n \"aria-label\",\n (trans.editSelection || \"Edit selection\") + \": \" + item.title,\n );\n info.appendChild(title);\n\n if (item.variantTitle && item.variantTitle !== \"Default Title\") {\n const variant = document.createElement(\"span\");\n variant.className = \"lb-mix-match__filled-variant\";\n variant.textContent = item.variantTitle;\n info.appendChild(variant);\n }\n\n const priceRow = document.createElement(\"span\");\n priceRow.className = \"lb-mix-match__filled-price\";\n const unitCompare = item.compareAtPrice || 0;\n if (unitCompare > item.price) {\n const strikeEl = document.createElement(\"s\");\n strikeEl.className = \"lb-mix-match__filled-compare\";\n strikeEl.textContent = formatMoney(unitCompare);\n priceRow.appendChild(strikeEl);\n }\n const saleEl = document.createElement(\"span\");\n saleEl.className = \"lb-bundle-product-price\";\n saleEl.textContent = formatMoney(item.price);\n priceRow.appendChild(saleEl);\n // Always rendered, even at ×1, so the count reads as a column the way the\n // fixed widget's quantity chip does.\n const qtyInline = document.createElement(\"span\");\n qtyInline.className = \"lb-bundle-qty-inline\";\n qtyInline.textContent = \"×\" + (item.quantity || 1);\n priceRow.appendChild(qtyInline);\n info.appendChild(priceRow);\n\n if (item.unitPrice) {\n const unitPriceEl = document.createElement(\"span\");\n unitPriceEl.className = \"lb-bundle-product-unit-price\";\n unitPriceEl.textContent = item.unitPrice;\n info.appendChild(unitPriceEl);\n }\n card.appendChild(info);\n\n if (removable) {\n const removeBtn = document.createElement(\"button\");\n removeBtn.type = \"button\";\n removeBtn.className = \"lb-mix-match__slot-remove\";\n removeBtn.setAttribute(\n \"aria-label\",\n (trans.removeItem || \"Remove\") + \" \" + item.title,\n );\n removeBtn.appendChild(removeIcon());\n removeBtn.addEventListener(\"click\", (e) => {\n e.stopPropagation();\n onRemove();\n });\n card.appendChild(removeBtn);\n } else {\n // Required pick at its floor — a quiet chip rather than a × that would\n // silently no-op. Removal returns once another slot covers the minimum.\n const requiredChip = document.createElement(\"span\");\n requiredChip.className = \"lb-mix-match__slot-required\";\n requiredChip.textContent = trans.required || \"Required\";\n card.appendChild(requiredChip);\n }\n\n // The whole card reopens the picker; the title button's keyboard activation\n // bubbles here too, and the × stops propagation above.\n card.addEventListener(\"click\", onEdit);\n return card;\n}\n\n","import { formatMoreToGo, formatOfSelected } from \"./i18n\";\nimport { canRemovePick } from \"./rules\";\nimport { buildSlotCard } from \"../picker/slot-card\";\nimport {\n totalUnits,\n type ProductRule,\n type SelectedItem,\n} from \"../picker/rules\";\nimport type { MixMatchData, MixMatchTranslations } from \"./types\";\n\nexport interface SlotRenderDeps {\n formatMoney: (cents: number) => string;\n rulesMap: Record<string, ProductRule | undefined>;\n onRemove: (index: number) => void;\n onEdit: () => void;\n}\n\nexport function updateSlotUI(\n cont: HTMLElement,\n items: SelectedItem[],\n reqQty: number,\n d: MixMatchData,\n deps: SlotRenderDeps,\n): void {\n const trans = d.translations || {};\n const slotsWrap = cont.querySelector<HTMLElement>(\"[data-selection-slots]\");\n\n if (slotsWrap) {\n slotsWrap.innerHTML = \"\";\n for (let i = 0; i < items.length; i++) {\n const index = i;\n slotsWrap.appendChild(\n buildSlotCard(\n items[index],\n trans,\n deps.formatMoney,\n canRemovePick(items, items[index], deps.rulesMap),\n () => deps.onRemove(index),\n deps.onEdit,\n ),\n );\n }\n }\n\n const trigger = cont.querySelector<HTMLElement>(\"[data-add-product-trigger]\");\n if (trigger) {\n trigger.style.display = totalUnits(items) >= reqQty ? \"none\" : \"\";\n }\n\n // Counts total UNITS, not slots filled: requiredQuantity is a unit total, so\n // one product at qty 3 completes a \"pick 3\" bundle — matching the Rust\n // discount function's gate.\n const ctaBtn = cont.querySelector<HTMLButtonElement>(\"[data-add-bundle]\");\n if (ctaBtn) {\n ctaBtn.disabled = totalUnits(items) < reqQty;\n // Written to the label child, never the button's textContent, which would\n // destroy the sibling spinner element. A forked theme missing the label\n // silently skips the text rather than falling back to that.\n const ctaLabel = ctaBtn.querySelector<HTMLElement>(\"[data-cta-label]\");\n if (ctaLabel) {\n ctaLabel.textContent =\n ctaBtn.getAttribute(\"data-cta-text\") || trans.addToCart || \"Add to cart\";\n }\n }\n}\n\nexport function updatePricing(\n cont: HTMLElement,\n items: SelectedItem[],\n reqQty: number,\n d: MixMatchData,\n calcDiscount: (total: number, type: string, value: number) => number,\n paint: (cont: HTMLElement, sale: number, compare: number) => void,\n): void {\n const pricingSection = cont.querySelector<HTMLElement>(\n \"[data-pricing-section]\",\n );\n\n if (totalUnits(items) < reqQty) {\n // Hide the whole summary band — price and savings both live inside it.\n if (pricingSection) pricingSection.style.display = \"none\";\n return;\n }\n\n if (pricingSection) pricingSection.style.display = \"\";\n\n let totalPrice = 0;\n for (let i = 0; i < items.length; i++) {\n totalPrice += items[i].price * (items[i].quantity || 1);\n }\n\n if (totalPrice > 0 && d.discountType) {\n paint(\n cont,\n calcDiscount(totalPrice, d.discountType, d.discountValue ?? 0),\n totalPrice,\n );\n }\n}\n\n/**\n * Repaint the progress bar.\n *\n * Runs over both roots because the modal is reparented to `<body>` and so\n * carries its own copy of the bar outside the widget container.\n */\nexport function updateProgress(\n cont: HTMLElement,\n overlay: HTMLElement | null,\n items: SelectedItem[],\n reqQty: number,\n trans: MixMatchTranslations,\n): void {\n const count = Math.min(totalUnits(items), reqQty);\n const remaining = Math.max(0, reqQty - count);\n\n const roots: HTMLElement[] = [cont];\n if (overlay) roots.push(overlay);\n\n for (const root of roots) {\n const segments = root.querySelectorAll<HTMLElement>(\n \"[data-progress-segment]\",\n );\n for (let s = 0; s < segments.length; s++) {\n segments[s].classList.toggle(\n \"lb-mix-match__progress-segment--filled\",\n s < count,\n );\n }\n\n const progressCount = root.querySelector<HTMLElement>(\n \"[data-progress-count]\",\n );\n if (progressCount) {\n progressCount.textContent = formatOfSelected(count, reqQty, trans);\n }\n\n const progressRemaining = root.querySelector<HTMLElement>(\n \"[data-progress-remaining]\",\n );\n if (progressRemaining) {\n progressRemaining.textContent = formatMoreToGo(remaining, trans);\n }\n\n const segWrap = root.querySelector<HTMLElement>(\n \".lb-mix-match__progress-segments\",\n );\n if (segWrap) segWrap.setAttribute(\"aria-valuenow\", String(count));\n }\n}\n","import type { FormatMoney } from \"@lime-bundles/render/host\";\n\n/**\n * The paint half of the shared pricing update.\n *\n * The theme host passes `LB.updatePricing`, which lives in its own DOM\n * helpers. This is the same thing for a host without that global.\n */\nexport function updatePricing(formatMoney: FormatMoney) {\n return (container: HTMLElement, salePrice: number, comparePrice: number): void => {\n const saleEl = container.querySelector<HTMLElement>(\"[data-sale-price]\");\n const compareEl = container.querySelector<HTMLElement>(\"[data-compare-price]\");\n const savingsBar = container.querySelector<HTMLElement>(\"[data-savings-bar]\");\n const savingsAmountEl = container.querySelector<HTMLElement>(\"[data-savings-amount]\");\n const savingsPercentEl = container.querySelector<HTMLElement>(\"[data-savings-percent]\");\n const savings = comparePrice - salePrice;\n\n if (saleEl) saleEl.textContent = formatMoney(salePrice);\n if (compareEl) {\n if (savings > 0) {\n compareEl.textContent = formatMoney(comparePrice);\n compareEl.style.display = \"\";\n } else {\n compareEl.style.display = \"none\";\n }\n }\n if (savingsBar) {\n if (savings > 0) {\n if (savingsAmountEl) savingsAmountEl.textContent = formatMoney(savings);\n if (savingsPercentEl) {\n const pct = comparePrice > 0 ? Math.round((savings * 100) / comparePrice) : 0;\n savingsPercentEl.textContent = \"(\" + pct + \"%)\";\n }\n savingsBar.style.display = \"\";\n } else {\n savingsBar.style.display = \"none\";\n }\n }\n };\n}\n","import {\n ruleFor,\n variantRuleFor,\n type ProductRule,\n type SelectedItem,\n} from \"../picker/rules\";\nimport type { EligibleProduct, EligibleVariant } from \"./types\";\n\n/**\n * The picks a mix & match bundle starts with.\n *\n * Two rules, in order:\n *\n * 1. **Required picks seed themselves.** Every product whose rule says\n * `required` gets a pick at its `rule.min`, because the discount only\n * applies while the cart holds that many — a shopper should not have to\n * discover that by trial. A required VARIANT seeds that exact variant\n * instead of the first available one: \"must include the black tote\" is a\n * thing the merchant can now say, and seeding an arbitrary colour would\n * both ignore them and fail the checkout gate. A product is required at one\n * level or the other, never both — the admin schema enforces it.\n * 2. **Otherwise, one courtesy pick — if the host asks for it.** Slot zero is\n * filled with the product the shopper is looking at when it qualifies, else\n * the first that does. That is theme UX: the shopper is standing on a\n * product page and expects to see it already in the bundle. A headless\n * embed has no such context, so it opts out and starts empty.\n *\n * Exact-size: a product whose minimum pick would overshoot the bundle size on\n * its own is never seeded.\n */\nexport function seedSelection(\n eligibleProducts: EligibleProduct[],\n rulesMap: Record<string, ProductRule | undefined>,\n requiredQty: number,\n courtesy?: { enabled: boolean; currentProductId?: number | null },\n): SelectedItem[] {\n const firstAvailable = (p: EligibleProduct): EligibleVariant | null =>\n p.variants.find((v) => v.available) ?? null;\n\n const pick = (\n p: EligibleProduct,\n v: EligibleVariant,\n quantity: number,\n ): SelectedItem => ({\n productId: p.id,\n variantId: v.id,\n title: p.title,\n url: p.url || null,\n variantTitle: v.title,\n featuredImage: p.featuredImage || null,\n price: v.price,\n compareAtPrice: v.compareAtPrice || null,\n unitPrice: v.unitPrice || null,\n quantity,\n });\n\n const required: SelectedItem[] = [];\n for (const p of eligibleProducts) {\n if (!p.available || !p.variants) continue;\n\n // Variant-level first: seed every variant the merchant named, at its own\n // minimum. A product with named variants never also carries a\n // product-level `required`, so this is not an either/or on the same units.\n let seededVariant = false;\n for (const v of p.variants) {\n const vRule = variantRuleFor(v.id, rulesMap);\n if (!vRule?.required || !v.available) continue;\n required.push(pick(p, v, Math.max(1, vRule.min)));\n seededVariant = true;\n }\n if (seededVariant) continue;\n\n const rule = ruleFor(p.id, rulesMap);\n if (!rule.required) continue;\n const v = firstAvailable(p);\n if (!v) continue;\n required.push(pick(p, v, Math.max(1, rule.min)));\n }\n if (required.length || !courtesy?.enabled) return required;\n\n const qualifies = (p: EligibleProduct): boolean =>\n p.available && ruleFor(p.id, rulesMap).min <= requiredQty;\n\n const seed =\n (courtesy.currentProductId != null\n ? eligibleProducts.find(\n (p) => p.id === courtesy.currentProductId && qualifies(p),\n )\n : null) ?? eligibleProducts.find(qualifies);\n if (!seed) return [];\n\n const v = firstAvailable(seed);\n return v ? [pick(seed, v, ruleFor(seed.id, rulesMap).min)] : [];\n}\n","\n/**\n * The DOM twin of `lb-picker-modal.liquid`.\n *\n * Mix & match and multi-step both open this shell; the shared `createPickerModal`\n * and `createWizard` query it by the `data-modal-*` hooks below, so those\n * attributes are the contract between the two languages — not the class names,\n * which only carry styling.\n */\n\nfunction icon(paths: Array<[string, string, string, string]>): SVGElement {\n const NS = \"http://www.w3.org/2000/svg\";\n const svg = document.createElementNS(NS, \"svg\");\n svg.setAttribute(\"width\", \"20\");\n svg.setAttribute(\"height\", \"20\");\n svg.setAttribute(\"viewBox\", \"0 0 20 20\");\n svg.setAttribute(\"fill\", \"none\");\n for (const [x1, y1, x2, y2] of paths) {\n const line = document.createElementNS(NS, \"line\");\n line.setAttribute(\"x1\", x1);\n line.setAttribute(\"y1\", y1);\n line.setAttribute(\"x2\", x2);\n line.setAttribute(\"y2\", y2);\n line.setAttribute(\"stroke\", \"currentColor\");\n line.setAttribute(\"stroke-width\", \"2\");\n line.setAttribute(\"stroke-linecap\", \"round\");\n svg.appendChild(line);\n }\n return svg;\n}\n\nfunction el(tag: string, className: string, attrs: Record<string, string> = {}) {\n const node = document.createElement(tag);\n if (className) node.className = className;\n for (const k in attrs) node.setAttribute(k, attrs[k]);\n return node;\n}\n\nexport interface PickerModalOptions {\n bundleGid: string;\n /** Unique per widget, so `aria-labelledby` resolves when several are on a page. */\n domId: string;\n showSearch: boolean;\n showTypeFilters: boolean;\n /**\n * Mix & match renders one progress segment per required unit up front.\n * The wizard leaves the container empty — its per-step segments are rebuilt\n * on every step change.\n */\n segmentCount: number | null;\n wizard: boolean;\n}\n\n/** Build the overlay, hidden. The caller appends it and drives open/close. */\nexport function buildPickerModal(opts: PickerModalOptions): HTMLElement {\n const titleId = \"lb-modal-title-\" + opts.domId;\n\n const overlay = el(\"div\", \"lb-mix-match__modal-overlay\", {\n \"data-modal-overlay\": \"\",\n \"data-bundle-gid\": opts.bundleGid,\n style: \"display: none;\",\n });\n\n const dialog = el(\n \"div\",\n \"lb-mix-match__modal\" +\n (opts.showTypeFilters ? \"\" : \" lb-mix-match__modal--filters-hidden\"),\n {\n role: \"dialog\",\n \"aria-modal\": \"true\",\n \"aria-labelledby\": titleId,\n tabindex: \"-1\",\n },\n );\n\n /* ── Header ────────────────────────────────────────────────── */\n\n const header = el(\"div\", \"lb-mix-match__modal-header\");\n const top = el(\"div\", \"lb-mix-match__modal-header-top\");\n\n const heading = el(\"div\", \"lb-mix-match__modal-heading\");\n const title = el(\"h4\", \"lb-mix-match__modal-title\", { id: titleId });\n // The wizard moves focus here on every step change, so it has to be\n // programmatically focusable.\n if (opts.wizard) title.setAttribute(\"tabindex\", \"-1\");\n title.textContent = \"Add to your bundle\";\n heading.appendChild(title);\n heading.appendChild(\n el(\"p\", \"lb-mix-match__modal-subtitle\", { \"data-modal-subtitle\": \"\" }),\n );\n top.appendChild(heading);\n\n const close = el(\"button\", \"lb-mix-match__modal-close\", {\n type: \"button\",\n \"data-modal-close\": \"\",\n \"aria-label\": \"Close\",\n });\n close.appendChild(\n icon([\n [\"5\", \"5\", \"15\", \"15\"],\n [\"15\", \"5\", \"5\", \"15\"],\n ]),\n );\n top.appendChild(close);\n header.appendChild(top);\n\n const progress = el(\n \"div\",\n \"lb-mix-match__progress lb-mix-match__modal-progress\",\n { \"data-progress\": \"\" },\n );\n const segments = el(\"div\", \"lb-mix-match__progress-segments\", {\n role: \"progressbar\",\n \"aria-valuenow\": \"0\",\n \"aria-valuemin\": \"0\",\n \"aria-valuemax\": String(opts.segmentCount ?? 0),\n });\n if (opts.wizard) {\n segments.setAttribute(\"data-modal-step-segments\", \"\");\n } else {\n for (let i = 0; i < (opts.segmentCount ?? 0); i++) {\n segments.appendChild(\n el(\"span\", \"lb-mix-match__progress-segment\", {\n \"data-progress-segment\": \"\",\n }),\n );\n }\n }\n progress.appendChild(segments);\n header.appendChild(progress);\n dialog.appendChild(header);\n\n /* ── Search and filters ────────────────────────────────────── */\n\n if (opts.showSearch) {\n const search = el(\"div\", \"lb-mix-match__modal-search\");\n const input = el(\"input\", \"lb-mix-match__modal-search-input\", {\n type: \"text\",\n \"data-modal-search\": \"\",\n role: \"searchbox\",\n \"aria-label\": \"Search products\",\n placeholder: \"Search products\",\n autocomplete: \"off\",\n });\n search.appendChild(input);\n\n const clear = el(\"button\", \"lb-mix-match__modal-search-clear\", {\n type: \"button\",\n \"data-modal-search-clear\": \"\",\n \"aria-label\": \"Clear search\",\n style: \"display: none;\",\n });\n clear.appendChild(\n icon([\n [\"5\", \"5\", \"15\", \"15\"],\n [\"15\", \"5\", \"5\", \"15\"],\n ]),\n );\n search.appendChild(clear);\n dialog.appendChild(search);\n }\n\n if (opts.showTypeFilters) {\n dialog.appendChild(\n el(\"div\", \"lb-mix-match__filters\", {\n \"data-modal-filters\": \"\",\n role: \"group\",\n \"aria-label\": \"Filter by type\",\n }),\n );\n }\n\n /* ── List, empty state, live region ────────────────────────── */\n\n dialog.appendChild(\n el(\"div\", \"lb-mix-match__modal-list\", { \"data-modal-list\": \"\" }),\n );\n\n const empty = el(\"div\", \"lb-mix-match__modal-empty\", {\n \"data-modal-empty\": \"\",\n style: \"display: none;\",\n });\n const emptyText = document.createElement(\"p\");\n emptyText.textContent = \"No products match your search\";\n empty.appendChild(emptyText);\n dialog.appendChild(empty);\n\n dialog.appendChild(\n el(\"span\", \"lb-visually-hidden\", {\n \"data-modal-live\": \"\",\n \"aria-live\": \"polite\",\n }),\n );\n\n /* ── Footer ────────────────────────────────────────────────── */\n\n const footer = el(\n \"div\",\n opts.wizard\n ? \"lb-mix-match__modal-footer lb-multi-step__modal-footer\"\n : \"lb-mix-match__modal-footer\",\n );\n\n if (opts.wizard) {\n const back = el(\"button\", \"lb-multi-step__modal-back\", {\n type: \"button\",\n \"data-modal-back\": \"\",\n style: \"display: none;\",\n });\n back.textContent = \"Back\";\n footer.appendChild(back);\n }\n\n footer.appendChild(\n el(\"span\", \"lb-mix-match__modal-footer-count\", {\n \"data-modal-footer-count\": \"\",\n \"aria-live\": \"polite\",\n }),\n );\n\n if (opts.wizard) {\n const next = el(\"button\", \"lb-mix-match__modal-done\", {\n type: \"button\",\n \"data-modal-next\": \"\",\n style: \"display: none;\",\n });\n next.textContent = \"Next\";\n footer.appendChild(next);\n }\n\n const done = el(\"button\", \"lb-mix-match__modal-done\", {\n type: \"button\",\n \"data-modal-done\": \"\",\n ...(opts.wizard ? { style: \"display: none;\" } : {}),\n });\n done.textContent = \"Done\";\n footer.appendChild(done);\n\n dialog.appendChild(footer);\n overlay.appendChild(dialog);\n\n return overlay;\n}\n","import type { CartLineInput, MixMatchBundleData } from \"@lime-bundles/core\";\nimport { adaptProducts } from \"@lime-bundles/render/adapt\";\nimport { bundleLineAttributes, intlFormatMoney } from \"@lime-bundles/render/host\";\nimport { createPickerModal } from \"@lime-bundles/render/mix-match/modal\";\nimport {\n updatePricing,\n updateProgress,\n updateSlotUI,\n} from \"@lime-bundles/render/mix-match/slots\";\nimport type { EligibleProduct } from \"@lime-bundles/render/mix-match/types\";\nimport {\n alreadyInBundleForVariant,\n atCapacity,\n canRemovePick,\n productUnitsInOtherSlots,\n remainingUnits,\n swapUnitsFor,\n} from \"@lime-bundles/render/mix-match/rules\";\nimport {\n committedQtyForVariant,\n indexOfVariantInBundle,\n mergeRuleMaps,\n ruleFor,\n totalUnits,\n variantRuleFor,\n type SelectedItem,\n} from \"@lime-bundles/render/picker/rules\";\nimport { calculateDiscount } from \"@lime-bundles/core\";\nimport { updatePricing as paintPricing } from \"./pricing-paint\";\nimport type { RowCapacity } from \"@lime-bundles/render/picker/row\";\nimport { seedSelection } from \"@lime-bundles/render/mix-match/seed\";\nimport { buildPickerModal } from \"@lime-bundles/render/picker-modal\";\nimport { buildProgressBar, buildWidgetShell } from \"@lime-bundles/render/skeleton\";\nimport { initCountdown } from \"@lime-bundles/render/countdown\";\nimport { DEFAULT_STRINGS } from \"@lime-bundles/render/strings\";\nimport { bindDropdowns } from \"./dropdown-host\";\n\n/**\n * Mix & match, headless.\n *\n * The picker itself was already fully client-rendered on both hosts, so this\n * only has to supply what Liquid supplies in a theme: the widget shell, the\n * modal shell, and the eligible-product pool shaped the way the picker expects.\n */\nexport function renderMixMatchBundle(\n container: HTMLElement,\n bundle: MixMatchBundleData,\n onAddToCart: (lines: CartLineInput[]) => void,\n onCleanup?: (fn: () => void) => void,\n currentProductHandle?: string | null,\n): void {\n const wc = bundle.widgetConfig;\n const t = DEFAULT_STRINGS;\n const requiredQty = bundle.minQuantity || 1;\n const rulesMap = mergeRuleMaps(bundle.productRules, bundle.variantRules);\n\n // The picker's pool is the bundle's products in render-layer shape, plus the\n // per-product availability flag it gates rows on. `adapted` is positionally\n // aligned with `bundle.products`, which still carries the Storefront-shaped\n // `productType` the filter pills group by.\n const adapted = adaptProducts(bundle);\n const eligibleProducts: EligibleProduct[] = adapted.map((p, i) => ({\n id: Number(p.productId),\n title: p.title ?? \"\",\n url: p.url ?? null,\n type: bundle.products[i]?.productType ?? \"\",\n featuredImage: p.featuredImage ?? null,\n available: p.variants.some((v) => v.available),\n optionNames: p.optionNames,\n variants: p.variants.map((v) => ({\n id: Number(v.id),\n title: v.title,\n options: v.options,\n available: v.available,\n price: v.price,\n compareAtPrice: v.compareAtPrice,\n unitPrice: v.unitPrice,\n image: v.image,\n inventoryQuantity: v.inventoryQuantity ?? null,\n })),\n }));\n if (!eligibleProducts.length) return;\n\n // An empty pool can't build anything: hide, matching the theme's units\n // gate and React's in-stock count. Partial stock renders greyed.\n const poolUnits = eligibleProducts.filter((p) => p.available).length;\n if (poolUnits === 0) return;\n\n const formatMoney = intlFormatMoney(\n bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? \"USD\",\n );\n\n const shell = buildWidgetShell(\"lb-mix-match\", {\n title: bundle.title,\n subtitle: bundle.description,\n showSavingsBar: wc.savingsBar?.visible !== false,\n showComparePrice: wc.pricing?.showCompareAtPrice !== false,\n ctaText: wc.cta?.ctaText || t.addToCart || \"Add to cart\",\n endsAt: wc.countdown?.showCountdown === false ? null : bundle.endsAt,\n });\n shell.root.setAttribute(\"data-required-quantity\", String(requiredQty));\n\n // The picker reads its slots and progress from these hooks, which Liquid\n // emits in a theme.\n shell.products.before(buildProgressBar(requiredQty));\n\n const slots = document.createElement(\"div\");\n slots.className = \"lb-mix-match__slots lb-edge-fade\";\n slots.setAttribute(\"data-selection-slots\", \"\");\n shell.products.replaceWith(slots);\n\n const addTrigger = document.createElement(\"button\");\n addTrigger.type = \"button\";\n addTrigger.className = \"lb-mix-match__add-product\";\n addTrigger.setAttribute(\"data-add-product-trigger\", \"\");\n addTrigger.textContent = t.addItem ? \"Add a product\" : \"Add a product\";\n slots.after(addTrigger);\n\n // Same merchant toggles the Liquid host reads (lb-mix-match.liquid's\n // show_search / show_type_filters / show_quantity_selector params).\n const overlay = buildPickerModal({\n bundleGid: bundle.id,\n domId: bundle.id.replace(/\\D/g, \"\"),\n showSearch: wc.showSearch !== false,\n showTypeFilters: wc.mixMatchShowTypeFilters !== false,\n segmentCount: requiredQty,\n wizard: false,\n });\n shell.root.appendChild(overlay);\n\n // Same seeding the theme host does on a product page: required picks\n // first, else a courtesy pick of the product the shopper is looking at,\n // else the first that qualifies. The theme always runs on a PDP; this host\n // only has that context when a product handle resolved, so an embed on a\n // landing page still starts empty.\n const currentIdx = currentProductHandle\n ? bundle.products.findIndex((p) => p.handle === currentProductHandle)\n : -1;\n const selectedItems: SelectedItem[] = seedSelection(\n eligibleProducts,\n rulesMap,\n requiredQty,\n {\n enabled: !!currentProductHandle,\n currentProductId:\n currentIdx !== -1 ? eligibleProducts[currentIdx]?.id ?? null : null,\n },\n );\n\n const rowCapacity: RowCapacity = {\n remainingSpots: () => remainingUnits(selectedItems, requiredQty),\n ownQty: (v) => committedQtyForVariant(selectedItems, v),\n variantUnitsElsewhere: (v) => alreadyInBundleForVariant(selectedItems, v),\n productUnitsElsewhere: (p, v) => productUnitsInOtherSlots(selectedItems, p, v),\n swapUnits: (p, v) => swapUnitsFor(selectedItems, p, v, rulesMap),\n isInBundle: (v) => indexOfVariantInBundle(selectedItems, v) !== -1,\n canRemove: (v) => {\n const i = indexOfVariantInBundle(selectedItems, v);\n return i !== -1 && canRemovePick(selectedItems, selectedItems[i], rulesMap);\n },\n isFull: () => atCapacity(selectedItems, requiredQty),\n commitQty: (v, qty) => {\n const i = indexOfVariantInBundle(selectedItems, v);\n if (i === -1 || selectedItems[i].quantity === qty) return false;\n selectedItems[i].quantity = qty;\n return true;\n },\n };\n\n function updateAll(): void {\n updateSlotUI(shell.root, selectedItems, requiredQty, bundle as never, {\n formatMoney,\n rulesMap,\n onRemove: (i) => {\n if (!canRemovePick(selectedItems, selectedItems[i], rulesMap)) return;\n selectedItems.splice(i, 1);\n updateAll();\n if (modal.isOpen()) modal.refresh();\n },\n onEdit: () => modal.open(),\n });\n updatePricing(\n shell.root,\n selectedItems,\n requiredQty,\n // The shared updater reads the theme data-block shape: discount fields\n // flat, not nested under discountConfig. Passing the bundle itself left\n // `discountType` undefined, so the summary never painted.\n {\n discountType: bundle.discountConfig.discountType,\n discountValue: bundle.discountConfig.discountValue,\n } as never,\n (total, type, value) =>\n calculateDiscount(total, type as \"percentage\" | \"fixed_amount\", value),\n paintPricing(formatMoney),\n );\n updateProgress(shell.root, modal.overlay, selectedItems, requiredQty, t);\n }\n\n const modal = createPickerModal({\n container: shell.root,\n data: { translations: t, discountType: bundle.discountConfig.discountType, discountValue: bundle.discountConfig.discountValue } as never,\n t,\n rulesMap,\n eligibleProducts,\n requiredQty,\n showQtySelector: wc.mixMatchShowQuantitySelector !== false,\n items: selectedItems,\n formatMoney,\n rowCapacity,\n dropdown: undefined,\n // Stays inside the shadow root, which carries the scoped styles.\n portalTarget: null,\n fallbackFocus: addTrigger,\n onSelectionChanged: updateAll,\n addSelection: (item) => {\n if (totalUnits(selectedItems) + (item.quantity || 1) > requiredQty) return;\n // The product envelope, minus what sibling variants already hold. The\n // variant's own cap binds on top of it and is NOT reduced by siblings.\n const vMax = variantRuleFor(item.variantId, rulesMap)?.max;\n let headroom =\n ruleFor(item.productId, rulesMap).max -\n productUnitsInOtherSlots(selectedItems, item.productId, item.variantId);\n if (typeof vMax === \"number\") headroom = Math.min(headroom, vMax);\n if ((item.quantity || 1) > headroom) return;\n selectedItems.push(item);\n updateAll();\n },\n removeVariant: (v) => {\n const i = indexOfVariantInBundle(selectedItems, v);\n if (i === -1 || !canRemovePick(selectedItems, selectedItems[i], rulesMap)) return;\n selectedItems.splice(i, 1);\n updateAll();\n },\n swapVariant: (item, qty) => {\n let freed = 0;\n for (let i = selectedItems.length - 1; i >= 0 && freed < qty; i--) {\n const it = selectedItems[i];\n if (it.productId !== item.productId || it.variantId === item.variantId) continue;\n const take = Math.min(it.quantity || 1, qty - freed);\n if (take >= (it.quantity || 1)) selectedItems.splice(i, 1);\n else it.quantity = (it.quantity || 1) - take;\n freed += take;\n }\n if (freed > 0) {\n item.quantity = freed;\n selectedItems.push(item);\n updateAll();\n }\n },\n swapUnitsFor: (p, v) => swapUnitsFor(selectedItems, p, v, rulesMap),\n isInBundle: (v) => indexOfVariantInBundle(selectedItems, v) !== -1,\n atCapacity: () => atCapacity(selectedItems, requiredQty),\n });\n\n addTrigger.addEventListener(\"click\", () => {\n if (!modal.isOpen()) modal.open();\n });\n\n shell.cta.addEventListener(\"click\", () => {\n if (totalUnits(selectedItems) < requiredQty) return;\n onAddToCart(\n selectedItems.map((item) => ({\n merchandiseId: \"gid://shopify/ProductVariant/\" + item.variantId,\n quantity: item.quantity || 1,\n attributes: bundleLineAttributes(bundle.id, bundle.bundleType),\n })),\n );\n });\n\n container.appendChild(shell.root);\n initCountdown(shell.root);\n updateAll();\n\n onCleanup?.(bindDropdowns(shell.root));\n}\n","/**\n * Per-tier stock gating for the volume widget, shared by both hosts.\n *\n * A \"buy 6\" tier with 3 units left used to render live everywhere; the\n * shopper clicked through to a cart Shopify capped at add time, paid full\n * price, and saw no discount. A tier the current variant can't cover now\n * greys out and refuses selection.\n *\n * `cap` is the same contract as the picker's `inventoryQuantity`: the units\n * the current variant can supply, or `null` when stock never caps\n * (untracked, backorder-allowed, or unknown) — null fails open to \"every\n * tier available\".\n */\n\nconst TIER_OOS_CLASS = \"lb-volume__tier--oos\";\n\nexport function tierQty(el: HTMLElement): number {\n return parseInt(el.getAttribute(\"data-tier-qty\") ?? \"\", 10) || 0;\n}\n\nexport function tierUnavailable(el: HTMLElement): boolean {\n return el.getAttribute(\"aria-disabled\") === \"true\";\n}\n\n/** Stamp availability state onto every tier row for the current variant. */\nexport function applyTierStockState(\n tierEls: ArrayLike<HTMLElement>,\n cap: number | null,\n): void {\n for (let i = 0; i < tierEls.length; i++) {\n const el = tierEls[i];\n const unavailable = cap !== null && tierQty(el) > cap;\n if (unavailable) {\n el.classList.add(TIER_OOS_CLASS);\n el.setAttribute(\"aria-disabled\", \"true\");\n } else {\n el.classList.remove(TIER_OOS_CLASS);\n el.removeAttribute(\"aria-disabled\");\n }\n }\n}\n\n/**\n * The tier index selection should land on: `preferred` when it is still\n * available, otherwise the first available tier, otherwise -1 (nothing the\n * variant can cover — the caller locks the CTA).\n */\nexport function resolveAvailableTierIndex(\n tierEls: ArrayLike<HTMLElement>,\n preferred: number,\n): number {\n const p = tierEls[preferred];\n if (p && !tierUnavailable(p)) return preferred;\n for (let i = 0; i < tierEls.length; i++) {\n if (!tierUnavailable(tierEls[i])) return i;\n }\n return -1;\n}\n\n/** Next selectable index for arrow-key movement; skips greyed tiers. */\nexport function stepToAvailableTier(\n tierEls: ArrayLike<HTMLElement>,\n from: number,\n direction: 1 | -1,\n): number {\n let i = from + direction;\n while (i >= 0 && i < tierEls.length) {\n if (!tierUnavailable(tierEls[i])) return i;\n i += direction;\n }\n return from;\n}\n","import type { FormatMoney } from \"../host\";\nimport type { VolumeTranslations } from \"./types\";\n\n/**\n * Normalise an Ajax Product API price to integer cents.\n *\n * The API returns cents as a number (12900 for $129). The string branch is a\n * guard for contexts that have been observed to return a decimal string.\n */\nexport function toCents(price: number | string): number {\n if (typeof price === \"string\") return Math.round(parseFloat(price) * 100);\n return price;\n}\n\n// NOTE: the tier index resolution helpers (bestValueTierIndex,\n// resolveDefaultTierIndex, resolvePopularTierIndex) live in\n// @lime-bundles/core (bundle/tier-calculator) so the React SDK shares them.\n// They are NOT re-exported here: a re-export of the core barrel would inline\n// the whole core bundle into the committed theme asset, which never uses\n// them — Liquid resolves the indices server-side. Hosts that need them\n// import them from core directly.\n\n/**\n * Per-unit price for one tier.\n *\n * Percentage floors the discount per unit, matching the checkout's rounding —\n * the same rule the fixed bundle follows.\n *\n * Currency: `data-tier-amt` must be in the same currency as `basePrice` —\n * the currency the host renders in. The merchant configures the amount in\n * the shop's STORE currency, so each host converts before this code runs:\n * the theme entry rewrites `data-tier-amt` at hydration from\n * `Shopify.currency.rate`, and the headless renderer builds the tier from a\n * bundle already converted by the consumer-supplied `currencyRate`. See the\n * currency note on `computeBundlePricing` in ../fixed/pricing.ts.\n */\nexport function calcTierPrice(\n basePrice: number,\n discountType: string,\n tierEl: HTMLElement,\n): number {\n if (discountType === \"fixed_amount\") {\n const amt = parseInt(tierEl.getAttribute(\"data-tier-amt\") ?? \"\", 10) || 0;\n return Math.max(0, basePrice - amt);\n }\n const pct = parseInt(tierEl.getAttribute(\"data-tier-pct\") ?? \"\", 10) || 0;\n return Math.max(0, basePrice - Math.floor((basePrice * pct) / 100));\n}\n\nexport interface TierTotals {\n qty: number;\n priceEach: number;\n totalPrice: number;\n undiscountedTotal: number;\n totalSavings: number;\n}\n\n/** Derive one tier's totals. Pure, so the arithmetic is testable without a DOM. */\nexport function computeTierTotals(\n basePrice: number,\n discountType: string,\n tierEl: HTMLElement,\n): TierTotals {\n const qty = parseInt(tierEl.getAttribute(\"data-tier-qty\") ?? \"\", 10);\n const priceEach = calcTierPrice(basePrice, discountType, tierEl);\n const totalPrice = priceEach * qty;\n const undiscountedTotal = basePrice * qty;\n return {\n qty,\n priceEach,\n totalPrice,\n undiscountedTotal,\n totalSavings: undiscountedTotal - totalPrice,\n };\n}\n\n/**\n * Resolve the \"(N items)\" label for a quantity.\n *\n * Uses Intl.PluralRules against the shop's locale so languages with more than\n * two plural forms read correctly; `__COUNT__` is the placeholder the Liquid\n * translation carries.\n */\nexport function formatItemCount(\n qty: number,\n translations: VolumeTranslations,\n): string | null {\n if (!translations.itemCount) return null;\n const form = new Intl.PluralRules(translations.locale || \"en\").select(qty);\n const tpl =\n translations.itemCount[form] || translations.itemCount.other || \"__COUNT__\";\n return \" (\" + tpl.split(\"__COUNT__\").join(String(qty)) + \")\";\n}\n\n/** Repaint every tier row's per-unit and compare-at price after a base price change. */\nexport function updateAllTierPrices(\n allTiers: ArrayLike<HTMLElement>,\n basePrice: number,\n discountType: string,\n formatMoney: FormatMoney,\n): void {\n for (let i = 0; i < allTiers.length; i++) {\n const tierEl = allTiers[i];\n const priceEachEl = tierEl.querySelector<HTMLElement>(\n \"[data-tier-price-each]\",\n );\n if (priceEachEl) {\n priceEachEl.textContent = formatMoney(\n calcTierPrice(basePrice, discountType, tierEl),\n );\n }\n const compareEl = tierEl.querySelector<HTMLElement>(\n \".lb-volume__tier-compare\",\n );\n if (compareEl) compareEl.textContent = formatMoney(basePrice);\n }\n}\n\n/** Mark a tier selected and repaint the total, item count and savings. */\nexport function selectTier(\n container: HTMLElement,\n allTiers: ArrayLike<HTMLElement>,\n basePrice: number,\n discountType: string,\n index: number,\n translations: VolumeTranslations,\n formatMoney: FormatMoney,\n): void {\n for (let i = 0; i < allTiers.length; i++) {\n allTiers[i].setAttribute(\"aria-checked\", i === index ? \"true\" : \"false\");\n // Roving tabindex: only the selected tier is in the tab order, so the\n // group is one stop and arrow keys move within it.\n allTiers[i].setAttribute(\"tabindex\", i === index ? \"0\" : \"-1\");\n }\n\n const { qty, totalPrice, undiscountedTotal, totalSavings } =\n computeTierTotals(basePrice, discountType, allTiers[index]);\n\n const itemCountEl = container.querySelector<HTMLElement>(\"[data-item-count]\");\n if (itemCountEl) {\n const label = formatItemCount(qty, translations);\n if (label !== null) itemCountEl.textContent = label;\n }\n\n const totalPriceEl = container.querySelector<HTMLElement>(\"[data-total-price]\");\n if (totalPriceEl) totalPriceEl.textContent = formatMoney(totalPrice);\n\n const compareEl = container.querySelector<HTMLElement>(\"[data-compare-price]\");\n if (compareEl) {\n if (totalSavings > 0) {\n compareEl.textContent = formatMoney(undiscountedTotal);\n compareEl.style.display = \"\";\n } else {\n compareEl.style.display = \"none\";\n }\n }\n\n const savingsBar = container.querySelector<HTMLElement>(\"[data-savings-bar]\");\n const savingsAmountEl =\n container.querySelector<HTMLElement>(\"[data-savings-amount]\");\n const savingsPercentEl = container.querySelector<HTMLElement>(\n \"[data-savings-percent]\",\n );\n if (savingsBar) {\n if (totalSavings > 0) {\n if (savingsAmountEl) {\n savingsAmountEl.textContent = formatMoney(totalSavings);\n }\n if (savingsPercentEl) {\n const savingsPct =\n undiscountedTotal > 0\n ? Math.round((totalSavings * 100) / undiscountedTotal)\n : 0;\n savingsPercentEl.textContent = \"(\" + savingsPct + \"%)\";\n }\n savingsBar.style.display = \"\";\n } else {\n savingsBar.style.display = \"none\";\n }\n }\n}\n","import { bundleLineAttributes, intlFormatMoney } from \"@lime-bundles/render/host\";\nimport { buildVolumeTiers, buildWidgetShell, type TierSpec } from \"@lime-bundles/render/skeleton\";\nimport { initCountdown } from \"@lime-bundles/render/countdown\";\nimport { DEFAULT_STRINGS } from \"@lime-bundles/render/strings\";\nimport {\n bestValueTierIndex,\n isVariantFulfillable,\n resolveContextProduct,\n resolveDefaultTierIndex,\n resolvePopularTierIndex,\n stockCapForVariant,\n type CartLineInput,\n type ProductContext,\n type VolumeBundleData,\n} from \"@lime-bundles/core\";\nimport {\n applyTierStockState,\n resolveAvailableTierIndex,\n stepToAvailableTier,\n tierUnavailable,\n} from \"@lime-bundles/render/volume/stock\";\nimport { selectTier, updateAllTierPrices } from \"@lime-bundles/render/volume/pricing\";\nimport { toCents } from \"@lime-bundles/render/adapt\";\nimport { fill } from \"@lime-bundles/render/picker/i18n\";\n\n/**\n * Volume / quantity-break bundle, headless.\n *\n * The tier list is built here rather than hydrated: unlike a product row it has\n * no server-rendered counterpart in this host, and its prices are recomputed\n * from `data-tier-*` by the shared `selectTier` the moment it is built. That is\n * also why volume has no layout-shift exposure — it paints once.\n */\nexport function renderVolumeBundle(\n container: HTMLElement,\n bundle: VolumeBundleData,\n onAddToCart: (lines: CartLineInput[]) => void,\n productContext?: ProductContext,\n): void {\n const wc = bundle.widgetConfig;\n // Price and sell the product page the widget is standing on, the way the\n // theme widget binds to Liquid's `product`; first product only when the\n // host supplied no context.\n const product = resolveContextProduct(bundle.products, productContext);\n if (!product) return;\n\n // Price the tiers from a variant that can cover the smallest tier,\n // falling back through merely-purchasable to the first so a fully\n // sold-out product still renders numbers rather than NaN.\n const variants = product.variants.nodes;\n const minTierQty = Math.min(\n ...bundle.volumeTiers.map((tier) => tier.minQuantity),\n );\n const pricingVariant =\n variants.find((v) => isVariantFulfillable(v, minTierQty)) ??\n variants.find((v) => v.availableForSale) ??\n variants[0];\n if (!pricingVariant) return;\n\n const basePrice = toCents(pricingVariant.price.amount);\n const discountType = bundle.discountConfig.discountType;\n const formatMoney = intlFormatMoney(product.priceRange.minVariantPrice.currencyCode);\n const t = DEFAULT_STRINGS;\n\n // The same three resolutions lb-volume.liquid performs, in the same order:\n // best-savings tier first, then the merchant's default tier and the popular\n // badge position, both of which fall back onto it.\n const bestIdx = bestValueTierIndex(bundle.volumeTiers, discountType);\n const defaultIdx = resolveDefaultTierIndex(\n wc.defaultTier,\n bundle.volumeTiers.length,\n bestIdx,\n );\n const popularIdx = resolvePopularTierIndex(\n wc.popularBadge?.tierIndex,\n wc.popularBadge?.visible !== false,\n bestIdx,\n );\n\n const tiers: TierSpec[] = bundle.volumeTiers.map((tier, i) => {\n const percent = discountType === \"fixed_amount\" ? 0 : Math.round(tier.percentage ?? 0);\n const amountCents = discountType === \"fixed_amount\" ? Math.round((tier.amount ?? 0) * 100) : 0;\n const priceEach =\n discountType === \"fixed_amount\"\n ? Math.max(0, basePrice - amountCents)\n : Math.max(0, basePrice - Math.floor((basePrice * percent) / 100));\n const savedPct =\n basePrice > 0 ? Math.round(((basePrice - priceEach) * 100) / basePrice) : 0;\n\n return {\n quantity: tier.minQuantity,\n percent,\n amountCents,\n label: fill(t.buyQty || \"Buy __COUNT__\", { COUNT: tier.minQuantity }),\n savingsLabel: savedPct > 0 ? \"Save \" + savedPct + \"%\" : null,\n badgeLabel: i === popularIdx ? wc.popularBadge?.text || \"Most popular\" : null,\n selected: i === defaultIdx,\n };\n });\n if (!tiers.length) return;\n\n const shell = buildWidgetShell(\"lb-volume\", {\n title: bundle.title,\n subtitle: bundle.description,\n summaryLabel: \"Total\",\n withItemCount: wc.pricing?.showItemCount !== false,\n totalPriceSlot: true,\n showSavingsBar: wc.savingsBar?.visible !== false,\n showComparePrice: wc.pricing?.showCompareAtPrice !== false,\n ctaText: wc.cta?.ctaText || t.addToCart || \"Add to cart\",\n endsAt: wc.countdown?.showCountdown === false ? null : bundle.endsAt,\n });\n\n const group = buildVolumeTiers(tiers, \"Select quantity\", {\n showCompare: wc.pricing?.showComparePrice !== false,\n showPriceEach: wc.pricing?.showPerUnitPrice !== false,\n unitLabel: (t.each || \"each\").trim(),\n });\n shell.products.replaceWith(group);\n\n const tierEls = group.querySelectorAll<HTMLElement>(\"[data-tier-index]\");\n // Paint each row's struck base price and discounted per-unit price — the\n // Liquid renders these server-side; this host has to fill its own skeleton.\n updateAllTierPrices(tierEls, basePrice, discountType, formatMoney);\n\n // Grey the tiers the pricing variant can't cover, and land the initial\n // selection on one it can.\n applyTierStockState(tierEls, stockCapForVariant(pricingVariant));\n let selectedIndex = resolveAvailableTierIndex(tierEls, defaultIdx);\n const noTierAvailable = selectedIndex === -1;\n if (noTierAvailable) selectedIndex = defaultIdx;\n\n const apply = () =>\n selectTier(shell.root, tierEls, basePrice, discountType, selectedIndex, t, formatMoney);\n\n group.addEventListener(\"click\", (e) => {\n const el = (e.target as HTMLElement).closest<HTMLElement>(\"[data-tier-index]\");\n if (!el || tierUnavailable(el)) return;\n selectedIndex = parseInt(el.getAttribute(\"data-tier-index\") ?? \"\", 10) || 0;\n apply();\n });\n\n // Arrow keys move within the group, matching the roving tabindex selectTier\n // maintains. Greyed tiers are skipped, not landed on.\n group.addEventListener(\"keydown\", (e) => {\n if (e.key === \"ArrowDown\" || e.key === \"ArrowRight\") {\n e.preventDefault();\n selectedIndex = stepToAvailableTier(tierEls, selectedIndex, 1);\n apply();\n tierEls[selectedIndex].focus();\n } else if (e.key === \"ArrowUp\" || e.key === \"ArrowLeft\") {\n e.preventDefault();\n selectedIndex = stepToAvailableTier(tierEls, selectedIndex, -1);\n apply();\n tierEls[selectedIndex].focus();\n }\n });\n\n if (noTierAvailable) {\n shell.cta.disabled = true;\n const label = shell.cta.querySelector(\"[data-cta-label]\");\n if (label) label.textContent = t.outOfStock || \"Out of stock\";\n } else {\n shell.cta.addEventListener(\"click\", () => {\n const qty = parseInt(\n tierEls[selectedIndex].getAttribute(\"data-tier-qty\") ?? \"\",\n 10,\n );\n onAddToCart([\n {\n merchandiseId: pricingVariant.id,\n quantity: qty,\n attributes: bundleLineAttributes(bundle.id, bundle.bundleType),\n },\n ]);\n });\n }\n\n container.appendChild(shell.root);\n initCountdown(shell.root);\n apply();\n}\n","import type { FormatMoney } from \"../host\";\nimport { findVariant } from \"../product/variants\";\nimport type { BundleProduct, BundleVariant } from \"../product/types\";\n\n/** One side of the offer. Same shape as any bundle product, plus its role. */\nexport interface BogoSide extends BundleProduct {\n role: \"buy\" | \"get\";\n}\n\n/** Per-unit discounted price, floored the way Shopify's discount engine rounds. */\nexport function discountedUnit(priceCents: number, percent: number): number {\n return priceCents - Math.floor((priceCents * percent) / 100);\n}\n\nexport interface BogoTotals {\n totalPrice: number;\n salePrice: number;\n savings: number;\n}\n\n/**\n * Totals for one reward set: N of the buy side plus M of the get side, with\n * the discount applied only to the get side.\n *\n * Pure, so the arithmetic is testable without a DOM.\n */\nexport function computeBogoTotals(\n sides: BogoSide[],\n percent: number,\n buyQty: number,\n getQty: number,\n): BogoTotals {\n const buyVariant = findVariant(sides[0], sides[0].selectedVariantId);\n const getVariant = findVariant(sides[1], sides[1].selectedVariantId);\n\n const totalPrice = buyVariant.price * buyQty + getVariant.price * getQty;\n const salePrice =\n buyVariant.price * buyQty +\n discountedUnit(getVariant.price, percent) * getQty;\n\n return { totalPrice, salePrice, savings: totalPrice - salePrice };\n}\n\n/**\n * Reprice one card's inline price elements.\n *\n * The get card shows the struck original beside the discounted unit — the same\n * treatment the admin preview and the headless renderers use. Liquid renders\n * the plain price server-side and this applies the reward look.\n */\nexport function repriceRow(\n row: HTMLElement,\n variant: BundleVariant,\n percent: number,\n isGet: boolean,\n formatMoney: FormatMoney,\n): void {\n const priceEl = row.querySelector<HTMLElement>(\"[data-product-price]\");\n const compareEl = row.querySelector<HTMLElement>(\n \"[data-product-compare-price]\",\n );\n if (!priceEl) return;\n\n if (isGet && percent > 0) {\n priceEl.textContent = formatMoney(discountedUnit(variant.price, percent));\n if (compareEl) {\n compareEl.textContent = formatMoney(variant.price);\n compareEl.hidden = false;\n }\n return;\n }\n\n priceEl.textContent = formatMoney(variant.price);\n if (compareEl) {\n const compare = variant.compareAtPrice;\n if (compare && compare > variant.price) {\n compareEl.textContent = formatMoney(compare);\n compareEl.hidden = false;\n } else {\n compareEl.textContent = \"\";\n compareEl.hidden = true;\n }\n }\n}\n\n/** Recompute and paint the bundle summary. */\nexport function recalcPricing(\n container: HTMLElement,\n sides: BogoSide[],\n percent: number,\n buyQty: number,\n getQty: number,\n formatMoney: FormatMoney,\n): void {\n const { totalPrice, salePrice, savings } = computeBogoTotals(\n sides,\n percent,\n buyQty,\n getQty,\n );\n\n const saleEl = container.querySelector<HTMLElement>(\"[data-sale-price]\");\n const compareEl = container.querySelector<HTMLElement>(\"[data-compare-price]\");\n const savingsBar = container.querySelector<HTMLElement>(\"[data-savings-bar]\");\n const savingsAmountEl =\n container.querySelector<HTMLElement>(\"[data-savings-amount]\");\n const savingsPercentEl = container.querySelector<HTMLElement>(\n \"[data-savings-percent]\",\n );\n\n if (saleEl) saleEl.textContent = formatMoney(salePrice);\n\n if (compareEl) {\n if (savings > 0) {\n compareEl.textContent = formatMoney(totalPrice);\n compareEl.style.display = \"\";\n } else {\n compareEl.style.display = \"none\";\n }\n }\n\n if (savingsBar) {\n if (savings > 0) {\n if (savingsAmountEl) savingsAmountEl.textContent = formatMoney(savings);\n if (savingsPercentEl) {\n const pct =\n totalPrice > 0 ? Math.round((savings * 100) / totalPrice) : 0;\n savingsPercentEl.textContent = \"(\" + pct + \"%)\";\n }\n savingsBar.style.display = \"\";\n } else {\n savingsBar.style.display = \"none\";\n }\n }\n}\n","import { discountedUnit, type BogoSide } from \"./pricing\";\nimport { findVariant } from \"../product/variants\";\nimport {\n BUNDLE_ROLE_ATTRIBUTE,\n BUNDLE_ROLE_GET,\n type CartItem,\n} from \"../host\";\n\n/**\n * Build the add-to-cart lines for one reward set.\n *\n * When both sides resolve to the same variant they merge into a single line of\n * buyQty + getQty, so the theme's cart doesn't show the same product twice.\n * The discount function's same-product pool maths is line-agnostic, so\n * checkout charges identically either way.\n *\n * A distinct reward line carries `_lime_bundle_role: \"get\"`, which is how the\n * discount function knows which line the shopper chose. Without it the\n * function fell back to cheapest-first over every line of the get product —\n * including the buy line on a same-product offer — and discounted a variant\n * the widget had not struck through (issue #421). The merged line carries no\n * role: one line, nothing to choose.\n */\nexport function buildBogoCartItems(\n sides: BogoSide[],\n percent: number,\n buyQty: number,\n getQty: number,\n): CartItem[] {\n const buyVariant = findVariant(sides[0], sides[0].selectedVariantId);\n const getVariant = findVariant(sides[1], sides[1].selectedVariantId);\n\n const buyLine: CartItem = {\n variantId: Number(sides[0].selectedVariantId),\n quantity: buyQty,\n priceCents: (buyVariant.price || 0) * buyQty,\n };\n const getPriceCents = discountedUnit(getVariant.price || 0, percent) * getQty;\n\n if (String(sides[0].selectedVariantId) === String(sides[1].selectedVariantId)) {\n return [\n {\n variantId: buyLine.variantId,\n quantity: buyQty + getQty,\n priceCents: (buyLine.priceCents ?? 0) + getPriceCents,\n },\n ];\n }\n\n return [\n buyLine,\n {\n variantId: Number(sides[1].selectedVariantId),\n quantity: getQty,\n priceCents: getPriceCents,\n attributes: [{ key: BUNDLE_ROLE_ATTRIBUTE, value: BUNDLE_ROLE_GET }],\n },\n ];\n}\n","import type { BogoBundleData, CartLineInput } from \"@lime-bundles/core\";\nimport { adaptProduct } from \"@lime-bundles/render/adapt\";\nimport { buildBogoCartItems } from \"@lime-bundles/render/bogo/cart\";\nimport { recalcPricing, repriceRow, type BogoSide } from \"@lime-bundles/render/bogo/pricing\";\nimport { bundleLineAttributes, intlFormatMoney } from \"@lime-bundles/render/host\";\nimport { bindVariantSelects } from \"@lime-bundles/render/product/option-selects\";\nimport { hydrateRowControls } from \"@lime-bundles/render/product/hydrate\";\nimport { updateThumbnail } from \"@lime-bundles/render/product/row\";\nimport { findVariant } from \"@lime-bundles/render/product/variants\";\nimport { buildRowSkeleton, buildWidgetShell } from \"@lime-bundles/render/skeleton\";\nimport { initCountdown } from \"@lime-bundles/render/countdown\";\nimport { DEFAULT_STRINGS, formatItemsOutOfStock } from \"@lime-bundles/render/strings\";\nimport { bindDropdowns } from \"./dropdown-host\";\n\n/** The \"+\" medallion between the buy and get cards — the DOM twin of the one\n * `lb-bogo.liquid` renders. */\nfunction buildBogoPlus(): HTMLElement {\n const plus = document.createElement(\"div\");\n plus.className = \"lb-bogo__plus\";\n plus.setAttribute(\"aria-hidden\", \"true\");\n plus.innerHTML =\n '<svg width=\"12\" height=\"12\" viewBox=\"0 0 18 18\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\">' +\n '<line x1=\"9\" y1=\"3\" x2=\"9\" y2=\"15\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"/>' +\n '<line x1=\"3\" y1=\"9\" x2=\"15\" y2=\"9\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"/>' +\n \"</svg>\";\n return plus;\n}\n\n/**\n * Buy X Get Y, headless.\n *\n * Two rows, one per side of the offer, built from the same skeleton the theme\n * gets from Liquid and hydrated through the same path. The reward side's\n * discount is applied on hydration rather than baked into the markup, which is\n * exactly why its price box has to be reserved.\n *\n * All-or-nothing: either side being unfulfillable locks the CTA, and `hide`\n * suppresses the widget entirely.\n */\nexport function renderBogoBundle(\n container: HTMLElement,\n bundle: BogoBundleData,\n onAddToCart: (lines: CartLineInput[]) => void,\n onCleanup?: (fn: () => void) => void,\n): void {\n const wc = bundle.widgetConfig;\n const buyProduct = bundle.products.find((p) => p.id === bundle.buyProductId);\n const getProduct = bundle.products.find((p) => p.id === bundle.getProductId);\n if (!buyProduct || !getProduct) return;\n\n const buyQty = bundle.buyQuantity;\n const getQty = bundle.getQuantity;\n const percent = bundle.discountConfig.discountValue;\n\n const quantities = {\n productQuantities: bundle.productQuantities,\n variantQuantities: bundle.variantQuantities,\n };\n // Per-side narrowing (issue #436): the positional selectedVariantIds\n // cannot hold two lists for a same-product BOGO, so each side carries its\n // own. An empty (or absent, for externally assembled data) list means the\n // merchant left that side open.\n const buyAllowed = bundle.buyVariantIds?.length ? bundle.buyVariantIds : null;\n const getAllowed = bundle.getVariantIds?.length ? bundle.getVariantIds : null;\n const sides: BogoSide[] = [\n {\n ...adaptProduct(buyProduct, bundle.id, {\n quantities,\n allowedVariantIds: buyAllowed,\n }),\n role: \"buy\",\n },\n {\n ...adaptProduct(getProduct, bundle.id, {\n quantities,\n allowedVariantIds: getAllowed,\n }),\n role: \"get\",\n },\n ];\n\n // Sold-out sides render greyed with the CTA locked; the status flip\n // hides a bundle that can't be bought.\n const oosCount = sides.filter((s) => !s.variants.some((v) => v.available)).length;\n\n const formatMoney = intlFormatMoney(\n buyProduct.priceRange.minVariantPrice.currencyCode,\n );\n const t = DEFAULT_STRINGS;\n\n const shell = buildWidgetShell(\"lb-bogo\", {\n title: bundle.title,\n subtitle: bundle.description,\n showSavingsBar: wc.savingsBar?.visible !== false,\n showComparePrice: wc.pricing?.showCompareAtPrice !== false,\n ctaText: wc.cta?.ctaText || t.addToCart || \"Add to cart\",\n endsAt: wc.countdown?.showCountdown === false ? null : bundle.endsAt,\n });\n shell.root.setAttribute(\"data-discount-value\", String(percent));\n shell.root.setAttribute(\"data-buy-qty\", String(buyQty));\n shell.root.setAttribute(\"data-get-qty\", String(getQty));\n\n const badgeText = percent >= 100 ? \"Free\" : percent + \"% off\";\n\n sides.forEach((side, i) => {\n const isGet = i === 1;\n // The medallion sits between the two cards, outside either row, exactly\n // where lb-bogo.liquid puts it.\n if (isGet) shell.products.appendChild(buildBogoPlus());\n const isOos = !side.variants.some((v) => v.available);\n const row = buildRowSkeleton(side, t, {\n isOos,\n href: side.url,\n bogoRole: side.role,\n inlineQuantity: isGet ? getQty : buyQty,\n badgeText: isGet && !isOos ? badgeText : null,\n thumbnailRatio: wc.productList?.thumbnailRatio,\n });\n shell.products.appendChild(row);\n if (isOos) return;\n\n const initial = findVariant(side, side.selectedVariantId);\n hydrateRowControls(row, side, initial);\n updateThumbnail(row, initial, side.featuredImage);\n repriceRow(row, initial, percent, isGet, formatMoney);\n\n bindVariantSelects(\n row,\n () => side,\n (variant) => {\n updateThumbnail(row, variant, side.featuredImage);\n repriceRow(row, variant, percent, isGet, formatMoney);\n recalcPricing(shell.root, sides, percent, buyQty, getQty, formatMoney);\n },\n );\n });\n\n if (oosCount > 0) {\n shell.cta.disabled = true;\n const label = shell.cta.querySelector(\"[data-cta-label]\");\n if (label) {\n label.textContent = formatItemsOutOfStock(t, oosCount);\n }\n } else {\n shell.cta.addEventListener(\"click\", () => {\n onAddToCart(\n buildBogoCartItems(sides, percent, buyQty, getQty).map((item) => ({\n merchandiseId: \"gid://shopify/ProductVariant/\" + item.variantId,\n quantity: item.quantity,\n // Per-line extras (the reward marker) on top of the attribution\n // every line carries.\n attributes: [\n ...bundleLineAttributes(bundle.id, bundle.bundleType),\n ...(item.attributes ?? []),\n ],\n })),\n );\n });\n }\n\n container.appendChild(shell.root);\n initCountdown(shell.root);\n recalcPricing(shell.root, sides, percent, buyQty, getQty, formatMoney);\n\n onCleanup?.(bindDropdowns(shell.root));\n}\n","import { fill } from \"../picker/i18n\";\nimport type { MultiStepTranslations } from \"./types\";\n\nexport { formatNeedsSpots } from \"../picker/i18n\";\n\nexport function stepOfLabel(\n i: number,\n total: number,\n t: MultiStepTranslations,\n): string {\n return fill(t.stepOf || \"Step __STEP__ of __TOTAL__\", {\n STEP: i + 1,\n TOTAL: total,\n });\n}\n\nexport function ofSelectedLabel(\n count: number,\n total: number,\n t: MultiStepTranslations,\n): string {\n return fill(t.ofSelected || \"__COUNT__ of __TOTAL__ added\", {\n COUNT: count,\n TOTAL: total,\n });\n}\n\n/**\n * Overall progress counts steps, not units.\n *\n * Unit detail lives on the per-step count chips, so the top-level bar stays\n * legible on a wizard with several steps.\n */\nexport function stepsCompletedLabel(\n completed: number,\n total: number,\n t: MultiStepTranslations,\n): string {\n return fill(t.stepsCompleted || \"__COUNT__ of __TOTAL__ steps completed\", {\n COUNT: completed,\n TOTAL: total,\n });\n}\n\nexport function moreToGoLabel(\n remaining: number,\n t: MultiStepTranslations,\n): string {\n return remaining > 0\n ? fill(t.moreToGo || \"__COUNT__ more to go\", { COUNT: remaining })\n : t.complete || \"Complete\";\n}\n\n/** Resolve a plural-form map (Intl.PluralRules categories) for a count. */\nfunction fillPlural(\n map: Record<string, string>,\n count: number,\n locale: string | undefined,\n): string {\n const form = new Intl.PluralRules(locale || \"en\").select(count);\n return fill(map[form] || map.other || \"__COUNT__\", { COUNT: count });\n}\n\n/** The door's main label: invitation before any pick, continuation after. */\nexport function doorLabel(\n anyPicks: boolean,\n t: MultiStepTranslations,\n): string {\n return anyPicks\n ? t.doorContinue || \"Continue building\"\n : t.doorChoose || \"Choose your products\";\n}\n\n/**\n * The door's sublabel: total step count while untouched (\"4 quick steps\"),\n * remaining unsatisfied steps mid-build (\"2 steps to go\").\n */\nexport function doorSubLabel(\n anyPicks: boolean,\n remaining: number,\n total: number,\n t: MultiStepTranslations,\n): string {\n if (!anyPicks) {\n const quick = t.doorQuickSteps;\n if (typeof quick === \"string\") return fill(quick, { COUNT: total });\n return fillPlural(\n quick || { one: \"__COUNT__ quick step\", other: \"__COUNT__ quick steps\" },\n total,\n t.locale,\n );\n }\n return fillPlural(\n t.doorStepsToGo || {\n one: \"__COUNT__ step to go\",\n other: \"__COUNT__ steps to go\",\n },\n remaining,\n t.locale,\n );\n}\n","import {\n ruleFor,\n variantRuleFor,\n type ProductRule,\n type SelectedItem,\n} from \"../picker/rules\";\n\n/**\n * Step-scoped capacity maths for the multi-step wizard.\n *\n * Selections are an array of arrays — one flat selection per step — so the\n * step-agnostic primitives in ../picker/rules apply per step, and everything\n * here is about how the steps relate. Pure: the state comes in as arguments.\n */\nexport type StepSelections = SelectedItem[][];\n\nexport interface WizardStep {\n title?: string;\n /** Units the shopper must pick before the step is satisfied. */\n minQuantity?: number;\n /** Units the step will accept; null or absent means unbounded. */\n maxQuantity?: number | null;\n eligibleProducts?: unknown[];\n}\n\nexport function unitsInStep(selections: StepSelections, i: number): number {\n let qty = 0;\n for (let j = 0; j < selections[i].length; j++) {\n qty += selections[i][j].quantity || 1;\n }\n return qty;\n}\n\nexport function stepSatisfied(\n selections: StepSelections,\n steps: WizardStep[],\n i: number,\n): boolean {\n return unitsInStep(selections, i) >= (steps[i].minQuantity || 1);\n}\n\n/** Open units left in a step; Infinity when the step declares no maximum. */\nexport function stepHeadroom(\n selections: StepSelections,\n steps: WizardStep[],\n i: number,\n): number {\n const max = steps[i].maxQuantity;\n if (max == null) return Infinity;\n return Math.max(0, max - unitsInStep(selections, i));\n}\n\nexport function allSatisfied(\n selections: StepSelections,\n steps: WizardStep[],\n): boolean {\n for (let i = 0; i < steps.length; i++) {\n if (!stepSatisfied(selections, steps, i)) return false;\n }\n return true;\n}\n\n/**\n * Units of a variant across every step.\n *\n * Stock is one pool for the whole bundle, so a variant picked in step 1 and\n * step 3 draws from the same inventory — this is what stops the wizard\n * overselling across steps.\n */\nexport function variantUnitsInBundle(\n selections: StepSelections,\n variantId: number | string,\n): number {\n let qty = 0;\n for (const step of selections) {\n for (const pick of step) {\n if (pick.variantId === variantId) qty += pick.quantity || 0;\n }\n }\n return qty;\n}\n\n/**\n * Units of a product held anywhere except one step's slot for one variant.\n *\n * `rule.max` caps a product bundle-wide, not per step, so a row's ceiling has\n * to account for the same product picked in other steps.\n */\nexport function productUnitsInOtherSlots(\n selections: StepSelections,\n productId: number | string,\n excludeStep: number,\n excludeVariantId: number | string,\n): number {\n let qty = 0;\n for (let i = 0; i < selections.length; i++) {\n for (const it of selections[i]) {\n if (it.productId !== productId) continue;\n if (i === excludeStep && it.variantId === excludeVariantId) continue;\n qty += it.quantity || 0;\n }\n }\n return qty;\n}\n\n/**\n * Whether a pick may be removed.\n *\n * Step-agnostic, like the rules themselves and the discount function's check:\n * removal is allowed only while the product's other slots — in any step — keep\n * it at or above rule.min.\n *\n * A required VARIANT is stricter and needs no tally. Slots are one per\n * (product, variant), so a required variant has no other slot to fall back on\n * and no sibling may stand in for it — that is the whole point of naming it.\n */\nexport function canRemovePick(\n selections: StepSelections,\n item: SelectedItem,\n rulesMap: Record<string, ProductRule | undefined>,\n): boolean {\n if (variantRuleFor(item.variantId, rulesMap)?.required) return false;\n\n const rule = ruleFor(item.productId, rulesMap);\n if (!rule.required) return true;\n\n let unitsElsewhere = 0;\n for (const step of selections) {\n for (const pick of step) {\n if (pick !== item && pick.productId === item.productId) {\n unitsElsewhere += pick.quantity || 1;\n }\n }\n }\n return unitsElsewhere >= rule.min;\n}\n\n/**\n * Units available for an atomic variant swap within one step, or 0.\n *\n * Cross-step displacement is deliberately off the table: taking units from\n * another step to satisfy this one would drain that step below its own\n * minimum.\n */\nexport function swapUnitsForStep(\n selections: StepSelections,\n i: number,\n productId: number | string,\n variantId: number | string,\n rulesMap: Record<string, ProductRule | undefined>,\n): number {\n const rule = ruleFor(productId, rulesMap);\n if (!rule.required || rule.max !== rule.min) return 0;\n if (indexOfVariantInStep(selections, i, variantId) !== -1) return 0;\n\n let qty = 0;\n for (const it of selections[i]) {\n if (it.productId === productId && it.variantId !== variantId) {\n qty += it.quantity || 1;\n }\n }\n return qty;\n}\n\nexport function indexOfVariantInStep(\n selections: StepSelections,\n i: number,\n variantId: number | string,\n): number {\n for (let j = 0; j < selections[i].length; j++) {\n if (selections[i][j].variantId === variantId) return j;\n }\n return -1;\n}\n\nexport function committedQtyForVariantInStep(\n selections: StepSelections,\n i: number,\n variantId: number | string,\n): number {\n const idx = indexOfVariantInStep(selections, i, variantId);\n return idx === -1 ? 0 : selections[i][idx].quantity || 0;\n}\n\n/** Every pick across every step, in step order — the add-to-cart payload. */\nexport function flattenSelections(selections: StepSelections): SelectedItem[] {\n const out: SelectedItem[] = [];\n for (const step of selections) out.push(...step);\n return out;\n}\n","import {\n doorLabel,\n doorSubLabel,\n moreToGoLabel,\n ofSelectedLabel,\n stepsCompletedLabel,\n} from \"./i18n\";\nimport {\n allSatisfied,\n canRemovePick,\n stepSatisfied,\n unitsInStep,\n type StepSelections,\n} from \"./rules\";\nimport { buildSlotCard } from \"../picker/slot-card\";\nimport type { ProductRule } from \"../picker/rules\";\nimport type { MultiStepData, MultiStepStep, MultiStepTranslations } from \"./types\";\n\nexport interface BodyDeps {\n container: HTMLElement;\n steps: MultiStepStep[];\n selections: StepSelections;\n t: MultiStepTranslations;\n rulesMap: Record<string, ProductRule | undefined>;\n formatMoney: (cents: number) => string;\n onRemove: (step: number, index: number) => void;\n /** Reopen the wizard at a given step. */\n onEdit: (step: number) => void;\n}\n\n/**\n * Rebuild each step's mode, count chips, and slot list.\n *\n * A step with picks presents as an open group (heading + chosen cards); a\n * step without presents as a slim checklist row. Both representations are in\n * the host's markup — `data-step-mode` on the section picks which one shows.\n * The count chip exists in each (heading and row), so chips are updated by\n * attribute across the whole section.\n */\nexport function updateStepGroups(deps: BodyDeps): void {\n const { container, steps, selections, t } = deps;\n\n for (let i = 0; i < steps.length; i++) {\n const min = steps[i].minQuantity || 1;\n\n const section = container.querySelector<HTMLElement>(\n '[data-step-group=\"' + i + '\"]',\n );\n if (section) {\n section.setAttribute(\n \"data-step-mode\",\n selections[i].length > 0 ? \"open\" : \"row\",\n );\n }\n\n const countLabel = ofSelectedLabel(\n Math.min(unitsInStep(selections, i), min),\n min,\n t,\n );\n const met = stepSatisfied(selections, steps, i);\n const countEls = container.querySelectorAll<HTMLElement>(\n '[data-step-count=\"' + i + '\"]',\n );\n for (let c = 0; c < countEls.length; c++) {\n countEls[c].textContent = countLabel;\n countEls[c].classList.toggle(\"lb-multi-step__group-count--met\", met);\n }\n\n const slotsWrap = container.querySelector<HTMLElement>(\n '[data-step-slots=\"' + i + '\"]',\n );\n if (!slotsWrap) continue;\n\n slotsWrap.innerHTML = \"\";\n for (let j = 0; j < selections[i].length; j++) {\n const stepIdx = i;\n const slotIdx = j;\n const item = selections[stepIdx][slotIdx];\n slotsWrap.appendChild(\n buildSlotCard(\n item,\n t,\n deps.formatMoney,\n canRemovePick(selections, item, deps.rulesMap),\n () => deps.onRemove(stepIdx, slotIdx),\n () => deps.onEdit(stepIdx),\n ),\n );\n }\n }\n}\n\n/**\n * The single dashed door that opens the wizard.\n *\n * It sits directly above the first unsatisfied step — the thing it acts on —\n * and migrates down as steps complete, then disappears once every step is\n * satisfied (adding beyond a satisfied step's minimum stays available through\n * the step's cards, which reopen the wizard at that step). Its\n * `data-step-door` value is kept at the wizard step it should open, so the\n * host's click handler reads it live instead of re-deriving the target.\n */\nexport function updateDoor(deps: BodyDeps): void {\n const { container, steps, selections, t } = deps;\n const door = container.querySelector<HTMLElement>(\"[data-step-door]\");\n if (!door) return;\n\n let firstUnsatisfied = -1;\n let remaining = 0;\n let anyPicks = false;\n for (let i = 0; i < steps.length; i++) {\n if (selections[i].length > 0) anyPicks = true;\n if (!stepSatisfied(selections, steps, i)) {\n remaining += 1;\n if (firstUnsatisfied === -1) firstUnsatisfied = i;\n }\n }\n\n if (firstUnsatisfied === -1) {\n // Park the hidden door at the top of the container: it stays in the DOM\n // (display:none), and mid-list it would still count as a sibling for the\n // CSS pair-spacing selectors. All-satisfied means every section is open,\n // so nothing ever needs to sit above it.\n door.style.display = \"none\";\n door.classList.add(\"lb-multi-step__door--hidden\");\n const parent = door.parentElement;\n if (parent && parent.firstElementChild !== door) {\n parent.insertBefore(door, parent.firstElementChild);\n }\n return;\n }\n door.style.display = \"\";\n door.classList.remove(\"lb-multi-step__door--hidden\");\n door.setAttribute(\"data-step-door\", String(firstUnsatisfied));\n\n const label = doorLabel(anyPicks, t);\n const labelEl = door.querySelector<HTMLElement>(\n \".lb-mix-match__add-product-label\",\n );\n if (labelEl) labelEl.textContent = label;\n const subEl = door.querySelector<HTMLElement>(\".lb-multi-step__door-sub\");\n if (subEl) {\n subEl.textContent = doorSubLabel(anyPicks, remaining, steps.length, t);\n }\n const stepName = steps[firstUnsatisfied].name || \"\";\n door.setAttribute(\"aria-label\", stepName ? label + \": \" + stepName : label);\n\n const target = container.querySelector<HTMLElement>(\n '[data-step-group=\"' + firstUnsatisfied + '\"]',\n );\n if (target && target.parentElement && target.previousElementSibling !== door) {\n target.parentElement.insertBefore(door, target);\n }\n}\n\nexport function updatePricing(\n deps: BodyDeps,\n data: MultiStepData,\n calcDiscount: (total: number, type: string, value: number) => number,\n paint: (cont: HTMLElement, sale: number, compare: number) => void,\n): void {\n const { container, steps, selections } = deps;\n const pricingSection = container.querySelector<HTMLElement>(\n \"[data-pricing-section]\",\n );\n\n if (!allSatisfied(selections, steps)) {\n if (pricingSection) pricingSection.style.display = \"none\";\n return;\n }\n if (pricingSection) pricingSection.style.display = \"\";\n\n let totalPrice = 0;\n for (const step of selections) {\n for (const pick of step) totalPrice += pick.price * (pick.quantity || 1);\n }\n\n if (totalPrice > 0 && data.discountType) {\n paint(\n container,\n calcDiscount(totalPrice, data.discountType, data.discountValue ?? 0),\n totalPrice,\n );\n }\n}\n\n/** One segment per step, filled as each step's minimum is met. */\nexport function updateProgress(deps: BodyDeps): void {\n const { container, steps, selections, t } = deps;\n\n let completed = 0;\n for (let i = 0; i < steps.length; i++) {\n if (stepSatisfied(selections, steps, i)) completed += 1;\n }\n const remaining = steps.length - completed;\n\n const segments = container.querySelectorAll<HTMLElement>(\n \"[data-progress] [data-progress-segment]\",\n );\n for (let s = 0; s < segments.length; s++) {\n segments[s].classList.toggle(\n \"lb-mix-match__progress-segment--filled\",\n s < completed,\n );\n }\n\n const progressCount = container.querySelector<HTMLElement>(\n \"[data-progress-count]\",\n );\n if (progressCount) {\n progressCount.textContent = stepsCompletedLabel(completed, steps.length, t);\n }\n\n const progressRemaining = container.querySelector<HTMLElement>(\n \"[data-progress-remaining]\",\n );\n if (progressRemaining) {\n progressRemaining.textContent = moreToGoLabel(remaining, t);\n }\n\n const segWrap = container.querySelector<HTMLElement>(\n \".lb-mix-match__progress-segments\",\n );\n if (segWrap) segWrap.setAttribute(\"aria-valuenow\", String(completed));\n}\n\nexport function updateCta(deps: BodyDeps): void {\n const ctaBtn = deps.container.querySelector<HTMLButtonElement>(\n \"[data-add-bundle]\",\n );\n if (!ctaBtn) return;\n\n ctaBtn.disabled = !allSatisfied(deps.selections, deps.steps);\n // Written to the label child so the sibling spinner survives.\n const ctaLabel = ctaBtn.querySelector<HTMLElement>(\"[data-cta-label]\");\n if (ctaLabel) {\n ctaLabel.textContent =\n ctaBtn.getAttribute(\"data-cta-text\") || deps.t.addToCart || \"Add to cart\";\n }\n}\n","import type { StepSelections } from \"./rules\";\nimport type { CartItem } from \"../host\";\n\n/**\n * Flatten the wizard's per-step selections into cart lines.\n *\n * Merged by variant: the discount function sees cart lines, not steps, so the\n * same variant picked in two steps has to arrive as one line of the combined\n * quantity. Unmerged, the shopper would get duplicate lines and the pool maths\n * would count them separately.\n *\n * First-seen order is preserved so the cart reads in wizard order.\n */\nexport function mergeCartLines(selections: StepSelections): CartItem[] {\n const byVariant: Record<string, CartItem> = Object.create(null);\n const order: string[] = [];\n\n for (const step of selections) {\n for (const item of step) {\n const qty = item.quantity || 1;\n const key = String(item.variantId);\n if (byVariant[key]) {\n byVariant[key].quantity += qty;\n byVariant[key].priceCents =\n (byVariant[key].priceCents ?? 0) + (item.price || 0) * qty;\n } else {\n byVariant[key] = {\n variantId: item.variantId,\n quantity: qty,\n priceCents: (item.price || 0) * qty,\n };\n order.push(key);\n }\n }\n }\n\n return order.map((key) => byVariant[key]);\n}\n","import { ofSelectedLabel, stepOfLabel } from \"./i18n\";\nimport {\n indexOfVariantInStep,\n stepHeadroom,\n stepSatisfied,\n swapUnitsForStep,\n unitsInStep,\n type StepSelections,\n} from \"./rules\";\nimport { fill } from \"../picker/i18n\";\nimport { getFocusable } from \"../picker/focus-trap\";\nimport { lock, unlock } from \"../picker/scroll-lock\";\nimport { sinkAddedRows } from \"../picker/sort\";\nimport { buildPickerRow, type PickerRow, type RowCapacity } from \"../picker/row\";\nimport { defaultPickQuantity, type ProductRule } from \"../picker/rules\";\nimport { debounce, normalizeText } from \"../utils\";\nimport type { DropdownApi } from \"../host\";\nimport type {\n EligibleProduct,\n MultiStepStep,\n MultiStepTranslations,\n SelectedItem,\n} from \"./types\";\n\nconst ALL_TYPES = \"__all__\";\nconst CLOSE_TRANSITION_MS = 300;\nconst SEARCH_DEBOUNCE_MS = 200;\n/**\n * How long the step-change cascade runs, plus a small margin. Kept in step\n * with the animation delays in `bundle-multi-step.css`, whose last card\n * lands at 0.42s.\n */\nconst STEP_CASCADE_MS = 500;\n/**\n * Auto-advance waits this long before moving on. Without it the step change\n * starts in the same frame as the shopper's click, so the Add button never\n * renders its \"Added\" state before the list is torn down and the advance\n * reads as a glitch rather than as a consequence of what they just did.\n */\nconst AUTO_ADVANCE_HOLD_MS = 160;\n\nexport interface WizardDeps {\n container: HTMLElement;\n steps: MultiStepStep[];\n selections: StepSelections;\n t: MultiStepTranslations;\n rulesMap: Record<string, ProductRule | undefined>;\n showQtySelector: boolean;\n showTypeFilters: boolean;\n /** Move to the next step automatically once picks reach the step's\n * maximum. Fires only on a commit that raises the unit count, never on\n * the last step; steps without a maximum are unaffected. */\n autoAdvance: boolean;\n formatMoney: (cents: number) => string;\n dropdown: DropdownApi | undefined;\n /** Where to portal the overlay, or null to leave it in place. */\n portalTarget: HTMLElement | null;\n /** Focus target when the wizard closes and the step's trigger is hidden. */\n ctaButton: HTMLElement;\n /** Repaint the widget body after any selection change. */\n onSelectionChanged: () => void;\n /** Displace in-step sibling units with this pick (atomic variant swap). */\n swapVariant: (step: number, item: SelectedItem, qty: number) => void;\n canRemove: (item: SelectedItem) => boolean;\n}\n\nexport interface Wizard {\n open: (atStep?: number) => void;\n close: () => void;\n isOpen: () => boolean;\n refresh: () => void;\n}\n\nexport function createWizard(deps: WizardDeps): Wizard {\n const { container, steps, selections, t } = deps;\n\n const overlay = container.querySelector<HTMLElement>(\"[data-modal-overlay]\");\n // See the note in mix-match/modal.ts: the theme host portals this to <body>\n // to escape a transformed ancestor; a shadow root neither needs that nor\n // survives it.\n if (overlay && deps.portalTarget) deps.portalTarget.appendChild(overlay);\n\n const q = <T extends HTMLElement>(sel: string): T | null =>\n overlay ? overlay.querySelector<T>(sel) : null;\n\n const modalDialog = q<HTMLElement>('[role=\"dialog\"]');\n const modalTitle = q<HTMLElement>(\".lb-mix-match__modal-title\");\n const searchInput = q<HTMLInputElement>(\"[data-modal-search]\");\n const searchClear = q<HTMLElement>(\"[data-modal-search-clear]\");\n const modalList = q<HTMLElement>(\"[data-modal-list]\");\n const modalEmpty = q<HTMLElement>(\"[data-modal-empty]\");\n const modalLive = q<HTMLElement>(\"[data-modal-live]\");\n const closeBtn = q<HTMLElement>(\"[data-modal-close]\");\n const backBtn = q<HTMLButtonElement>(\"[data-modal-back]\");\n const nextBtn = q<HTMLButtonElement>(\"[data-modal-next]\");\n const doneBtn = q<HTMLButtonElement>(\"[data-modal-done]\");\n const footerCountEl = q<HTMLElement>(\"[data-modal-footer-count]\");\n const subtitleEl = q<HTMLElement>(\"[data-modal-subtitle]\");\n const segmentsEl = q<HTMLElement>(\"[data-modal-step-segments]\");\n const filtersContainer = q<HTMLElement>(\"[data-modal-filters]\");\n\n let activeType = ALL_TYPES;\n let modalOpen = false;\n /** The step whose rows the list currently holds. */\n let stepIndex = 0;\n let rows: PickerRow[] = [];\n /** Pending clear of `data-step-transition` once the cascade has played. */\n let cascadeTimer: ReturnType<typeof setTimeout> | null = null;\n /** Pending auto-advance, held briefly so the shopper's pick lands first. */\n let autoAdvanceTimer: ReturnType<typeof setTimeout> | null = null;\n\n const productsFor = (i: number): EligibleProduct[] =>\n steps[i].eligibleProducts || [];\n\n /**\n * Capacity scoped to the step being displayed.\n *\n * Reads `stepIndex` at call time rather than closing over a value, so one\n * adapter serves every step the wizard walks through.\n */\n const rowCapacity: RowCapacity = {\n remainingSpots: () => stepHeadroom(selections, steps, stepIndex),\n ownQty: (variantId) => {\n const idx = indexOfVariantInStep(selections, stepIndex, variantId);\n return idx === -1 ? 0 : selections[stepIndex][idx].quantity || 0;\n },\n // Stock is one pool for the whole bundle, so units in other steps count.\n variantUnitsElsewhere: (variantId) => {\n let qty = 0;\n for (const step of selections) {\n for (const pick of step) {\n if (pick.variantId === variantId) qty += pick.quantity || 0;\n }\n }\n return qty;\n },\n productUnitsElsewhere: (productId, variantId) => {\n let qty = 0;\n for (let i = 0; i < selections.length; i++) {\n for (const it of selections[i]) {\n if (it.productId !== productId) continue;\n if (i === stepIndex && it.variantId === variantId) continue;\n qty += it.quantity || 0;\n }\n }\n return qty;\n },\n swapUnits: (productId, variantId) =>\n swapUnitsForStep(selections, stepIndex, productId, variantId, deps.rulesMap),\n isInBundle: (variantId) =>\n indexOfVariantInStep(selections, stepIndex, variantId) !== -1,\n canRemove: (variantId) => {\n const idx = indexOfVariantInStep(selections, stepIndex, variantId);\n return idx !== -1 && deps.canRemove(selections[stepIndex][idx]);\n },\n isFull: () => stepHeadroom(selections, steps, stepIndex) === 0,\n commitQty: (variantId, qty) => {\n const idx = indexOfVariantInStep(selections, stepIndex, variantId);\n if (idx === -1 || selections[stepIndex][idx].quantity === qty) return false;\n selections[stepIndex][idx].quantity = qty;\n return true;\n },\n };\n\n /* ── Step progress segments ────────────────────────────────── */\n\n /** One segment per required unit of the step, filled from its unit count. */\n function buildStepSegments(i: number): void {\n if (!segmentsEl) return;\n const min = steps[i].minQuantity || 1;\n segmentsEl.innerHTML = \"\";\n segmentsEl.setAttribute(\"aria-valuemax\", String(min));\n for (let s = 0; s < min; s++) {\n const seg = document.createElement(\"span\");\n seg.className = \"lb-mix-match__progress-segment\";\n seg.setAttribute(\"data-progress-segment\", \"\");\n segmentsEl.appendChild(seg);\n }\n paintStepSegments();\n }\n\n function paintStepSegments(): void {\n if (!segmentsEl) return;\n const min = steps[stepIndex].minQuantity || 1;\n const count = Math.min(unitsInStep(selections, stepIndex), min);\n const segs = segmentsEl.querySelectorAll<HTMLElement>(\n \"[data-progress-segment]\",\n );\n for (let s = 0; s < segs.length; s++) {\n segs[s].classList.toggle(\n \"lb-mix-match__progress-segment--filled\",\n s < count,\n );\n }\n segmentsEl.setAttribute(\"aria-valuenow\", String(count));\n }\n\n /* ── Footer and nav ────────────────────────────────────────── */\n\n function updateModalMeta(): void {\n const min = steps[stepIndex].minQuantity || 1;\n const count = Math.min(unitsInStep(selections, stepIndex), min);\n\n if (footerCountEl) {\n footerCountEl.textContent = ofSelectedLabel(count, min, t);\n }\n if (subtitleEl) {\n subtitleEl.textContent =\n stepOfLabel(stepIndex, steps.length, t) +\n \" · \" +\n (steps[stepIndex].name || \"\");\n }\n\n const satisfied = stepSatisfied(selections, steps, stepIndex);\n const isLast = stepIndex === steps.length - 1;\n\n if (backBtn) backBtn.style.display = stepIndex > 0 ? \"\" : \"none\";\n if (nextBtn) {\n nextBtn.style.display = isLast ? \"none\" : \"\";\n // Can't advance until this step's minimum is met.\n nextBtn.disabled = !satisfied;\n }\n if (doneBtn) {\n doneBtn.style.display = isLast ? \"\" : \"none\";\n doneBtn.disabled = !satisfied;\n }\n paintStepSegments();\n }\n\n function refresh(): void {\n for (const r of rows) r.refresh();\n updateModalMeta();\n }\n\n /**\n * A step filled to its maximum has nothing left to do, so with the\n * merchant setting on the wizard moves on by itself. Called only from the\n * two commit paths that can raise the step's unit count (Add and the\n * committed-row stepper) — a swap keeps the total fixed and opening at an\n * already-full step is the shopper revisiting, not filling.\n *\n * The move is held for AUTO_ADVANCE_HOLD_MS so the pick the shopper just\n * made renders before its step is replaced. Any step change during the\n * hold cancels it (`goToStep` clears the timer) and so does closing the\n * wizard: navigation the shopper asked for outranks navigation we were\n * about to do for them.\n */\n function maybeAutoAdvance(): void {\n if (!deps.autoAdvance) return;\n // The last step never auto-advances; the shopper reviews and closes.\n if (stepIndex >= steps.length - 1) return;\n // A step without a maximum has Infinity headroom and never hits 0.\n if (stepHeadroom(selections, steps, stepIndex) !== 0) return;\n\n const from = stepIndex;\n if (autoAdvanceTimer !== null) clearTimeout(autoAdvanceTimer);\n autoAdvanceTimer = setTimeout(() => {\n autoAdvanceTimer = null;\n if (!modalOpen) return;\n goToStep(from + 1);\n }, AUTO_ADVANCE_HOLD_MS);\n }\n\n /* ── Product list ──────────────────────────────────────────── */\n\n function buildProductList(forStep: number): void {\n if (!modalList) return;\n\n deps.dropdown?.unbindAll(modalList);\n modalList.innerHTML = \"\";\n rows = [];\n\n const pool = productsFor(forStep);\n for (let i = 0; i < pool.length; i++) {\n const row = buildPickerRow(pool[i], i, {\n t,\n rulesMap: deps.rulesMap,\n capacity: rowCapacity,\n showQtySelector: deps.showQtySelector,\n formatMoney: deps.formatMoney,\n onCommittedQtyChanged: () => {\n deps.onSelectionChanged();\n refresh();\n maybeAutoAdvance();\n },\n });\n rows.push(row);\n modalList.appendChild(row.el);\n }\n\n // Products already picked in this step sink below the ones still to\n // pick. The list is only rebuilt on open and step change, so the order\n // holds still while the shopper works through the step.\n const stepPicks = selections[forStep] || [];\n const selectedProductIds: Record<string, true> = Object.create(null);\n for (const it of stepPicks) selectedProductIds[String(it.productId)] = true;\n sinkAddedRows(\n modalList,\n rows.map((r) => r.el),\n (i) => selectedProductIds[String(pool[i].id)] === true,\n );\n\n deps.dropdown?.bindAll(modalList);\n }\n\n function onListClick(e: MouseEvent): void {\n const addBtn = (e.target as HTMLElement).closest<HTMLButtonElement>(\n \"[data-add-product]\",\n );\n if (!addBtn || addBtn.disabled) return;\n\n const pool = productsFor(stepIndex);\n const prod = pool[parseInt(addBtn.getAttribute(\"data-add-product\") ?? \"\", 10)];\n if (!prod) return;\n\n const row = addBtn.closest<HTMLElement>(\".lb-mix-match__modal-product\");\n const selectedId = row && row.getAttribute(\"data-selected-variant-id\");\n let selectedVariant =\n (selectedId\n ? prod.variants.find((v) => v.id === parseInt(selectedId, 10))\n : null) ?? null;\n if (!selectedVariant) {\n selectedVariant = prod.variants.find((v) => v.available) ?? null;\n }\n if (!selectedVariant) return;\n\n // Toggle off, scoped to this step: the same variant in another step keeps\n // its slot.\n const existing = indexOfVariantInStep(\n selections,\n stepIndex,\n selectedVariant.id,\n );\n if (existing !== -1) {\n if (!deps.canRemove(selections[stepIndex][existing])) return;\n selections[stepIndex].splice(existing, 1);\n deps.onSelectionChanged();\n refresh();\n return;\n }\n\n const rowQtyEl = row?.querySelector<HTMLElement>(\"[data-row-qty]\");\n const pickedQty = rowQtyEl\n ? Math.max(\n 1,\n parseInt(\n rowQtyEl.getAttribute(\"data-qty\") || rowQtyEl.textContent || \"\",\n 10,\n ) || 1,\n )\n : defaultPickQuantity(prod.id, deps.rulesMap, selectedVariant.id);\n\n const newItem: SelectedItem = {\n productId: prod.id,\n variantId: selectedVariant.id,\n title: prod.title,\n url: prod.url || null,\n variantTitle: selectedVariant.title,\n featuredImage: selectedVariant.image || prod.featuredImage || null,\n price: selectedVariant.price,\n compareAtPrice: selectedVariant.compareAtPrice || null,\n unitPrice: selectedVariant.unitPrice || null,\n quantity: pickedQty,\n };\n\n // A swap keeps the step total fixed, so it runs instead of the headroom\n // guard rather than after it.\n const swapUnits = swapUnitsForStep(\n selections,\n stepIndex,\n prod.id,\n selectedVariant.id,\n deps.rulesMap,\n );\n if (swapUnits > 0) {\n deps.swapVariant(stepIndex, newItem, Math.min(pickedQty, swapUnits));\n refresh();\n return;\n }\n\n const room = stepHeadroom(selections, steps, stepIndex);\n if (room !== Infinity && pickedQty > room) return;\n\n selections[stepIndex].push(newItem);\n deps.onSelectionChanged();\n refresh();\n maybeAutoAdvance();\n }\n\n if (modalList) modalList.addEventListener(\"click\", onListClick);\n\n /* ── Filters and search ────────────────────────────────────── */\n\n function buildFilters(forStep: number): void {\n if (!filtersContainer || !deps.showTypeFilters) return;\n\n const types: string[] = [];\n const seen: Record<string, boolean> = Object.create(null);\n for (const p of productsFor(forStep)) {\n const ty = (p.type || \"\").trim();\n if (ty && !seen[ty]) {\n seen[ty] = true;\n types.push(ty);\n }\n }\n\n if (types.length < 2) {\n filtersContainer.style.display = \"none\";\n modalDialog?.classList.add(\"lb-mix-match__modal--filters-hidden\");\n return;\n }\n // Re-shown explicitly: a previous step may have hidden the row.\n filtersContainer.style.display = \"\";\n modalDialog?.classList.remove(\"lb-mix-match__modal--filters-hidden\");\n\n filtersContainer.innerHTML = \"\";\n for (const value of [ALL_TYPES, ...types]) {\n const pill = document.createElement(\"button\");\n pill.type = \"button\";\n pill.className = \"lb-mix-match__filter\";\n pill.setAttribute(\"data-filter\", value);\n pill.setAttribute(\"aria-pressed\", value === activeType ? \"true\" : \"false\");\n if (value === activeType) pill.classList.add(\"lb-mix-match__filter--active\");\n pill.textContent = value === ALL_TYPES ? t.allTypes || \"All\" : value;\n pill.addEventListener(\"click\", () => {\n activeType = value;\n syncActiveFilterPill();\n filterProducts(searchInput ? searchInput.value : \"\");\n });\n filtersContainer.appendChild(pill);\n }\n }\n\n function syncActiveFilterPill(): void {\n if (!filtersContainer) return;\n const pills = filtersContainer.querySelectorAll<HTMLElement>(\"[data-filter]\");\n for (let i = 0; i < pills.length; i++) {\n const on = pills[i].getAttribute(\"data-filter\") === activeType;\n pills[i].classList.toggle(\"lb-mix-match__filter--active\", on);\n pills[i].setAttribute(\"aria-pressed\", on ? \"true\" : \"false\");\n }\n }\n\n function filterProducts(query: string): void {\n if (!modalList) return;\n const normalizedQuery = normalizeText(query);\n const listItems = modalList.querySelectorAll<HTMLElement>(\n \"[data-product-item]\",\n );\n let visibleCount = 0;\n\n for (let i = 0; i < listItems.length; i++) {\n const title = listItems[i].getAttribute(\"data-title\") ?? \"\";\n const type = listItems[i].getAttribute(\"data-type\") || \"\";\n const matches =\n (!normalizedQuery || title.indexOf(normalizedQuery) !== -1) &&\n (activeType === ALL_TYPES || type === activeType);\n listItems[i].classList.toggle(\"lb-hidden\", !matches);\n if (matches) visibleCount++;\n }\n\n if (modalEmpty) {\n modalEmpty.style.display =\n visibleCount === 0 && normalizedQuery.length > 0 ? \"\" : \"none\";\n }\n if (searchClear) {\n searchClear.style.display = query.length > 0 ? \"\" : \"none\";\n }\n if (modalLive) {\n modalLive.textContent = fill(\n t.nProductsShown || \"__COUNT__ products shown\",\n { COUNT: visibleCount },\n );\n }\n }\n\n if (searchInput) {\n const handleSearch = debounce(\n () => filterProducts(searchInput.value),\n SEARCH_DEBOUNCE_MS,\n );\n searchInput.addEventListener(\"input\", handleSearch);\n }\n if (searchClear) {\n searchClear.addEventListener(\"click\", () => {\n if (!searchInput) return;\n searchInput.value = \"\";\n filterProducts(\"\");\n searchInput.focus();\n });\n }\n\n /* ── Navigation ────────────────────────────────────────────── */\n\n /**\n * Move to a step: rebuild its product list, filters and segments, reset the\n * search, and announce the change.\n *\n * Every step change (manual Next/Back and auto-advance) stamps\n * `data-step-transition=\"forward\" | \"back\"` on the dialog so the CSS can\n * cascade the incoming step's content in — without it the whole region\n * swaps in one paint and the shopper can't tell they've moved.\n * `animate: false` (the open path) removes the attribute instead: the\n * modal entrance carries that moment's motion, and a leftover value would\n * replay the cascade every time `display: none` is lifted.\n *\n * The attribute is cleared again once the cascade has played. That is not\n * tidiness: the cascade animates the product cards individually, and\n * search/filter hide cards with `.lb-hidden` (`display: none`), so\n * restoring one restarts its animation. A live attribute would replay the\n * lift on every keystroke that reveals a card.\n */\n function goToStep(\n i: number,\n opts: { skipFocus?: boolean; animate?: boolean } = {},\n ): void {\n const target = Math.max(0, Math.min(steps.length - 1, i));\n // Any step change supersedes a held auto-advance, including the one\n // firing right now (which has already cleared its own handle).\n if (autoAdvanceTimer !== null) {\n clearTimeout(autoAdvanceTimer);\n autoAdvanceTimer = null;\n }\n if (modalDialog) {\n // Cleared first with a reflow between: re-stamping the same value\n // would leave a mid-flight animation running instead of restarting it.\n if (cascadeTimer !== null) clearTimeout(cascadeTimer);\n cascadeTimer = null;\n modalDialog.removeAttribute(\"data-step-transition\");\n if (opts.animate !== false && target !== stepIndex) {\n void modalDialog.offsetHeight;\n modalDialog.setAttribute(\n \"data-step-transition\",\n target > stepIndex ? \"forward\" : \"back\",\n );\n const dialog = modalDialog;\n cascadeTimer = setTimeout(() => {\n cascadeTimer = null;\n dialog.removeAttribute(\"data-step-transition\");\n }, STEP_CASCADE_MS);\n }\n }\n stepIndex = target;\n\n buildProductList(stepIndex);\n buildFilters(stepIndex);\n activeType = ALL_TYPES;\n syncActiveFilterPill();\n if (searchInput) searchInput.value = \"\";\n filterProducts(\"\");\n\n // A step always presents from the top of its list — arriving mid-scroll\n // from the previous step hides the new step's first products.\n if (modalList) modalList.scrollTop = 0;\n\n buildStepSegments(stepIndex);\n refresh();\n\n if (modalLive) {\n const min = steps[stepIndex].minQuantity || 1;\n modalLive.textContent =\n stepOfLabel(stepIndex, steps.length, t) +\n \". \" +\n (steps[stepIndex].name || \"\") +\n \". \" +\n ofSelectedLabel(\n Math.min(unitsInStep(selections, stepIndex), min),\n min,\n t,\n );\n }\n if (!opts.skipFocus && modalTitle && typeof modalTitle.focus === \"function\") {\n modalTitle.focus();\n }\n }\n\n function open(atStep?: number): void {\n if (!overlay || modalOpen) return;\n modalOpen = true;\n // Focus goes to the close icon below, so the step heading doesn't take\n // it on the initial open.\n goToStep(typeof atStep === \"number\" ? atStep : 0, {\n skipFocus: true,\n animate: false,\n });\n\n overlay.style.display = \"\";\n void overlay.offsetHeight;\n overlay.classList.add(\"lb-mix-match__modal-overlay--open\");\n\n lock();\n\n // The close icon, not the search input: focusing search on a phone pops\n // the keyboard over half the modal before the shopper asked for it.\n setTimeout(() => {\n if (closeBtn) closeBtn.focus();\n else modalDialog?.focus();\n }, 50);\n }\n\n function close(): void {\n if (!overlay || !modalOpen) return;\n modalOpen = false;\n\n // A held auto-advance must not fire into a closed wizard, and the\n // cascade clear is redundant once the next open resets the attribute.\n if (autoAdvanceTimer !== null) {\n clearTimeout(autoAdvanceTimer);\n autoAdvanceTimer = null;\n }\n if (cascadeTimer !== null) {\n clearTimeout(cascadeTimer);\n cascadeTimer = null;\n }\n\n overlay.classList.remove(\"lb-mix-match__modal-overlay--open\");\n unlock();\n\n setTimeout(() => {\n if (!modalOpen) overlay.style.display = \"none\";\n }, CLOSE_TRANSITION_MS);\n\n // Back to the current step's checklist row; a step with picks has no\n // row, so fall through to the door, and to the CTA once every step is\n // satisfied and the door has retired too.\n const visible = (el: HTMLElement | null): HTMLElement | null =>\n el && el.offsetParent !== null ? el : null;\n const elToFocus =\n visible(\n container.querySelector<HTMLElement>(\n '[data-step-row=\"' + stepIndex + '\"]',\n ),\n ) ||\n visible(container.querySelector<HTMLElement>(\"[data-step-door]\")) ||\n deps.ctaButton;\n if (elToFocus && typeof elToFocus.focus === \"function\") {\n setTimeout(() => elToFocus.focus(), 0);\n }\n }\n\n closeBtn?.addEventListener(\"click\", close);\n doneBtn?.addEventListener(\"click\", close);\n backBtn?.addEventListener(\"click\", () => {\n if (stepIndex > 0) goToStep(stepIndex - 1);\n });\n nextBtn?.addEventListener(\"click\", () => {\n if (\n stepIndex < steps.length - 1 &&\n stepSatisfied(selections, steps, stepIndex)\n ) {\n goToStep(stepIndex + 1);\n }\n });\n\n if (overlay) {\n let mouseDownTarget: EventTarget | null = null;\n overlay.addEventListener(\"mousedown\", (e) => {\n mouseDownTarget = e.target;\n });\n overlay.addEventListener(\"mouseup\", (e) => {\n if (e.target === overlay && mouseDownTarget === overlay) close();\n mouseDownTarget = null;\n });\n }\n\n document.addEventListener(\"keydown\", (e) => {\n if (!modalOpen) return;\n\n if (e.key === \"Escape\") {\n e.preventDefault();\n close();\n return;\n }\n\n if (e.key === \"Tab\" && modalDialog) {\n const focusable = getFocusable(modalDialog);\n if (focusable.length === 0) {\n e.preventDefault();\n return;\n }\n const first = focusable[0];\n const last = focusable[focusable.length - 1];\n if (e.shiftKey && document.activeElement === first) {\n e.preventDefault();\n last.focus();\n } else if (!e.shiftKey && document.activeElement === last) {\n e.preventDefault();\n first.focus();\n }\n }\n });\n\n return { open, close, isOpen: () => modalOpen, refresh };\n}\n","import {\n calculateDiscount,\n productsForStep,\n type CartLineInput,\n type MultiStepBundleData,\n} from \"@lime-bundles/core\";\nimport { adaptProduct } from \"@lime-bundles/render/adapt\";\nimport { bundleLineAttributes, intlFormatMoney } from \"@lime-bundles/render/host\";\nimport {\n updateCta,\n updateDoor,\n updatePricing,\n updateProgress,\n updateStepGroups,\n type BodyDeps,\n} from \"@lime-bundles/render/multi-step/body\";\nimport { mergeCartLines } from \"@lime-bundles/render/multi-step/cart\";\nimport {\n allSatisfied,\n canRemovePick,\n type StepSelections,\n} from \"@lime-bundles/render/multi-step/rules\";\nimport type { EligibleProduct } from \"@lime-bundles/render/multi-step/types\";\nimport { createWizard } from \"@lime-bundles/render/multi-step/wizard\";\nimport { mergeRuleMaps } from \"@lime-bundles/render/picker/rules\";\nimport { buildPickerModal } from \"@lime-bundles/render/picker-modal\";\nimport { buildProgressBar, buildWidgetShell } from \"@lime-bundles/render/skeleton\";\nimport { initCountdown } from \"@lime-bundles/render/countdown\";\nimport { DEFAULT_STRINGS } from \"@lime-bundles/render/strings\";\nimport { updatePricing as paintPricing } from \"./pricing-paint\";\nimport { bindDropdowns } from \"./dropdown-host\";\n\n/**\n * Multi-step wizard, headless.\n *\n * Like mix & match, the wizard itself was already client-rendered on both\n * hosts. What Liquid provides in a theme — the widget shell, the per-step\n * groups with their triggers, and the modal — is built here.\n */\nfunction buildPlusIcon(): SVGSVGElement {\n const NS = \"http://www.w3.org/2000/svg\";\n const svg = document.createElementNS(NS, \"svg\");\n svg.setAttribute(\"width\", \"18\");\n svg.setAttribute(\"height\", \"18\");\n svg.setAttribute(\"viewBox\", \"0 0 18 18\");\n svg.setAttribute(\"fill\", \"none\");\n for (const [x1, y1, x2, y2] of [\n [\"9\", \"3\", \"9\", \"15\"],\n [\"3\", \"9\", \"15\", \"9\"],\n ]) {\n const line = document.createElementNS(NS, \"line\");\n line.setAttribute(\"x1\", x1);\n line.setAttribute(\"y1\", y1);\n line.setAttribute(\"x2\", x2);\n line.setAttribute(\"y2\", y2);\n line.setAttribute(\"stroke\", \"currentColor\");\n line.setAttribute(\"stroke-width\", \"2\");\n line.setAttribute(\"stroke-linecap\", \"round\");\n svg.appendChild(line);\n }\n return svg;\n}\n\nexport function renderMultiStepBundle(\n container: HTMLElement,\n bundle: MultiStepBundleData,\n onAddToCart: (lines: CartLineInput[]) => void,\n onCleanup?: (fn: () => void) => void,\n): void {\n const wc = bundle.widgetConfig;\n const t = DEFAULT_STRINGS;\n const rulesMap = mergeRuleMaps(bundle.productRules, bundle.variantRules);\n const steps = bundle.steps ?? [];\n if (!steps.length) return;\n\n const quantities = {\n productQuantities: bundle.productQuantities,\n variantQuantities: bundle.variantQuantities,\n };\n\n /** Each step's pool, in the shape the picker row expects. */\n const pools: EligibleProduct[][] = steps.map((_, i) =>\n productsForStep(bundle, i).map((product) => {\n const p = adaptProduct(product, bundle.id, {\n quantities,\n allowedVariantIds: null,\n });\n return {\n id: Number(p.productId),\n title: p.title ?? \"\",\n url: p.url ?? null,\n type: product.productType ?? \"\",\n featuredImage: p.featuredImage ?? null,\n available: p.variants.some((v) => v.available),\n optionNames: p.optionNames,\n variants: p.variants.map((v) => ({\n id: Number(v.id),\n title: v.title,\n options: v.options,\n available: v.available,\n price: v.price,\n compareAtPrice: v.compareAtPrice,\n unitPrice: v.unitPrice,\n image: v.image,\n inventoryQuantity: v.inventoryQuantity ?? null,\n })),\n };\n }),\n );\n\n // Every step must be completable or the bundle can't be finished: hide,\n // matching the theme's per-step gate and React's stepHasInStockProduct.\n // Rendering on \"any one step has stock\" walked shoppers into a wizard\n // they could never complete.\n const everyStepPickable =\n pools.length > 0 && pools.every((pool) => pool.some((p) => p.available));\n if (!everyStepPickable) return;\n\n const formatMoney = intlFormatMoney(\n bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? \"USD\",\n );\n\n const shell = buildWidgetShell(\"lb-multi-step\", {\n title: bundle.title,\n subtitle: bundle.description,\n showSavingsBar: wc.savingsBar?.visible !== false,\n showComparePrice: wc.pricing?.showCompareAtPrice !== false,\n ctaText: wc.cta?.ctaText || t.addToCart || \"Add to cart\",\n endsAt: wc.countdown?.showCountdown === false ? null : bundle.endsAt,\n });\n\n // Overall progress counts steps, not units.\n shell.products.before(buildProgressBar(steps.length));\n\n // One dashed door plus one section per step. A step without picks shows a\n // slim checklist row, one with picks shows the heading + chosen cards —\n // data-step-mode picks the representation and updateDoor keeps the door\n // directly above the first unsatisfied step (both hooks the Liquid also\n // emits; tests and any merchant CSS target them).\n const groups = document.createElement(\"div\");\n groups.className = \"lb-multi-step__groups\";\n groups.setAttribute(\"data-step-groups\", \"\");\n\n const door = document.createElement(\"button\");\n door.type = \"button\";\n door.className = \"lb-mix-match__add-product lb-multi-step__door\";\n door.setAttribute(\"data-step-door\", \"0\");\n const doorIcon = document.createElement(\"span\");\n doorIcon.className = \"lb-mix-match__add-product-icon\";\n doorIcon.setAttribute(\"aria-hidden\", \"true\");\n doorIcon.appendChild(buildPlusIcon());\n door.appendChild(doorIcon);\n const doorCopy = document.createElement(\"span\");\n doorCopy.className = \"lb-multi-step__door-copy\";\n const doorLabel = document.createElement(\"span\");\n doorLabel.className = \"lb-mix-match__add-product-label\";\n doorCopy.appendChild(doorLabel);\n const doorSub = document.createElement(\"span\");\n doorSub.className = \"lb-multi-step__door-sub\";\n doorCopy.appendChild(doorSub);\n door.appendChild(doorCopy);\n groups.appendChild(door);\n\n steps.forEach((step, i) => {\n const group = document.createElement(\"section\");\n group.className = \"lb-multi-step__group\";\n group.setAttribute(\"data-step-group\", String(i));\n group.setAttribute(\"data-step-mode\", \"row\");\n\n const buildCount = (): HTMLElement => {\n const count = document.createElement(\"span\");\n count.className = \"lb-multi-step__group-count\";\n count.setAttribute(\"data-step-count\", String(i));\n return count;\n };\n\n const row = document.createElement(\"button\");\n row.type = \"button\";\n row.className = \"lb-multi-step__step-row\";\n row.setAttribute(\"data-step-row\", String(i));\n row.setAttribute(\"aria-label\", \"Choose products: \" + step.name);\n const ix = document.createElement(\"span\");\n ix.className = \"lb-multi-step__step-ix\";\n ix.setAttribute(\"aria-hidden\", \"true\");\n ix.textContent = String(i + 1);\n row.appendChild(ix);\n const rowName = document.createElement(\"span\");\n rowName.className = \"lb-multi-step__group-name\";\n rowName.textContent = step.name;\n row.appendChild(rowName);\n row.appendChild(buildCount());\n group.appendChild(row);\n\n const heading = document.createElement(\"div\");\n heading.className = \"lb-multi-step__group-heading\";\n const name = document.createElement(\"span\");\n name.className = \"lb-multi-step__group-name\";\n name.textContent = step.name;\n heading.appendChild(name);\n heading.appendChild(buildCount());\n group.appendChild(heading);\n\n const slots = document.createElement(\"div\");\n slots.className = \"lb-mix-match__slots lb-edge-fade lb-multi-step__group-slots\";\n slots.setAttribute(\"data-step-slots\", String(i));\n group.appendChild(slots);\n\n groups.appendChild(group);\n });\n shell.products.replaceWith(groups);\n\n // Same merchant toggles the Liquid host reads — multi-step shares the\n // mix-and-match picker keys (lb-multi-step.liquid takes the identical\n // show_search / show_type_filters / show_quantity_selector params).\n const overlay = buildPickerModal({\n bundleGid: bundle.id,\n domId: bundle.id.replace(/\\D/g, \"\"),\n showSearch: wc.showSearch !== false,\n showTypeFilters: wc.mixMatchShowTypeFilters !== false,\n segmentCount: null,\n wizard: true,\n });\n shell.root.appendChild(overlay);\n\n const selections: StepSelections = steps.map(() => []);\n\n const bodyDeps: BodyDeps = {\n container: shell.root,\n steps: steps.map((s, i) => ({\n name: s.name,\n minQuantity: s.minQuantity,\n maxQuantity: s.maxQuantity,\n eligibleProducts: pools[i],\n })),\n selections,\n t,\n rulesMap,\n formatMoney,\n onRemove: (step, index) => {\n const item = selections[step][index];\n if (!item || !canRemovePick(selections, item, rulesMap)) return;\n selections[step].splice(index, 1);\n updateAll();\n if (wizard.isOpen()) wizard.refresh();\n },\n onEdit: (step) => wizard.open(step),\n };\n\n function updateAll(): void {\n updateStepGroups(bodyDeps);\n updateDoor(bodyDeps);\n updatePricing(\n bodyDeps,\n {\n discountType: bundle.discountConfig.discountType,\n discountValue: bundle.discountConfig.discountValue,\n },\n (total, type, value) =>\n calculateDiscount(total, type as \"percentage\" | \"fixed_amount\", value),\n paintPricing(formatMoney),\n );\n updateProgress(bodyDeps);\n updateCta(bodyDeps);\n }\n\n const wizard = createWizard({\n container: shell.root,\n steps: bodyDeps.steps,\n selections,\n t,\n rulesMap,\n showQtySelector: wc.mixMatchShowQuantitySelector !== false,\n showTypeFilters: wc.mixMatchShowTypeFilters !== false,\n // Off by default: a stored config predating the key must not advance.\n autoAdvance: wc.multiStepAutoAdvance === true,\n formatMoney,\n dropdown: undefined,\n portalTarget: null,\n ctaButton: shell.cta,\n onSelectionChanged: updateAll,\n swapVariant: (stepIdx, item, qty) => {\n let freed = 0;\n const step = selections[stepIdx];\n for (let j = step.length - 1; j >= 0 && freed < qty; j--) {\n const it = step[j];\n if (it.productId !== item.productId || it.variantId === item.variantId) {\n continue;\n }\n const take = Math.min(it.quantity || 1, qty - freed);\n if (take >= (it.quantity || 1)) step.splice(j, 1);\n else it.quantity = (it.quantity || 1) - take;\n freed += take;\n }\n if (freed > 0) {\n item.quantity = freed;\n step.push(item);\n updateAll();\n }\n },\n canRemove: (item) => canRemovePick(selections, item, rulesMap),\n });\n\n // The door opens the wizard at whatever step updateDoor points it at;\n // each empty step's checklist row deep-links its own step.\n door.addEventListener(\"click\", () => {\n if (wizard.isOpen()) return;\n wizard.open(parseInt(door.getAttribute(\"data-step-door\") ?? \"\", 10) || 0);\n });\n shell.root.querySelectorAll<HTMLElement>(\"[data-step-row]\").forEach((el) => {\n el.addEventListener(\"click\", () => {\n if (wizard.isOpen()) return;\n wizard.open(parseInt(el.getAttribute(\"data-step-row\") ?? \"\", 10) || 0);\n });\n });\n\n shell.cta.addEventListener(\"click\", () => {\n if (!allSatisfied(selections, bodyDeps.steps)) return;\n onAddToCart(\n mergeCartLines(selections).map((item) => ({\n merchandiseId: \"gid://shopify/ProductVariant/\" + item.variantId,\n quantity: item.quantity,\n attributes: bundleLineAttributes(bundle.id, bundle.bundleType),\n })),\n );\n });\n\n container.appendChild(shell.root);\n initCountdown(shell.root);\n updateAll();\n\n onCleanup?.(bindDropdowns(shell.root));\n}\n","/**\n * Input-mode tracker — toggles `using-mouse` / `using-keyboard` classes on a\n * target element so CSS can scope focus styles by the customer's current\n * input device. Default is mouse; the first keyboard-navigation keypress\n * (Tab, arrow keys, Enter, Space, Escape, Home/End/PageUp/PageDown) flips\n * to keyboard mode, and the next pointer click flips back.\n *\n * Multiple targets share one pair of document-level listeners — installed\n * on the first `trackInputMode` call, removed when the last target is\n * released. Safe to call across every widget instance on a page without\n * stacking listeners.\n *\n * CSS shape (see bundle-base.css):\n * .using-mouse .lb-bundle-widget :focus { outline: none; }\n *\n * The Liquid theme mirrors this behaviour from `bundle-widget.js` against\n * `document.documentElement` so classic and headless storefronts render the\n * same focus rings.\n */\n\nconst NAV_KEYS = new Set([\n \"Tab\",\n \"ArrowUp\",\n \"ArrowDown\",\n \"ArrowLeft\",\n \"ArrowRight\",\n \"Home\",\n \"End\",\n \"PageUp\",\n \"PageDown\",\n \"Enter\",\n \" \",\n \"Escape\",\n]);\n\nconst targets = new Set<HTMLElement>();\nlet listenersAttached = false;\n\nfunction setAll(on: \"using-mouse\" | \"using-keyboard\"): void {\n const off = on === \"using-mouse\" ? \"using-keyboard\" : \"using-mouse\";\n for (const el of targets) {\n el.classList.add(on);\n el.classList.remove(off);\n }\n}\n\nfunction onKeyDown(e: KeyboardEvent): void {\n if (NAV_KEYS.has(e.key)) setAll(\"using-keyboard\");\n}\n\nfunction onPointerDown(): void {\n setAll(\"using-mouse\");\n}\n\nfunction attachListeners(): void {\n if (listenersAttached) return;\n listenersAttached = true;\n document.addEventListener(\"keydown\", onKeyDown, true);\n document.addEventListener(\"pointerdown\", onPointerDown, true);\n}\n\nfunction detachListeners(): void {\n if (!listenersAttached) return;\n listenersAttached = false;\n document.removeEventListener(\"keydown\", onKeyDown, true);\n document.removeEventListener(\"pointerdown\", onPointerDown, true);\n}\n\nexport function trackInputMode(target: HTMLElement): () => void {\n target.classList.add(\"using-mouse\");\n target.classList.remove(\"using-keyboard\");\n targets.add(target);\n attachListeners();\n\n return () => {\n targets.delete(target);\n target.classList.remove(\"using-mouse\");\n target.classList.remove(\"using-keyboard\");\n if (targets.size === 0) detachListeners();\n };\n}\n","/**\n * Base + type-specific widget CSS, inlined as template literals so the web\n * component can dump them into its shadow root. Source of truth for these\n * rules is `extensions/bundle-theme/assets/*.css` — the classic Shopify\n * theme app block reads the same files. Keep the two in lockstep; the\n * bundle-css-parity.test.ts golden test enforces byte equality.\n *\n * `BUNDLE_SKELETON_CSS` (at the bottom of this file) is intentionally\n * web-component-only. The theme app block never renders a loading\n * state — its Liquid render is synchronous on the server — so the\n * skeleton styles would be dead rules there. Excluding from parity.\n */\nexport const BUNDLE_BASE_CSS = `/* Lime Bundles — shared base styles for all bundle widget types */\n\n.lb-bundle-widget.lb-bundle-widget,\n.lb-bundle-widget.lb-bundle-widget * {\n line-height: normal;\n}\n\n.lb-bundle-widget {\n /* Internal CSS-only vars (not merchant-configurable). */\n --lb-thumbnail-bg: #F0F0F0;\n --lb-widget-pad: 20px;\n /* Cap on the per-bundle product/slot/tier list height — keeps long\n bundles from pushing the CTA off-screen. The list scrolls\n internally (with the .lb-edge-fade treatment) when content exceeds\n this. */\n --lb-list-max-height: 360px;\n /* Depth of the soft fade at a scroll list's clipped edge. */\n --lb-edge-fade-size: 20px;\n /* Derived \"small\" corner radius for chips and badges — the low-stock badge,\n the volume tier badge, the multi-step count chip. Controls and media\n (thumbnails, selects, steppers, buttons) take the full --lb-radius: they\n are the elements the merchant is looking at when they pick a preset, and\n the picker's own help text already promises they follow it.\n\n Proportional rather than a fixed subtraction. At \"subtle\" (6px) minus-4px\n left 2px, which reads as square — the merchant saw a picker set to Subtle\n render with sharp corners (issue #363). Two thirds holds the proportion at\n every preset: none 0, subtle 4px, rounded 8px, round 12px. No max() needed,\n since 0 × ⅔ is 0. */\n --lb-radius-sm: calc(var(--lb-radius) * 2 / 3);\n\n font-family: inherit;\n font-size: 16px;\n background: var(--lb-bg);\n border: var(--lb-border-width) solid var(--lb-border);\n border-radius: var(--lb-radius);\n padding: var(--lb-widget-pad) var(--lb-widget-pad) 20px;\n box-sizing: border-box;\n /* Cap the widget at a comfortable reading width on desktop. Below\n 440px viewports the container is already narrower than the cap,\n so the rule is inert on mobile. */\n max-width: 440px;\n}\n\n/* Edge fade — the shared treatment for inset scroll lists (fixed products,\n mix & match slots, volume tiers). The native scrollbar channel is hidden;\n instead the clipped edge of the list softly fades so content melts away\n rather than ending in a hard cut against an app-chrome scrollbar. JS\n (LB.attachEdgeFade) toggles data-edge-fade so an edge fades ONLY when content\n is actually scrolled past it — never at rest — which keeps the first/last\n card and the volume \"Most popular\" badge from ever being dimmed. Without JS\n the list simply scrolls with no bar and no fade (a safe, quiet fallback). */\n.lb-edge-fade {\n scrollbar-width: none;\n}\n.lb-edge-fade::-webkit-scrollbar {\n width: 0;\n height: 0;\n}\n.lb-edge-fade[data-edge-fade=\"top\"] {\n -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 var(--lb-edge-fade-size));\n mask-image: linear-gradient(to bottom, transparent 0, #000 var(--lb-edge-fade-size));\n}\n.lb-edge-fade[data-edge-fade=\"bottom\"] {\n -webkit-mask-image: linear-gradient(to top, transparent 0, #000 var(--lb-edge-fade-size));\n mask-image: linear-gradient(to top, transparent 0, #000 var(--lb-edge-fade-size));\n}\n.lb-edge-fade[data-edge-fade=\"both\"] {\n -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 var(--lb-edge-fade-size), #000 calc(100% - var(--lb-edge-fade-size)), transparent 100%);\n mask-image: linear-gradient(to bottom, transparent 0, #000 var(--lb-edge-fade-size), #000 calc(100% - var(--lb-edge-fade-size)), transparent 100%);\n}\n\n/* Countdown timer bar — a full-bleed \"limited time\" strip sitting below the\n title, with the same breathing room the title gives the product list. */\n.lb-bundle-countdown {\n margin: 0 calc(-1 * var(--lb-widget-pad)) 20px;\n padding: 12px 20px;\n background: var(--lb-countdown-bg);\n display: flex;\n align-items: center;\n justify-content: space-between;\n}\n\n.lb-bundle-countdown__label {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.lb-bundle-countdown__label svg {\n width: 16px;\n height: 16px;\n flex-shrink: 0;\n color: var(--lb-countdown-text);\n}\n\n.lb-bundle-countdown__label span {\n font-size: 12px;\n font-weight: 500;\n line-height: 1;\n color: var(--lb-countdown-text);\n}\n\n.lb-bundle-countdown__timer {\n font-family: 'SF Mono', 'Roboto Mono', ui-monospace, monospace;\n font-size: 12px;\n font-weight: 600;\n line-height: 1;\n color: var(--lb-countdown-text);\n letter-spacing: 0.02em;\n}\n\n/* Hide wrapper when the inner snippet rendered nothing (product OOS / unfulfillable).\n The --loading class is excluded: the web component's skeleton has none of the\n snippet root classes, so this rule matched it and beat its own display:block,\n hiding the skeleton it renders on the first frame. */\n.lb-bundle-widget:not(.lb-bundle-widget--loading):not(\n :has(.lb-fixed, .lb-mix-match, .lb-volume, .lb-bogo, .lb-multi-step)\n ) {\n display: none;\n}\n\n/* Sibling widget spacing — separates multiple bundles on the same product page.\n Uses \\`~\\` (general sibling) rather than \\`+\\` (adjacent) because each Liquid\n loop iteration emits a {% style %} block before its widget div, so the\n rendered DOM alternates <style><widget><style><widget>. The \\`+\\` combinator\n requires immediate adjacency and would match nothing; \\`~\\` matches every\n widget after the first regardless of elements between. Single-widget pages\n stay unaffected (no prior \\`.lb-bundle-widget\\` sibling to match against). */\n.lb-bundle-widget ~ .lb-bundle-widget {\n margin-top: 24px;\n}\n\n/* Header — a plain section-heading title block in the merchant's own type.\n No colored band, no full-bleed, no save-badge pill: the widget reads as a\n part of the store, not as third-party app chrome. The 16px below holds\n whether the title is followed by the product list or the countdown strip. */\n.lb-bundle-header {\n margin: 0 0 16px;\n}\n\n.lb-bundle-header__content {\n min-width: 0;\n}\n\n.lb-bundle-title {\n /* Weight 600 (not 700): the receipt's sale price is the widget's heaviest,\n largest element, so the title steps down to clear the hierarchy. */\n font-size: 18px;\n font-weight: 600;\n line-height: 24px;\n letter-spacing: -0.01em;\n color: var(--lb-text);\n margin: 0;\n}\n\n.lb-bundle-subtitle {\n font-size: 14px;\n font-weight: 400;\n line-height: 20px;\n color: color-mix(in srgb, var(--lb-text) 60%, transparent);\n margin: 4px 0 0;\n}\n\n/* Override Dawn's \\`div:empty { display: none }\\` reset for decorative elements */\n.lb-bundle-divider:empty,\n.lb-mix-match__progress-segment:empty {\n display: block;\n}\n\n/* Divider */\n.lb-bundle-divider {\n height: var(--lb-border-width);\n background: var(--lb-border);\n margin: 16px 0;\n}\n\n/* Product rows */\n.lb-bundle-product-row {\n display: flex;\n gap: 20px;\n padding: 12px 0;\n}\n\n.lb-bundle-thumbnail {\n position: relative;\n width: 48px;\n min-width: 48px;\n /* Aspect-ratio comes from the merchant \\`thumbnailRatio\\` enum via Liquid;\n \"original\" sets it to \\`auto\\` so the box sizes to the image's intrinsic\n ratio. Default keeps the historical 1:1 behaviour. */\n aspect-ratio: var(--lb-thumbnail-aspect-ratio, 1 / 1);\n background: var(--lb-thumbnail-bg);\n border-radius: var(--lb-radius);\n overflow: hidden;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.lb-bundle-thumbnail img {\n width: 100%;\n /* Height + fit come from the same merchant enum; \"original\" sets them to\n \\`auto\\` / \\`contain\\` so the image renders uncropped at intrinsic ratio. */\n height: var(--lb-thumbnail-img-height, 100%);\n object-fit: var(--lb-thumbnail-img-fit, cover);\n}\n\n.lb-bundle-thumbnail svg {\n width: 28px;\n height: 28px;\n color: color-mix(in srgb, var(--lb-text) 35%, transparent);\n}\n\n.lb-bundle-product-info {\n flex: 1;\n min-width: 0;\n}\n\n.lb-bundle-product-name {\n font-size: 16px;\n font-weight: 600;\n line-height: 20px;\n color: var(--lb-text);\n margin: 0;\n text-decoration: none;\n display: block;\n}\n\n/* The underline fades in — text-decoration itself can't transition, but\n its color can. Links only: plain titles carry no hover affordance. */\na.lb-bundle-product-name {\n text-decoration: underline;\n text-decoration-color: transparent;\n transition: text-decoration-color 0.25s ease;\n}\n\na.lb-bundle-product-name:hover {\n text-decoration-color: currentColor;\n}\n\na.lb-bundle-product-name:focus-visible {\n outline: 2px solid var(--lb-primary-color);\n outline-offset: 0;\n box-shadow: none;\n border-radius: 2px;\n}\n\n.lb-bundle-product-price {\n /* --lb-product-price-display fallback is the \"on\" branch; Liquid sets 'none' when merchant disables. */\n display: var(--lb-product-price-display, block);\n font-size: 14px;\n font-weight: 400;\n line-height: 20px;\n color: var(--lb-text);\n font-variant-numeric: tabular-nums;\n}\n\n.lb-bundle-product-prices {\n display: flex;\n align-items: baseline;\n gap: 6px;\n flex-wrap: wrap;\n margin-top: 4px;\n}\n\n.lb-bundle-product-compare-price {\n /* --lb-product-compare-display fallback is the \"on\" branch; Liquid sets 'none' when merchant disables. */\n display: var(--lb-product-compare-display, inline);\n font-size: 14px;\n font-weight: 400;\n line-height: 20px;\n color: color-mix(in srgb, var(--lb-text) 60%, transparent);\n text-decoration: line-through;\n font-variant-numeric: tabular-nums;\n}\n\n/* Setting \\`display\\` above outranks the UA [hidden] rule — restore it so rows\n without a compare-at price don't leave a phantom flex item + gap. */\n.lb-bundle-product-compare-price[hidden] {\n display: none;\n}\n\n/* Unit price (e.g. \"$0.50/100ml\") — only rendered when the merchant has\n configured unit pricing on the variant in the Shopify admin. No merchant\n toggle: present in admin → shown; absent → hidden. Styled as muted\n secondary text beneath the price row so it doesn't compete visually. */\n.lb-bundle-product-unit-price {\n /* Fallback is the \"on\" branch; Liquid sets 'none' when the merchant disables\n productList.showUnitPrice. The [hidden] rule below still wins for rows\n whose variant has no unit price at all. */\n display: var(--lb-product-unit-price-display, block);\n font-size: 12px;\n line-height: 16px;\n color: color-mix(in srgb, var(--lb-text) 60%, transparent);\n margin-top: 2px;\n}\n\n.lb-bundle-product-unit-price[hidden] {\n display: none;\n}\n\n/* Read-only variant text shown below the product title when only one\n variant is in scope (single-variant product, or merchant pinned a\n single variant). Same visual treatment as the mix-match filled\n slot's variant text — see .lb-mix-match__filled-variant in\n bundle-mix-match.css. Both share this rule via the comma selector\n so the storefront UX stays consistent across bundle types. */\n.lb-bundle-variant-badge,\n.lb-mix-match__slot--filled .lb-mix-match__filled-variant {\n display: block;\n font-size: 14px;\n line-height: 20px;\n margin-top: 2px;\n color: color-mix(in srgb, var(--lb-text) 60%, transparent);\n}\n\n/* Per-option variant pickers. Each option's label + select sit inside a\n .lb-bundle-variant-option-group (flex column, 4px gap between label and\n select); the groups stack inside a .lb-bundle-variant-option-groups\n parent (flex column, 8px gap between groups). The parent owns the top\n offset from the preceding unit-price line, so individual labels and\n selects don't carry their own vertical margins. */\n.lb-bundle-variant-option-groups {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: 8px;\n}\n\n.lb-bundle-variant-option-group {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.lb-bundle-variant-option-label {\n display: block;\n margin: 0;\n font-size: 12px;\n line-height: 16px;\n font-weight: 600;\n letter-spacing: 0.05em;\n text-transform: uppercase;\n color: color-mix(in srgb, var(--lb-text) 60%, transparent);\n}\n\n.lb-mix-match__modal-product-info .lb-bundle-variant-option-groups {\n margin-top: 8px;\n}\n\n/* Desktop: lay variant option groups in a wrapping row inside the modal\n (Size + Color side-by-side). Mobile keeps the column stack from the\n base rule above. The 768px breakpoint mirrors bundle-mix-match.css. */\n@media (min-width: 768px) {\n .lb-mix-match__modal-product-info .lb-bundle-variant-option-groups {\n flex-direction: row;\n flex-wrap: wrap;\n }\n /* Direct children only — the qty-stepper-group is also a\n .lb-bundle-variant-option-group but lives in the actions wrapper\n and must keep its natural width. */\n .lb-mix-match__modal-product-info .lb-bundle-variant-option-groups > .lb-bundle-variant-option-group {\n flex: 1;\n }\n}\n\n/* Focus-ring suppression for mouse users. A small JS helper — see\n packages/widget/src/utils/input-mode.ts and bundle-widget.js — toggles\n .using-mouse / .using-keyboard on the widget root (or html in the Liquid\n path) based on the customer's current input device. Default is mouse, so\n click-to-focus doesn't leave a keyboard-style ring. The modal overlay\n gets its own selector because the Liquid path reparents it to body,\n outside the widget root. */\n.using-mouse .lb-bundle-widget :focus:not([aria-checked=\"true\"]),\n.using-mouse .lb-bundle-widget :focus-visible:not([aria-checked=\"true\"]),\n.using-mouse .lb-mix-match__modal-overlay :focus:not([aria-checked=\"true\"]),\n.using-mouse .lb-mix-match__modal-overlay :focus-visible:not([aria-checked=\"true\"]) {\n outline: none;\n outline-offset: 0;\n box-shadow: none;\n}\n\n.lb-bundle-quantity {\n font-size: 12px;\n line-height: 16px;\n color: color-mix(in srgb, var(--lb-text) 60%, transparent);\n margin-left: 8px;\n white-space: nowrap;\n}\n\n/* Receipt summary — a column: the total row, then one quiet savings line.\n The sale price is the widget's #1 element (largest, heaviest), so the eye\n lands on what the bundle costs. Inherits the widget's global text color. */\n.lb-bundle-summary {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n margin-bottom: 16px;\n}\n\n/* Left column — the label over the savings line. Paired with the price stack on\n the right and vertically centred against it, so the summary stays balanced\n whether or not a savings line / compare price is present (e.g. an undiscounted\n bundle reads as a clean \"label … total\" on one centred line). */\n.lb-bundle-summary__text {\n display: flex;\n flex-direction: column;\n gap: 2px;\n min-width: 0;\n}\n\n.lb-bundle-summary__label {\n font-size: 15px;\n font-weight: 400;\n line-height: 1.4;\n text-transform: uppercase;\n color: color-mix(in srgb, var(--lb-text) 70%, transparent);\n}\n\n/* Savings line — the one savings signal, quiet text under the label. No pill,\n no lime: lime is reserved for the CTA. JS toggles its display when savings\n reach/leave zero (the [data-savings-bar] hook name is unchanged). */\n.lb-bundle-savings-line {\n font-size: 13px;\n font-weight: 400;\n line-height: 1.4;\n color: color-mix(in srgb, var(--lb-text) 60%, transparent);\n margin: 0;\n}\n\n.lb-bundle-savings-line [data-savings-amount] {\n color: var(--lb-text);\n font-variant-numeric: tabular-nums;\n}\n\n/* Right column — the struck original over the bundle total (\"was / now\"),\n vertically centred against the label/savings column on the left. */\n.lb-bundle-summary__prices {\n display: flex;\n flex-direction: column;\n align-items: flex-end;\n gap: 1px;\n flex-shrink: 0;\n text-align: right;\n}\n\n.lb-bundle-sale-price {\n font-size: 24px;\n font-weight: 600;\n line-height: 1.1;\n letter-spacing: -0.02em;\n color: var(--lb-text);\n font-variant-numeric: tabular-nums;\n}\n\n.lb-bundle-compare-price {\n font-size: 14px;\n font-weight: 400;\n line-height: 1.3;\n color: color-mix(in srgb, var(--lb-text) 55%, transparent);\n text-decoration: line-through;\n font-variant-numeric: tabular-nums;\n}\n\n/* Inline quantity — \"×2\" beside the product price (mix & match chosen-product\n cards). Quiet, theme-inherited; the fixed widget uses .lb-bundle-qty-chip in\n the card's right slot instead. */\n.lb-bundle-qty-inline {\n /* Fallback is the \"on\" branch; Liquid sets 'none' when the merchant disables\n productList.showQuantity. The fixed widget's chip carries its own var\n because it is a flex box, not inline text. */\n display: var(--lb-product-qty-inline-display, inline);\n font-size: 13px;\n font-weight: 400;\n color: color-mix(in srgb, var(--lb-text) 55%, transparent);\n font-variant-numeric: tabular-nums;\n white-space: nowrap;\n}\n\n/* CTA button.\n * Label and spinner share a single 1×1 grid cell so the button's intrinsic\n * width/height stays fixed when swapping between them — no layout shift when\n * entering the loading state. Visibility (not display) is used so the hidden\n * child still contributes to the cell's min-content sizing. See the\n * [data-loading=\"true\"] rules below. */\n.lb-bundle-cta {\n display: grid;\n grid-template-rows: 1fr;\n grid-template-columns: 1fr;\n width: 100%;\n padding: 12px 16px;\n border: var(--lb-cta-border-width) solid var(--lb-cta-border-color);\n border-radius: var(--lb-radius);\n font-size: 16px;\n font-weight: 600;\n line-height: 20px;\n cursor: pointer;\n text-align: center;\n transition: opacity 0.25s ease;\n font-family: inherit;\n}\n\n.lb-bundle-cta:not(:disabled) {\n background: var(--lb-primary-color);\n color: var(--lb-btn-text);\n}\n\n.lb-bundle-cta:not(:disabled):hover {\n opacity: 0.9;\n}\n\n.lb-bundle-cta:disabled {\n background: color-mix(in srgb, var(--lb-primary-color) 35%, var(--lb-bg));\n color: color-mix(in srgb, var(--lb-btn-text) 85%, transparent);\n cursor: not-allowed;\n}\n\n/* Both the label and the spinner occupy grid cell (1, 1). Only one is\n * visible at a time; the other keeps its box for sizing but is invisible. */\n.lb-cta-label,\n.lb-cta-spinner {\n grid-row: 1;\n grid-column: 1;\n display: flex;\n align-items: center;\n justify-content: center;\n min-width: 0;\n}\n\n.lb-cta-spinner {\n visibility: hidden;\n}\n\n.lb-cta-spinner svg {\n width: 20px;\n height: 20px;\n animation: lb-cta-spin 0.8s linear infinite;\n}\n\n@keyframes lb-cta-spin {\n to { transform: rotate(360deg); }\n}\n\n.lb-bundle-cta[data-loading=\"true\"] {\n cursor: wait;\n pointer-events: none;\n}\n\n.lb-bundle-cta[data-loading=\"true\"] .lb-cta-label {\n visibility: hidden;\n}\n\n.lb-bundle-cta[data-loading=\"true\"] .lb-cta-spinner {\n visibility: visible;\n}\n\n/* Transient \"Added\" confirmation — JS swaps the label text on success; a quick\n pop draws the eye to the state change. No bounce. */\n.lb-bundle-cta[data-added=\"true\"] .lb-cta-label {\n animation: lb-cta-added-pop 0.22s cubic-bezier(0.25, 1, 0.5, 1);\n}\n\n@keyframes lb-cta-added-pop {\n from { opacity: 0.5; transform: scale(0.96); }\n to { opacity: 1; transform: scale(1); }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .lb-cta-spinner svg {\n animation-duration: 2.5s;\n }\n .lb-bundle-cta[data-added=\"true\"] .lb-cta-label {\n animation: none;\n }\n}\n\n/* Error / warning message — amber, never red. Stock and warning states share\n the merchant-overridable low-stock token; the leading icon keeps meaning\n from being carried by color alone. */\n.lb-bundle-error {\n font-size: 16px;\n color: var(--lb-low-stock-text);\n margin-top: 8px;\n display: none;\n}\n\n.lb-bundle-error[data-visible=\"true\"] {\n display: flex;\n align-items: center;\n gap: 6px;\n}\n\n.lb-bundle-error[data-visible=\"true\"]::before {\n content: \"\";\n flex-shrink: 0;\n width: 18px;\n height: 18px;\n background-color: currentColor;\n -webkit-mask: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z'/%3E%3Cline x1='12' y1='9' x2='12' y2='13'/%3E%3Cline x1='12' y1='17' x2='12.01' y2='17'/%3E%3C/svg%3E\") center / contain no-repeat;\n mask: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z'/%3E%3Cline x1='12' y1='9' x2='12' y2='13'/%3E%3Cline x1='12' y1='17' x2='12.01' y2='17'/%3E%3C/svg%3E\") center / contain no-repeat;\n}\n\n/* Visually hidden — accessible to screen readers only */\n.lb-visually-hidden {\n position: absolute !important;\n width: 1px !important;\n height: 1px !important;\n padding: 0 !important;\n margin: -1px !important;\n overflow: hidden !important;\n clip-path: inset(50%) !important;\n white-space: nowrap !important;\n border: 0 !important;\n}\n\n/* Placeholder SVG icon for missing images */\n.lb-bundle-placeholder-icon {\n width: 28px;\n height: 28px;\n stroke: color-mix(in srgb, var(--lb-text) 35%, transparent);\n stroke-width: 1.5;\n fill: none;\n}\n\n/* Out-of-stock product row */\n.lb-bundle-product-row--oos {\n opacity: 0.5;\n}\n\n.lb-bundle-oos-label {\n font-size: 12px;\n font-weight: 500;\n color: var(--lb-low-stock-text);\n white-space: nowrap;\n margin-left: auto;\n}\n\n/* \"Only X left\" low-stock badge — appears alongside product/variant\n info when ProductVariant.quantityAvailable falls at or below\n wc.lowStockThreshold. Hidden when quantityAvailable is unknown\n (Storefront token without read_product_inventory). */\n.lb-bundle-low-stock-badge {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n padding: 2px 8px;\n font-size: 11px;\n font-weight: 600;\n line-height: 1.4;\n color: var(--lb-low-stock-text);\n background-color: var(--lb-low-stock-bg);\n border-radius: var(--lb-radius-sm);\n white-space: nowrap;\n}\n\n/* A/B test: hide save badge until JS swaps the label (prevents flash of default) */\n.lb-ab-pending {\n visibility: hidden;\n}\n\n/* ── Geometry reservation ─────────────────────────────────────────────────\n Liquid renders each row's structure and its product title; everything else\n is filled by JS on hydration. These rules give the empty boxes the exact\n height their filled counterparts will occupy, so a row never changes size\n between first paint and hydration — cumulative layout shift stays at zero.\n\n \\`:empty\\` stops matching the moment JS puts content in, handing the height\n back to the real content, which measures the same.\n\n The title is the one box NOT reserved this way: text wrapping depends on the\n merchant's font, so it is server-rendered and its height is simply real. */\n\n.lb-bundle-product-price:empty {\n display: inline-block;\n min-height: 20px;\n}\n\n.lb-bundle-variant-badge:empty {\n display: block;\n min-height: 20px;\n}\n\n.lb-bundle-product-unit-price:empty {\n display: block;\n min-height: 16px;\n}\n\n/* Label (16) + gap (4) + trigger (8 + 16 + 8 padding/line-height) per group,\n plus the 8px gap between groups. --lb-row-option-count is set per row by\n Liquid, which knows how many options the product has. */\n.lb-bundle-variant-option-groups:empty {\n min-height: calc(\n var(--lb-row-option-count, 1) * (52px + 2 * var(--lb-border-width)) +\n (var(--lb-row-option-count, 1) - 1) * 8px\n );\n}\n\n/* The thumbnail is a fixed 48px wide with an aspect-ratio, so its box is sized\n before the image loads. The \"original\" ratio is the exception — it sizes to\n the image's intrinsic dimensions, which nothing knows up front. Liquid emits\n the real ratio per row so even that case reserves correctly. */\n.lb-bundle-thumbnail[style*=\"--lb-row-thumb-ratio\"] {\n aspect-ratio: var(--lb-row-thumb-ratio);\n}\n/* This selector outranks the \\`.lb-bundle-thumbnail\\` rule above, so Liquid must\n emit the inline var for the \"original\" ratio and nothing else. Emitting it\n unconditionally silently replaced every merchant ratio with the image's own,\n which looks like the setting being ignored. */\n`;\nexport const BUNDLE_FIXED_CSS = `/* Lime Bundles — Fixed bundle styles */\n\n.lb-fixed__products {\n display: flex;\n flex-direction: column;\n gap: 12px;\n margin: 0;\n max-height: var(--lb-list-max-height);\n overflow-y: auto;\n /* Scrollbar hidden + edge fade — see .lb-edge-fade in bundle-base.css. */\n}\n\n/* Fixed bundles: product cards. Each product is a bordered card — thumbnail,\n info, and a right-slot quantity chip — mirroring the mix & match chosen-\n product card so the two bundle types read the same. The info column flows\n name → variant → price → unit → pickers on the inherited type. */\n.lb-fixed .lb-bundle-product-row {\n display: flex;\n align-items: flex-start;\n gap: 12px;\n padding: 8px;\n border: var(--lb-border-width) solid var(--lb-border);\n border-radius: var(--lb-radius);\n box-sizing: border-box;\n}\n\n/* Larger thumbnails. Height comes from --lb-thumbnail-aspect-ratio\n (set on .lb-bundle-widget via Liquid / applyWidgetConfigVars). Thumbnails\n carry only the derived corner radius — no border. */\n.lb-fixed .lb-bundle-thumbnail {\n width: 60px;\n min-width: 60px;\n border-radius: var(--lb-radius);\n box-sizing: border-box;\n}\n\n.lb-fixed .lb-bundle-thumbnail img {\n border-radius: var(--lb-radius);\n border: none;\n}\n\n/* Quantity chip — the card's right slot. A quiet, non-interactive count (×N),\n deliberately border-less so it never reads as the mix & match remove button. */\n.lb-bundle-qty-chip {\n flex-shrink: 0;\n min-width: 34px;\n height: 28px;\n padding: 0 8px;\n /* Fallback is the \"on\" branch; Liquid sets 'none' when the merchant disables\n productList.showQuantity. Separate from --lb-product-qty-inline-display\n because this chip is a flex box and the other counts are inline text. */\n display: var(--lb-product-qty-chip-display, inline-flex);\n align-items: center;\n justify-content: center;\n border-radius: var(--lb-radius);\n background: color-mix(in srgb, var(--lb-text) 7%, transparent);\n color: color-mix(in srgb, var(--lb-text) 75%, transparent);\n font-size: 13px;\n font-weight: 500;\n line-height: 1;\n font-variant-numeric: tabular-nums;\n white-space: nowrap;\n}\n\n/* Variant picker select — styled to match the variant badge aesthetic.\n Sits inside .lb-bundle-variant-option-group so vertical spacing is owned\n by the group/groups flex gap, not the select itself. */\n.lb-bundle-variant-select {\n display: inline-block;\n border: var(--lb-border-width) solid var(--lb-border);\n border-radius: var(--lb-radius);\n padding: 8px 32px 8px 12px;\n font-size: 12px;\n line-height: 16px;\n color: var(--lb-text);\n background: var(--lb-bg);\n font-family: inherit;\n cursor: pointer;\n appearance: none;\n -webkit-appearance: none;\n background-image: var(--lb-variant-chevron);\n background-repeat: no-repeat;\n background-position: right 8px center;\n background-size: 12px;\n width: 50%;\n max-width: 50%;\n}\n\n.lb-bundle-variant-select:focus-visible {\n outline: 2px solid var(--lb-primary-color);\n outline-offset: 2px;\n box-shadow: none;\n}\n`;\nexport const BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles — Mix & Match styles */\n\n/* === Selected product list ================================================\n The chosen products render as bordered cards. Capped height so long\n bundles don't push the CTA off the page; internal scroll uses the same\n custom 4px scrollbar as the variant dropdown panel. */\n.lb-mix-match__slots {\n display: flex;\n flex-direction: column;\n gap: 12px;\n max-height: var(--lb-list-max-height);\n overflow-y: auto;\n /* Scrollbar hidden + edge fade — see .lb-edge-fade in bundle-base.css. */\n}\n\n.lb-mix-match__slots:empty {\n display: none;\n}\n\n/* === Progress Bar (segmented) ============================================\n One rounded segment per required pick; the first N (count) fill with the\n primary colour as the shopper adds products. */\n.lb-mix-match__progress {\n margin-bottom: 16px;\n}\n\n.lb-mix-match__progress-segments {\n display: flex;\n gap: 6px;\n width: 100%;\n}\n\n.lb-mix-match__progress-segment {\n flex: 1;\n height: 6px;\n /* Follows the global radius (like the widget's other small elements); the\n modal override below switches it to the picker radius. Capped by the 6px\n height, so it reads as a pill until the merchant picks a tighter radius. */\n border-radius: var(--lb-radius-sm);\n background: color-mix(in srgb, var(--lb-text) 12%, transparent);\n /* The fill is an inner layer that grows/retracts horizontally (0.3s per\n DESIGN.md's motion vocabulary) rather than a colour swap. */\n position: relative;\n overflow: hidden;\n}\n\n.lb-mix-match__progress-segment::after {\n content: '';\n position: absolute;\n inset: 0;\n background: var(--lb-text);\n border-radius: inherit;\n transform: scaleX(0);\n transform-origin: left;\n transition: transform 0.3s ease;\n}\n\n.lb-mix-match__progress-segment--filled::after {\n transform: scaleX(1);\n}\n\n.lb-mix-match__progress-labels {\n display: flex;\n justify-content: space-between;\n margin-top: 8px;\n}\n\n.lb-mix-match__progress-count {\n font-size: 12px;\n font-weight: 500;\n line-height: 16px;\n color: color-mix(in srgb, var(--lb-text) 60%, transparent);\n}\n\n.lb-mix-match__progress-remaining {\n font-size: 12px;\n font-weight: 500;\n line-height: 16px;\n color: var(--lb-text);\n}\n\n/* In the modal the segmented bar lives inside the header, below the subtitle\n and above the header's bottom border. */\n.lb-mix-match__modal-progress {\n margin: 0;\n padding: 0;\n}\n\n/* The modal's progress segments follow the picker radius, not the global\n one — and the picker PALETTE: inside the modal the track and fill read\n --lb-picker-text, not the widget's --lb-text. */\n.lb-mix-match__modal-progress .lb-mix-match__progress-segment {\n border-radius: var(--lb-picker-radius-sm);\n background: color-mix(in srgb, var(--lb-picker-text) 12%, transparent);\n}\n\n.lb-mix-match__modal-progress .lb-mix-match__progress-segment::after {\n background: var(--lb-picker-text);\n}\n\n/* === Selected product cards === */\n.lb-mix-match__slot--filled {\n display: flex;\n align-items: flex-start;\n gap: 12px;\n padding: 8px;\n border: var(--lb-border-width) solid var(--lb-border);\n border-radius: var(--lb-radius);\n box-sizing: border-box;\n /* The whole card is an edit target — clicking it reopens the picker. */\n cursor: pointer;\n transition: border-color 0.25s ease;\n}\n\n.lb-mix-match__slot--filled:hover {\n border-color: color-mix(in srgb, var(--lb-text) 45%, transparent);\n}\n\n/* Mix-match thumbnails. Height comes from --lb-thumbnail-aspect-ratio\n (set on .lb-bundle-widget via Liquid / applyWidgetConfigVars). */\n.lb-mix-match .lb-bundle-thumbnail {\n position: relative;\n width: 60px;\n min-width: 60px;\n border-radius: var(--lb-radius);\n box-sizing: border-box;\n}\n\n.lb-mix-match .lb-bundle-thumbnail img {\n border-radius: var(--lb-radius);\n border: none;\n}\n\n.lb-mix-match__slot--filled .lb-mix-match__filled-info {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n}\n\n/* The title is an edit BUTTON (reopens the picker), not a product link —\n reset the UA button chrome and inherit the theme font. */\n.lb-mix-match__slot--filled .lb-mix-match__filled-title {\n font-size: 16px;\n font-weight: 600;\n line-height: 20px;\n color: var(--lb-text);\n text-decoration: none;\n overflow-wrap: break-word;\n background: none;\n border: none;\n margin: 0;\n padding: 0;\n font-family: inherit;\n text-align: left;\n cursor: pointer;\n}\n\n.lb-mix-match__slot--filled .lb-mix-match__filled-title:focus-visible {\n outline: 2px solid var(--lb-primary-color);\n outline-offset: 2px;\n box-shadow: none;\n border-radius: 2px;\n}\n\n/* .lb-mix-match__filled-variant — styled jointly with\n .lb-bundle-variant-badge above to keep the variant text consistent\n across mix-match and fixed bundle widgets. */\n\n.lb-mix-match__slot--filled .lb-mix-match__filled-price {\n /* Canonical price row (WIDGET-DESIGN.md): flex container, 6px gap, no\n toggles here — show/hide vars live on the inner price elements. */\n display: flex;\n align-items: baseline;\n gap: 6px;\n flex-wrap: wrap;\n margin-top: 4px;\n}\n\n.lb-mix-match__filled-compare {\n /* --lb-product-compare-display fallback is the \"on\" branch; Liquid sets 'none' when merchant disables. */\n display: var(--lb-product-compare-display, inline);\n font-size: 14px;\n font-weight: 400;\n color: color-mix(in srgb, var(--lb-text) 60%, transparent);\n text-decoration: line-through;\n}\n\n/* Boxed × remove control on the right of each card. */\n/* Remove control — the interactive member of the right-slot chip family\n (WIDGET-DESIGN.md): same 28px / --lb-radius-sm / neutral-fill silhouette\n as the quantity and savings chips. The visual box is 28px; the ::before\n pseudo-element extends the touch target to 44px without changing layout. */\n.lb-mix-match__slot-remove {\n flex-shrink: 0;\n position: relative;\n width: 28px;\n height: 28px;\n min-width: 28px;\n display: flex;\n align-items: center;\n justify-content: center;\n background: color-mix(in srgb, var(--lb-text) 7%, transparent);\n border: none;\n border-radius: var(--lb-radius-sm);\n cursor: pointer;\n color: color-mix(in srgb, var(--lb-text) 75%, transparent);\n padding: 0;\n margin-left: auto;\n transition: background 0.25s ease, color 0.25s ease;\n}\n\n.lb-mix-match__slot-remove::before {\n content: '';\n position: absolute;\n inset: -8px;\n}\n\n.lb-mix-match__slot-remove:hover {\n background: color-mix(in srgb, var(--lb-text) 12%, transparent);\n color: var(--lb-text);\n}\n\n.lb-mix-match__slot-remove:focus-visible {\n outline: 2px solid var(--lb-primary-color);\n outline-offset: -2px;\n box-shadow: none;\n}\n\n/* \"Required\" chip — shown in the right slot in place of the × when a\n required product sits at its minimum (removing it would break the\n bundle). Right-slot chip family silhouette; pure state, not a control. */\n.lb-mix-match__slot-required {\n flex-shrink: 0;\n height: 28px;\n display: inline-flex;\n align-items: center;\n padding: 0 8px;\n background: color-mix(in srgb, var(--lb-text) 7%, transparent);\n border-radius: var(--lb-radius-sm);\n color: color-mix(in srgb, var(--lb-text) 75%, transparent);\n font-size: 13px;\n font-weight: 500;\n line-height: 1;\n white-space: nowrap;\n margin-left: auto;\n}\n\n/* === \"Add a product\" — a dashed empty slot === */\n/* Reads as the next card to fill, mirroring the filled cards' geometry (the +\n sits in the thumbnail position). Dashed + theme-inherited text, so lime stays\n reserved for the CTA rather than tinting a non-action element. */\n.lb-mix-match__add-product {\n display: flex;\n align-items: center;\n gap: 12px;\n width: 100%;\n margin-top: 12px;\n padding: 8px;\n background: none;\n border: 1px dashed color-mix(in srgb, var(--lb-text) 28%, transparent);\n border-radius: var(--lb-radius);\n color: color-mix(in srgb, var(--lb-text) 75%, transparent);\n font-family: inherit;\n font-size: 15px;\n font-weight: 500;\n line-height: 1.2;\n text-align: left;\n cursor: pointer;\n box-sizing: border-box;\n transition: border-color 0.25s ease, color 0.25s ease;\n}\n\n.lb-mix-match__add-product:hover {\n border-color: color-mix(in srgb, var(--lb-text) 45%, transparent);\n color: var(--lb-text);\n}\n\n.lb-mix-match__add-product:focus-visible {\n outline: 2px solid var(--lb-primary-color);\n outline-offset: 2px;\n box-shadow: none;\n}\n\n.lb-mix-match__add-product-icon {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 60px;\n min-width: 60px;\n height: 40px;\n flex-shrink: 0;\n color: inherit;\n}\n\n/* Derived \"small\" radius for the picker's chips and badges — the progress\n segments, the \"Added\" badge, the close button, the filter pills. The\n picker-scoped mirror of --lb-radius-sm, and proportional for the same reason\n (see bundle-base.css).\n\n Everything the merchant would call a control or an image — the modal, search,\n product tiles, thumbnails, variant dropdown, quantity stepper and the\n buttons — takes the full --lb-picker-radius instead. That is what the\n editor's own Borders copy promises them, and the reduced tier had it\n rendering square at the Subtle preset (issue #363).\n\n Declared on BOTH hosts the modal can live under. On the storefront it portals\n to [data-modal-overlay], which the variant listbox portals up to as well. In\n the admin widget editor there is no overlay at all — the preview renders the\n modal directly under .lb-bundle-widget — so scoping this to the overlay alone\n left the variable undefined there and every control using it fell back to\n square corners while the modal and tiles rounded correctly (issue #363). */\n.lb-bundle-widget,\n[data-modal-overlay] {\n --lb-picker-radius-sm: calc(var(--lb-picker-radius) * 2 / 3);\n}\n\n/* === Modal Overlay === */\n.lb-mix-match__modal-overlay {\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(0, 0, 0, 0.5);\n z-index: 9999;\n display: flex;\n align-items: center;\n justify-content: center;\n opacity: 0;\n transition: opacity 0.2s ease-out;\n}\n\n.lb-mix-match__modal-overlay--open {\n opacity: 1;\n}\n\n/* === Modal Panel === */\n.lb-mix-match__modal {\n background: var(--lb-picker-bg);\n color: var(--lb-picker-text);\n border-radius: var(--lb-picker-radius);\n border: var(--lb-picker-border-width) solid var(--lb-picker-border-color);\n box-sizing: border-box;\n width: 100%;\n max-width: 520px;\n max-height: 70vh;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.16);\n transform: translateY(24px);\n transition: transform 0.25s ease-out;\n will-change: transform;\n}\n\n.lb-mix-match__modal-overlay--open .lb-mix-match__modal {\n transform: translateY(0);\n}\n\n/* === Modal Header === */\n.lb-mix-match__modal-header {\n display: flex;\n flex-direction: column;\n gap: 16px;\n padding: 20px;\n border-bottom: 1px solid color-mix(in srgb, var(--lb-picker-text) 15%, transparent);\n flex-shrink: 0;\n}\n\n.lb-mix-match__modal-header-top {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: 12px;\n}\n\n.lb-mix-match__modal-heading {\n min-width: 0;\n}\n\n.lb-mix-match__modal-title {\n font-size: 20px;\n font-weight: 600;\n line-height: 24px;\n margin: 0;\n color: inherit;\n}\n\n/* Dynamic \"Choose N more to unlock X% off\" line under the title. */\n.lb-mix-match__modal-subtitle {\n margin: 4px 0 0;\n font-size: 13px;\n line-height: 18px;\n color: color-mix(in srgb, var(--lb-picker-text) 60%, transparent);\n}\n\n.lb-mix-match__modal-subtitle:empty {\n display: none;\n}\n\n.lb-mix-match__modal-close {\n min-width: 44px;\n min-height: 44px;\n display: flex;\n align-items: center;\n justify-content: center;\n background: none;\n border: none;\n cursor: pointer;\n color: var(--lb-picker-text);\n border-radius: var(--lb-picker-radius-sm);\n padding: 0;\n margin: -12px -12px 0 0;\n transition: background 0.25s ease;\n}\n\n.lb-mix-match__modal-close:hover {\n background: color-mix(in srgb, var(--lb-picker-text) 5%, transparent);\n}\n\n.lb-mix-match__modal-close:focus-visible {\n outline: 2px solid var(--lb-primary-color);\n outline-offset: 0;\n box-shadow: none;\n}\n\n/* === Modal Filter Pills === */\n.lb-mix-match__filters {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n /* Bottom padding lives on the filters (a fixed section) rather than the\n scrolling list, so the gap below the pills stays put as the list scrolls. */\n padding: 16px 20px 16px;\n flex-shrink: 0;\n}\n\n.lb-mix-match__filter {\n padding: 6px 14px;\n border: var(--lb-picker-border-width) solid var(--lb-picker-border-color);\n /* Pills followed the merchant's picker radius as of #363 — a fixed 999px\n read as a stray rounded shape in a picker set to None or Subtle. */\n border-radius: var(--lb-picker-radius-sm);\n background: transparent;\n color: var(--lb-picker-text);\n font-family: inherit;\n font-size: 13px;\n font-weight: 500;\n line-height: 1;\n cursor: pointer;\n white-space: nowrap;\n transition: background 0.25s ease, border-color 0.25s ease, color 0.25s ease;\n}\n\n.lb-mix-match__filter:focus-visible {\n outline: 2px solid var(--lb-primary-color);\n outline-offset: 2px;\n box-shadow: none;\n}\n\n/* Picker vars, not widget vars — the modal is its own colour context;\n mixing the two palettes can render the active label invisible. */\n.lb-mix-match__filter--active {\n background: var(--lb-picker-text);\n border-color: var(--lb-picker-text);\n color: var(--lb-picker-bg);\n}\n\n/* === Modal Search === */\n.lb-mix-match__modal-search {\n padding: 16px 20px 0;\n position: relative;\n flex-shrink: 0;\n}\n\n/* Search icon pinned inside the left edge of the input. Rendered as a masked\n pseudo-element on the search row (an <input> can't host ::before) so the\n icon colour tracks the merchant's picker text at 60% — consistent with the\n variant-select chevron and the rest of the picker. */\n.lb-mix-match__modal-search::before {\n content: \"\";\n position: absolute;\n left: 36px;\n top: 16px;\n bottom: 0;\n width: 18px;\n background-color: color-mix(in srgb, var(--lb-picker-text) 60%, transparent);\n -webkit-mask: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0'/%3E%3Cpath d='M21 21l-6 -6'/%3E%3C/svg%3E\") center / 18px no-repeat;\n mask: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0'/%3E%3Cpath d='M21 21l-6 -6'/%3E%3C/svg%3E\") center / 18px no-repeat;\n pointer-events: none;\n}\n\n.lb-mix-match__modal-search-input {\n width: 100%;\n padding: 12px 40px 12px 44px;\n border: var(--lb-picker-border-width) solid var(--lb-picker-border-color);\n border-radius: var(--lb-picker-radius);\n font-size: 16px;\n line-height: 20px;\n color: var(--lb-picker-text);\n /* padding-left (above) clears the search icon, which is drawn by\n .lb-mix-match__modal-search::before so it can take a themed colour. */\n background-color: var(--lb-picker-bg);\n box-sizing: border-box;\n -webkit-appearance: none;\n appearance: none;\n}\n\n.lb-mix-match__modal-search-input::placeholder {\n color: color-mix(in srgb, var(--lb-picker-text) 50%, transparent);\n}\n\n.lb-mix-match__modal-search-input:focus-visible {\n outline: 2px solid var(--lb-primary-color);\n outline-offset: 2px;\n box-shadow: none;\n}\n\n.lb-mix-match__modal-search-clear {\n position: absolute;\n right: 32px;\n /* Anchor to the input area only — the search row's top padding (16px) plus\n the input height keeps the button centred on it. */\n top: 16px;\n bottom: 0;\n min-width: 32px;\n min-height: 32px;\n display: flex;\n align-items: center;\n justify-content: center;\n background: none;\n border: none;\n cursor: pointer;\n color: color-mix(in srgb, var(--lb-picker-text) 60%, transparent);\n padding: 0;\n}\n\n/* When the filter row isn't shown (filters off, or fewer than two product\n types), the search row owns the gap above the product list. The icon and\n clear button re-anchor to bottom: 16px so they stay centred on the input. */\n.lb-mix-match__modal--filters-hidden .lb-mix-match__modal-search {\n padding-bottom: 16px;\n}\n\n.lb-mix-match__modal--filters-hidden .lb-mix-match__modal-search::before,\n.lb-mix-match__modal--filters-hidden .lb-mix-match__modal-search-clear {\n bottom: 16px;\n}\n\n/* === Modal Product List === */\n.lb-mix-match__modal-list {\n overflow-y: auto;\n flex: 1;\n /* Always keep a small top padding so the first row's count bubbles\n (which sit at top: -8px) aren't clipped by overflow. The larger gap\n above the list comes from the filters' bottom padding, or — when no\n filters show — the search's bottom padding (see --filters-hidden). */\n padding: 8px 20px 20px;\n -webkit-overflow-scrolling: touch;\n /* Two-column product grid. Each product is a vertical card (image on\n top, info below) built by JS. Stays 2-up on mobile too (the modal\n is a bottom sheet there). */\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n column-gap: 24px;\n row-gap: 28px;\n align-content: start;\n}\n\n/* When neither search nor filters render, the list follows the header\n directly and needs the full top gap restored. */\n.lb-mix-match__modal-header + .lb-mix-match__modal-list {\n padding-top: 16px;\n}\n\n.lb-mix-match__modal-product {\n display: flex;\n flex-direction: column;\n padding: 8px;\n border: var(--lb-picker-border-width) solid var(--lb-picker-border-color);\n border-radius: var(--lb-picker-radius);\n box-sizing: border-box;\n transition: border-color 0.25s ease, background 0.25s ease;\n}\n\n.lb-mix-match__modal-product-thumb {\n /* Fills the grid cell — the image sets the visual size now, not a fixed\n 60px thumb. Aspect-ratio + object-fit (below) crop to the merchant's\n pickerThumbnailRatio; \"original\" shows the full uncropped image. */\n width: 100%;\n /* Positioning context for the \"Added\" badge. */\n position: relative;\n /* Modal picker thumbs follow the merchant's pickerThumbnailRatio —\n independent from the main widget's thumbnailRatio so a merchant can\n e.g. show tall picker thumbs with square main thumbs. */\n aspect-ratio: var(--lb-picker-thumbnail-aspect-ratio, 1 / 1);\n border-radius: var(--lb-picker-radius);\n box-sizing: border-box;\n overflow: hidden;\n background: var(--lb-thumbnail-bg);\n margin-bottom: 10px;\n}\n\n.lb-mix-match__modal-product-thumb img {\n width: 100%;\n /* Height + fit come from pickerThumbnailRatio — \"original\" sets both\n to auto/contain so the image renders uncropped at intrinsic ratio. */\n height: var(--lb-picker-thumbnail-img-height, 100%);\n object-fit: var(--lb-picker-thumbnail-img-fit, cover);\n border-radius: var(--lb-picker-radius);\n}\n\n.lb-mix-match__modal-product-info {\n flex: 1;\n min-width: 0;\n}\n\n.lb-mix-match__modal-product-title {\n font-size: 16px;\n font-weight: 600;\n line-height: 20px;\n color: inherit;\n margin: 0;\n}\n\n/* Modal titles link to the product page (new tab) — quiet by default,\n underline on hover so the row's Add affordance stays dominant. */\n.lb-mix-match__modal-product-title a {\n color: inherit;\n /* Underline fades in — text-decoration itself can't transition, but its\n color can. */\n text-decoration: underline;\n text-decoration-color: transparent;\n transition: text-decoration-color 0.25s ease;\n}\n\n.lb-mix-match__modal-product-title a:hover {\n text-decoration-color: currentColor;\n}\n\n.lb-mix-match__modal-product-title a:focus-visible {\n outline: 2px solid var(--lb-primary-color);\n outline-offset: 2px;\n border-radius: 2px;\n}\n\n.lb-mix-match__modal-product-price {\n display: flex;\n align-items: baseline;\n gap: 6px;\n font-size: 14px;\n line-height: 20px;\n color: inherit;\n margin: 2px 0 0;\n}\n\n.lb-mix-match__modal-product-compare {\n /* --lb-product-compare-display fallback is the \"on\" branch; Liquid sets 'none' when merchant disables. */\n display: var(--lb-product-compare-display, inline);\n font-size: 13px;\n font-weight: 400;\n color: color-mix(in srgb, var(--lb-text) 55%, transparent);\n text-decoration: line-through;\n}\n\n.lb-mix-match__modal-product-compare[hidden] {\n display: none;\n}\n\n.lb-mix-match__modal-product-unit-price {\n font-size: 12px;\n line-height: 16px;\n color: color-mix(in srgb, var(--lb-text) 60%, transparent);\n margin: 2px 0 0;\n}\n\n.lb-mix-match__modal-product-unit-price[hidden] {\n display: none;\n}\n\n.lb-mix-match__variant-select {\n font-size: 12px;\n padding: 4px 24px 4px 8px;\n border: var(--lb-picker-border-width) solid var(--lb-picker-border-color);\n border-radius: var(--lb-picker-radius);\n background-color: var(--lb-picker-bg);\n background-image: var(--lb-picker-variant-chevron);\n background-repeat: no-repeat;\n background-position: right 6px center;\n background-size: 12px;\n color: var(--lb-picker-text);\n font-family: inherit;\n box-sizing: border-box;\n min-height: 40px;\n max-height: 40px;\n cursor: pointer;\n width: 100%;\n max-width: 100%;\n appearance: none;\n -webkit-appearance: none;\n}\n\n.lb-mix-match__variant-select:focus-visible {\n outline: 2px solid var(--lb-primary-color);\n outline-offset: 2px;\n box-shadow: none;\n}\n\n/* Per-pick quantity stepper inside the picker modal product row.\n Three flex cells (− / number / +) sharing a single outer border that\n inherits the modal \"Borders\" (--lb-picker-border-* / --lb-picker-radius),\n like every other control in the modal. The cell dividers come from a 1px\n border on the centre value rather than per-button borders, so the rounded\n outer corners stay clean. */\n/* Quantity label + stepper sit together in a .lb-bundle-variant-option-group\n wrapper so the label-to-stepper gap inherits the same 2px the variant\n pickers use. The wrapper carries the margin-top that spaces the qty\n group from the unit-price / low-stock-badge above; the stepper itself\n has no top margin so the group can be repositioned without coupling. */\n.lb-mix-match__qty-stepper-group {\n /* Override the base .lb-bundle-variant-option-group column flex so the\n stepper child stretches on the cross-axis (vertical) — this is what\n lets the group track the Add button's height in the action row\n without pinning a fixed pixel value. */\n flex-direction: row;\n width: fit-content;\n}\n\n.lb-mix-match__qty-stepper {\n display: inline-flex;\n align-items: center;\n justify-content: space-between;\n width: 100%;\n flex-shrink: 0;\n border: var(--lb-picker-border-width) solid var(--lb-picker-border-color);\n border-radius: var(--lb-picker-radius);\n background-color: var(--lb-picker-bg);\n overflow: hidden;\n box-sizing: border-box;\n /* Pin the height so the stepper and Add button stay equal-height even when\n they wrap onto separate rows on narrow (mobile) cards — without this the\n stepper loses its cross-axis stretch reference and shrinks. */\n min-height: 32px;\n max-height: 32px;\n}\n\n.lb-mix-match__qty-stepper-button {\n appearance: none;\n -webkit-appearance: none;\n background: transparent;\n border: none;\n margin: 0;\n padding: 0 4px;\n width: 24px;\n height: 24px;\n box-sizing: border-box;\n font-family: inherit;\n font-size: 16px;\n font-weight: 500;\n line-height: 1;\n color: var(--lb-picker-text);\n cursor: pointer;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n}\n\n.lb-mix-match__qty-stepper-button:focus-visible {\n outline: 2px solid var(--lb-primary-color);\n outline-offset: -2px;\n box-shadow: none;\n}\n\n.lb-mix-match__qty-stepper-button:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n}\n\n.lb-mix-match__qty-stepper-value {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 24px;\n height: 24px;\n font-family: inherit;\n font-size: 13px;\n font-weight: 500;\n color: var(--lb-picker-text);\n box-sizing: border-box;\n}\n\n/* Action row — qty stepper (left) + Add button (right). Sits at the\n bottom of the info column with a 12px top margin so it visually\n separates from the variant pickers / price block above. When the\n stepper is hidden, Add fills the row via flex: 1 below.\n align-items: stretch so the stepper-group tracks the Add button's\n height (driven by font size + button padding) without a pinned px. */\n.lb-mix-match__modal-product-actions {\n display: flex;\n flex-wrap: wrap;\n align-items: stretch;\n row-gap: 8px;\n column-gap: 8px;\n margin-top: 12px;\n}\n\n.lb-mix-match__modal-add {\n flex: 1;\n padding: 8px 16px;\n background: var(--lb-picker-add-bg);\n color: var(--lb-picker-add-label);\n border: var(--lb-picker-add-border-width) solid var(--lb-picker-add-border-color);\n border-radius: var(--lb-picker-radius);\n box-sizing: border-box;\n font-size: 12px;\n font-weight: 600;\n cursor: pointer;\n /* Matches the qty stepper's pinned height so the two stay equal-height\n whether they sit on one row or wrap onto two on narrow (mobile) cards. */\n min-height: 32px;\n max-height: 32px;\n transition: opacity 0.25s ease, background 0.25s ease, color 0.25s ease, border-color 0.25s ease;\n}\n\n.lb-mix-match__modal-add:hover:not(:disabled) {\n opacity: 0.9;\n}\n\n.lb-mix-match__modal-add:focus-visible {\n outline: 2px solid var(--lb-primary-color);\n outline-offset: 2px;\n box-shadow: none;\n}\n\n.lb-mix-match__modal-add:disabled {\n background: color-mix(in srgb, var(--lb-picker-add-bg) 35%, var(--lb-picker-bg));\n color: color-mix(in srgb, var(--lb-picker-add-label) 85%, transparent);\n cursor: not-allowed;\n}\n\n/* In-bundle \"Remove\" — the Add button's colours INVERTED (label and\n background swap), no icons. The base :hover (opacity 0.9) applies as-is;\n state feedback lives on the thumbnail's \"Added\" badge, not the button. */\n.lb-mix-match__modal-add--added {\n background: var(--lb-picker-add-label);\n color: var(--lb-picker-add-bg);\n}\n\n/* Required-locked row — the Remove slot holds an inert quiet \"Required\"\n label (button disabled); the thumbnail's Added badge still carries the\n in-bundle state. Removal returns once another slot covers the product's\n minimum (variant swap flow). */\n.lb-mix-match__modal-add--added.lb-mix-match__modal-add--required {\n background: transparent;\n border-color: transparent;\n color: color-mix(in srgb, var(--lb-picker-text) 60%, transparent);\n cursor: default;\n}\n\n.lb-mix-match__modal-add--added.lb-mix-match__modal-add--required:hover {\n opacity: 1;\n}\n\n/* In-bundle card — border only, a touch darker than the resting border;\n the thumbnail badge carries the \"Added\" state (no background tint). */\n.lb-mix-match__modal-product--in-bundle {\n border-color: color-mix(in srgb, var(--lb-picker-text) 30%, transparent);\n}\n\n/* \"Added\" badge — top-right of the thumbnail, in the Add button's colours;\n pure state (pointer-events: none), the row's Remove button acts. */\n.lb-mix-match__modal-added-badge {\n position: absolute;\n top: 6px;\n right: 6px;\n background: var(--lb-picker-add-bg);\n color: var(--lb-picker-add-label);\n font-size: 11px;\n font-weight: 600;\n line-height: 1;\n padding: 8px 16px;\n border-radius: var(--lb-picker-radius-sm);\n pointer-events: none;\n}\n\n/* Sold out product row */\n.lb-mix-match__modal-product--sold-out {\n opacity: 0.5;\n}\n\n.lb-mix-match__modal-product--sold-out .lb-mix-match__modal-sold-out-label {\n font-size: 12px;\n color: inherit;\n font-weight: 500;\n white-space: nowrap;\n}\n\n/* \"Needs N spots\" hint — exact-size enforcement: the product's minimum\n pick exceeds the bundle's remaining open spots, so its Add and stepper\n are disabled. Same quiet label treatment as the sold-out state. */\n.lb-mix-match__modal-needs-spots {\n display: block;\n margin-top: 8px;\n font-size: 12px;\n color: inherit;\n font-weight: 500;\n white-space: nowrap;\n opacity: 0.7;\n}\n\n/* === Modal Empty State === */\n.lb-mix-match__modal-empty {\n padding: 32px 20px;\n text-align: center;\n}\n\n.lb-mix-match__modal-empty p {\n margin: 0;\n font-size: 16px;\n color: var(--lb-picker-text);\n}\n\n/* === Modal Footer === */\n.lb-mix-match__modal-footer {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n padding: 16px 20px;\n border-top: 1px solid color-mix(in srgb, var(--lb-picker-text) 15%, transparent);\n flex-shrink: 0;\n}\n\n.lb-mix-match__modal-footer-count {\n font-size: 13px;\n font-weight: 500;\n color: color-mix(in srgb, var(--lb-picker-text) 70%, transparent);\n}\n\n/* \"Done\" reuses the picker Add-button styling so it matches the Add buttons.\n The flex centring + min-height pin the footer-button silhouette: the wizard\n Back (bundle-multi-step.css) copies these metrics so the pair stays\n equal-height whatever border widths the merchant's picker tokens set. */\n.lb-mix-match__modal-done {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-height: 40px;\n padding: 10px 24px;\n transition: opacity 0.25s ease;\n background: var(--lb-picker-add-bg);\n color: var(--lb-picker-add-label);\n border: var(--lb-picker-add-border-width) solid var(--lb-picker-add-border-color);\n border-radius: var(--lb-picker-radius);\n box-sizing: border-box;\n font-family: inherit;\n font-size: 14px;\n font-weight: 600;\n cursor: pointer;\n}\n\n.lb-mix-match__modal-done:hover:not(:disabled) {\n opacity: 0.9;\n}\n\n.lb-mix-match__modal-done:focus-visible {\n outline: 2px solid var(--lb-primary-color);\n outline-offset: 2px;\n box-shadow: none;\n}\n\n/* Disabled Next/Done (wizard step minimum not met) — same washed-out\n treatment as a disabled row Add, so an unfinished step reads at a\n glance. Hover is gated off above; the solid button returns the moment\n the requirement is met. */\n.lb-mix-match__modal-done:disabled {\n background: color-mix(in srgb, var(--lb-picker-add-bg) 35%, var(--lb-picker-bg));\n color: color-mix(in srgb, var(--lb-picker-add-label) 85%, transparent);\n cursor: not-allowed;\n}\n\n/* Hidden utility for search filtering */\n.lb-hidden {\n display: none !important;\n}\n\n/* === Desktop: stepper group + Add button share the action row equally === */\n@media (min-width: 768px) {\n .lb-mix-match__qty-stepper-group {\n flex: 1;\n }\n}\n\n/* === Mobile Full-Screen Modal === */\n@media (max-width: 767px) {\n .lb-mix-match__modal-overlay {\n align-items: flex-end;\n }\n\n .lb-mix-match__modal {\n max-width: 100%;\n max-height: 90vh;\n /* Bottom sheet: only the top corners round, but they round to whatever the\n merchant picked rather than a fixed 16px (issue #363). */\n border-radius: var(--lb-picker-radius) var(--lb-picker-radius) 0 0;\n transform: translateY(100%);\n }\n\n .lb-mix-match__modal-overlay--open .lb-mix-match__modal {\n transform: translateY(0);\n }\n\n /* Touch targets: bump the picker controls to ~44px on touch devices.\n The stepper and Add button stay equal-height (both pinned to 44px). */\n .lb-mix-match__variant-select {\n width: 100%;\n max-width: 100%;\n min-height: 44px;\n max-height: none;\n }\n\n .lb-mix-match__qty-stepper {\n min-height: 44px;\n max-height: 44px;\n }\n\n .lb-mix-match__qty-stepper-button {\n width: 44px;\n height: 44px;\n }\n\n .lb-mix-match__modal-add {\n min-height: 44px;\n max-height: 44px;\n }\n\n /* Full-width qty stepper on mobile — once it wraps below the Add button,\n stretch it to the row and space the −/value/+ across it so it mirrors\n the full-width button instead of staying a compact control. */\n .lb-mix-match__qty-stepper-group {\n width: 100%;\n }\n}\n\n/* Small phones: a 2-up card grid leaves each picker card cramped (image +\n variant selects + stepper + Add at ~148px wide). Drop to a single column\n below 430px; larger phones and up keep the 2-up grid. */\n@media (max-width: 430px) {\n .lb-mix-match__modal-list {\n grid-template-columns: 1fr;\n }\n}\n\n/* === Reduced Motion === */\n@media (prefers-reduced-motion: reduce) {\n .lb-mix-match__modal-overlay,\n .lb-mix-match__modal,\n .lb-mix-match__progress-segment,\n .lb-mix-match__progress-segment::after {\n transition: none;\n }\n}\n`;\nexport const BUNDLE_VOLUME_CSS = `/* Lime Bundles — Volume / Quantity Breaks styles */\n\n.lb-volume__tiers {\n display: flex;\n flex-direction: column;\n gap: 16px;\n max-height: var(--lb-list-max-height);\n overflow-y: auto;\n /* Scrollbar hidden + edge fade — see .lb-edge-fade in bundle-base.css. */\n}\n\n/* Reserve room for the floating \"Most popular\" badge ONLY when the first tier\n is the popular one — that's the single case where the badge has no card above\n it to float into and would otherwise clip at the scroll container's top. */\n.lb-volume__tiers:has(> .lb-volume__tier:first-child .lb-volume__tier-badge) {\n padding-top: 12px;\n}\n\n/* Tier card — mirrors the fixed / mix & match bundle card so volume reads as\n the same app: a bordered card with matching geometry, a price row, and a\n right-slot chip. The radio sits where those cards put the thumbnail, since a\n volume tier is the same product at a different quantity (no thumbnail). */\n.lb-volume__tier {\n position: relative;\n display: flex;\n align-items: center;\n gap: 12px;\n border: var(--lb-border-width) solid var(--lb-border);\n border-radius: var(--lb-radius);\n padding: 12px;\n /* border-box so the selected tier's thicker (2px) border grows inward and\n doesn't shift the card by 1px when selected. */\n box-sizing: border-box;\n cursor: pointer;\n transition: border-color 0.25s ease, background 0.25s ease;\n}\n\n.lb-volume__tier:hover {\n border-color: color-mix(in srgb, var(--lb-text) 28%, transparent);\n}\n\n/* Suppress the UA default focus outline + Dawn's focus shadow so a freshly\n clicked tier doesn't briefly show the focus ring on top of the selected\n state. Keyboard focus is still indicated by the :focus-visible rule. */\n.lb-volume__tier:focus {\n outline: none;\n box-shadow: none;\n}\n\n.lb-volume__tier:focus-visible:not([aria-checked=\"true\"]) {\n outline: 2px solid var(--lb-primary-color);\n outline-offset: -2px;\n box-shadow: none;\n}\n\n/* Selected tier — the same quiet neutral highlight as the mix & match\n in-bundle / Added card (soft tint + stronger border), plus the filled\n radio, so \"selected\" looks the same everywhere in the app. */\n.lb-volume__tier[aria-checked=\"true\"] {\n border-color: var(--lb-text);\n border-width: 2px;\n}\n\n.lb-volume__radio {\n width: 20px;\n height: 20px;\n min-width: 20px;\n flex: none;\n box-sizing: border-box;\n border: 2px solid color-mix(in srgb, var(--lb-text) 45%, transparent);\n border-radius: 50%;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: border-color 0.25s ease;\n}\n\n.lb-volume__tier[aria-checked=\"true\"] .lb-volume__radio {\n border-color: var(--lb-text);\n}\n\n.lb-volume__radio-dot {\n width: 10px;\n height: 10px;\n border-radius: 50%;\n background: transparent;\n transition: background 0.25s ease;\n}\n\n.lb-volume__tier[aria-checked=\"true\"] .lb-volume__radio-dot {\n background: var(--lb-text);\n}\n\n/* Info column: the quantity over the price row. */\n.lb-volume__tier-info {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n}\n\n.lb-volume__tier-label {\n font-size: 16px;\n font-weight: 600;\n line-height: 20px;\n color: var(--lb-text);\n}\n\n/* Right column — stacks the \"Most popular\" badge (when present) over the\n savings chip, so the markers group on the right and the quantity row stays\n clean. */\n.lb-volume__tier-right {\n flex: none;\n display: flex;\n flex-direction: column;\n align-items: flex-end;\n gap: 6px;\n}\n\n/* \"Most popular\" pill — a floating crown straddling the card's top-right edge.\n The 16px gap on .lb-volume__tiers leaves room for it above each card. */\n.lb-volume__tier-badge {\n position: absolute;\n top: -12px;\n right: 12px;\n z-index: 1;\n font-size: 12px;\n font-weight: 500;\n line-height: 1;\n color: var(--lb-popular-badge-text);\n background: var(--lb-popular-badge-bg);\n border: var(--lb-popular-badge-border-width) solid var(--lb-popular-badge-border-color);\n border-radius: var(--lb-radius-sm);\n padding: 4px 9px;\n}\n\n/* Price row — the same struck → per-unit → \"each\" treatment the product cards\n use (compare-at muted and struck, per-unit price, tabular figures). */\n.lb-volume__tier-price {\n display: flex;\n align-items: baseline;\n gap: 6px;\n flex-wrap: wrap;\n margin-top: 4px;\n font-size: 14px;\n color: var(--lb-text);\n font-variant-numeric: tabular-nums;\n}\n\n.lb-volume__tier-compare {\n color: color-mix(in srgb, var(--lb-text) 55%, transparent);\n text-decoration: line-through;\n}\n\n.lb-volume__tier-unit {\n font-size: 12px;\n color: color-mix(in srgb, var(--lb-text) 60%, transparent);\n}\n\n/* Savings chip — the same soft neutral chip as the fixed card's quantity chip,\n here carrying the per-tier discount so \"buy more, save more\" is scannable. */\n.lb-volume__tier-savings {\n flex-shrink: 0;\n display: inline-flex;\n align-items: center;\n height: 28px;\n padding: 0 8px;\n border-radius: var(--lb-radius-sm);\n background: color-mix(in srgb, var(--lb-text) 7%, transparent);\n color: color-mix(in srgb, var(--lb-text) 75%, transparent);\n font-size: 13px;\n font-weight: 500;\n line-height: 1;\n white-space: nowrap;\n font-variant-numeric: tabular-nums;\n}\n\n/* Tier the selected variant can't cover — same greyed treatment as an\n out-of-stock product row, and not selectable. */\n.lb-volume__tier--oos {\n opacity: 0.5;\n cursor: not-allowed;\n}\n`;\n\nexport const BUNDLE_BOGO_CSS = `/* Lime Bundles — BOGO (Buy X Get Y) bundle styles */\n\n.lb-bogo__products {\n display: flex;\n flex-direction: column;\n gap: 12px;\n margin: 0;\n}\n\n/* Buy/get product cards — same bordered-card construction as fixed rows so\n the two bundle types read the same on a storefront. */\n.lb-bogo .lb-bundle-product-row {\n display: flex;\n align-items: flex-start;\n gap: 12px;\n padding: 8px;\n border: var(--lb-border-width) solid var(--lb-border);\n border-radius: var(--lb-radius);\n box-sizing: border-box;\n}\n\n.lb-bogo .lb-bundle-thumbnail {\n width: 60px;\n min-width: 60px;\n border-radius: var(--lb-radius);\n box-sizing: border-box;\n}\n\n.lb-bogo .lb-bundle-thumbnail img {\n border-radius: var(--lb-radius);\n border: none;\n}\n\n/* The \"+\" divider between the buy and get cards — a small accent medallion\n so the offer's hinge (\"this plus that\") stands out. Circular by design:\n it is a connector dot, not a card control, so it sits outside the\n --lb-radius system (blessed in WIDGET-DESIGN.md). */\n.lb-bogo__plus {\n align-self: center;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 24px;\n height: 24px;\n border-radius: 50%;\n background: var(--lb-primary-color);\n color: var(--lb-btn-text);\n}\n\n/* Reward badge on the get card: \"Free\" or \"P% off\". Neutral chip colors\n (right-slot chip family) — the accent stays reserved for the CTA and the\n plus medallion; 600 weight carries the reward emphasis. */\n.lb-bogo__badge {\n display: inline-flex;\n align-items: center;\n align-self: flex-start;\n flex-shrink: 0;\n height: 28px;\n padding: 0 8px;\n border-radius: var(--lb-radius-sm);\n background: color-mix(in srgb, var(--lb-text) 7%, transparent);\n color: color-mix(in srgb, var(--lb-text) 75%, transparent);\n font-size: 13px;\n font-weight: 600;\n line-height: 1;\n white-space: nowrap;\n}\n\n/* Struck-through original price on the get card when the reward applies. */\n.lb-bogo .lb-bundle-product-compare-price {\n text-decoration: line-through;\n opacity: 0.6;\n}\n`;\n\nexport const BUNDLE_MULTI_STEP_CSS = `/**\n * Lime Bundles — Multi-step bundle widget styles (wizard chrome ONLY).\n *\n * The multi-step widget reuses the mix & match classes wholesale —\n * bundle-widget.liquid emits bundle-mix-match.css whenever has_multi_step\n * is set. This file carries only what the wizard adds on top: step groups\n * in the widget body, the group heading + count chip, and the modal's\n * three-slot Back / count / Next-Done footer.\n *\n * Canon: WIDGET-DESIGN.md (spacing scale, radius system, chip family).\n */\n\n/* ── Step groups (widget body) ────────────────────────────────────\n One dashed door + one section per step. A step without picks shows a\n slim checklist row; a step with picks shows the heading + chosen\n cards. data-step-mode (\"row\" | \"open\") on the section picks the\n representation — both are in the markup, CSS hides the other, so the\n server-rendered untouched state IS the hydrated no-selection state.\n Spacing is per-pair rather than a flat gap: consecutive rows sit\n flush (hairline-separated), everything else breathes 16px. */\n\n.lb-multi-step__groups {\n display: flex;\n flex-direction: column;\n gap: 0;\n}\n\n.lb-multi-step__group {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n.lb-multi-step__group + .lb-multi-step__group {\n margin-top: 16px;\n}\n\n/* A run of empty-step rows reads as one checklist: flush, hairlines\n between (the first row of a run keeps no border). */\n.lb-multi-step__group[data-step-mode=\"row\"] + .lb-multi-step__group[data-step-mode=\"row\"] {\n margin-top: 0;\n}\n\n.lb-multi-step__group[data-step-mode=\"row\"]\n + .lb-multi-step__group[data-step-mode=\"row\"]\n .lb-multi-step__step-row {\n border-top: var(--lb-border-width) solid var(--lb-border);\n}\n\n/* Mode switch: a row-mode step hides its open chrome and vice versa. */\n.lb-multi-step__group[data-step-mode=\"row\"] .lb-multi-step__group-heading,\n.lb-multi-step__group[data-step-mode=\"row\"] .lb-multi-step__group-slots {\n display: none;\n}\n\n.lb-multi-step__group[data-step-mode=\"open\"] .lb-multi-step__step-row {\n display: none;\n}\n\n/* Checklist row — the empty step's whole representation, and a tap\n target that deep-links the wizard to that step. Quiet by design: the\n door above it is the primary action. */\n.lb-multi-step__step-row {\n display: flex;\n align-items: center;\n gap: 12px;\n width: 100%;\n min-height: 48px;\n padding: 10px 2px;\n background: none;\n border: none;\n font-family: inherit;\n text-align: left;\n cursor: pointer;\n box-sizing: border-box;\n transition: background-color 0.25s ease;\n /* Deliberately no border-radius. This row paints the checklist hairline as\n its own border-top (see the run rule above), and a radius tapers that\n border into the corner arcs — the dividers came out visibly curved at the\n \"round\" preset (issue #363). A full-bleed list row is meant to be square\n anyway; the hover tint below spans the card width, so it has no corner to\n round against. The :focus-visible radius is fine: an outline is a\n separate paint and never touches the border. */\n}\n\n.lb-multi-step__step-row:hover {\n background: color-mix(in srgb, var(--lb-text) 3%, transparent);\n}\n\n.lb-multi-step__step-row:focus-visible {\n outline: 2px solid var(--lb-primary-color);\n outline-offset: -2px;\n border-radius: var(--lb-radius-sm);\n}\n\n.lb-multi-step__step-row .lb-multi-step__group-count {\n margin-left: auto;\n}\n\n/* Step number — quiet index, tabular so multi-digit lists align. */\n.lb-multi-step__step-ix {\n width: 18px;\n min-width: 18px;\n font-size: 12px;\n font-weight: 500;\n color: color-mix(in srgb, var(--lb-text) 45%, transparent);\n font-variant-numeric: tabular-nums;\n}\n\n/* The door — the single wizard entry point, reusing the dashed\n add-product recipe with a two-line label. JS moves it directly above\n the first unsatisfied step and hides it once every step is met. The\n add-product recipe's own margin-top is for its mix & match position\n (below the slot list); here the pair rules own all spacing. */\n.lb-multi-step__door {\n margin-top: 0;\n}\n\n.lb-multi-step__door + .lb-multi-step__group {\n margin-top: 12px;\n}\n\n.lb-multi-step__door + .lb-multi-step__group[data-step-mode=\"row\"] {\n margin-top: 4px;\n}\n\n.lb-multi-step__group + .lb-multi-step__door {\n margin-top: 16px;\n}\n\n/* A hidden door is parked as the container's first child (updateDoor), so\n the group after it is really the first visible element — no door gap. */\n.lb-multi-step__door--hidden:first-child + .lb-multi-step__group {\n margin-top: 0;\n}\n\n.lb-multi-step__door-copy {\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.lb-multi-step__door-sub {\n font-size: 13px;\n font-weight: 400;\n line-height: 1.2;\n color: color-mix(in srgb, var(--lb-text) 55%, transparent);\n}\n\n/* Heading is a quiet, non-interactive row: step name left, live count\n chip right. The step's chosen cards below are the edit targets. */\n.lb-multi-step__group-heading {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n color: var(--lb-text);\n}\n\n.lb-multi-step__group-name {\n font-size: 14px;\n font-weight: 600;\n line-height: 1.4;\n}\n\n/* Count chip — right-slot chip family (28px, 0 8px, radius-sm, 13/500,\n neutral 7%/75% fill). Weight-only emphasis when the step is met. */\n.lb-multi-step__group-count {\n display: inline-flex;\n align-items: center;\n height: 28px;\n padding: 0 8px;\n border-radius: var(--lb-radius-sm);\n background: color-mix(in srgb, var(--lb-text) 7%, transparent);\n color: color-mix(in srgb, var(--lb-text) 75%, transparent);\n font-size: 13px;\n font-weight: 500;\n white-space: nowrap;\n}\n\n.lb-multi-step__group-count--met {\n font-weight: 600;\n}\n\n/* Empty step group: the slots container collapses so the heading rows\n read as a compact checklist until picks land. */\n.lb-multi-step__group-slots:empty {\n display: none;\n}\n\n/* ── Wizard modal footer: [Back] [count] [Next|Done] ───────────\n Three-column grid so the count stays dead-centre whether or not the\n Back button is showing (Next and Done share the right cell — only one\n is visible at a time). */\n.lb-multi-step__modal-footer {\n display: grid;\n grid-template-columns: 1fr auto 1fr;\n align-items: center;\n gap: 12px;\n}\n\n.lb-multi-step__modal-footer > .lb-multi-step__modal-back {\n grid-area: 1 / 1;\n justify-self: start;\n}\n\n.lb-multi-step__modal-footer > .lb-mix-match__modal-footer-count {\n grid-area: 1 / 2;\n text-align: center;\n}\n\n.lb-multi-step__modal-footer > .lb-mix-match__modal-done {\n grid-area: 1 / 3;\n justify-self: end;\n}\n\n/* Back — secondary (outlined) treatment beside the primary Next/Done\n (which reuse .lb-mix-match__modal-done). Same silhouette as Next —\n metrics copied from the modal-done recipe (padding, type, radius,\n min-height) — and picker tokens only: the modal is portaled out of the\n widget, so the widget vars (--lb-text, --lb-border-*) it previously\n used don't reliably resolve here and break palette isolation. */\n.lb-multi-step__modal-back {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-height: 40px;\n padding: 10px 24px;\n background: none;\n border: var(--lb-picker-border-width) solid var(--lb-picker-border-color);\n border-radius: var(--lb-picker-radius);\n box-sizing: border-box;\n color: var(--lb-picker-text);\n font-family: inherit;\n font-size: 14px;\n font-weight: 600;\n cursor: pointer;\n transition: background-color 0.25s ease;\n}\n\n.lb-multi-step__modal-back:hover {\n background: color-mix(in srgb, var(--lb-picker-text) 5%, transparent);\n}\n\n.lb-multi-step__modal-back:focus-visible {\n outline: 2px solid var(--lb-primary-color);\n outline-offset: 2px;\n}\n\n/* ── Step-change transition ────────────────────────────────────\n Moving between steps swaps the whole step-scoped region in a single\n paint, which reads as nothing happening (or as a glitch) rather than\n as arriving somewhere new — and worst of all under auto-advance,\n where the shopper never clicked anything. So the step assembles\n instead of arriving whole: the step label leads, the filter pills\n follow, then the product cards one after another. Staging is what\n makes the change legible; a uniform slide of everything at once only\n wobbles the card.\n\n The wizard stamps data-step-transition=\"forward\" | \"back\" on the\n dialog on every step change (manual Next/Back and auto-advance\n alike, never on open, where the modal entrance already carries the\n motion), and clears it again once the cascade has played — see\n STEP_CASCADE_MS in multi-step/wizard.ts. That clear is load-bearing,\n not tidiness: search and filter hide cards with .lb-hidden\n (display: none), and restoring display restarts a card's animation,\n so a live attribute would replay the whole cascade on every\n keystroke that reveals a card.\n\n Two elements deliberately hold still. The footer, because Back/Next\n are the shopper's anchor while everything above them moves. And the\n progress rail, because it is the one thing that answers \"where am I\n now\" — animating the shopper's only orientation cue is what made the\n previous version hard to read. Its segment fill carries its own\n state change already. */\n@keyframes lb-multi-step-cascade-fwd {\n from {\n opacity: 0;\n transform: translateY(10px);\n }\n}\n\n@keyframes lb-multi-step-cascade-back {\n from {\n opacity: 0;\n transform: translateY(-10px);\n }\n}\n\n.lb-mix-match__modal[data-step-transition] .lb-mix-match__modal-subtitle {\n animation: lb-multi-step-cascade-fwd 0.2s ease-out both;\n}\n\n.lb-mix-match__modal[data-step-transition] .lb-mix-match__filters {\n animation: lb-multi-step-cascade-fwd 0.2s ease-out 0.05s both;\n}\n\n/* Cards stagger 30ms apart, but only the first four: a step can hold\n dozens of products and the two-column grid shows about four at a\n time, so past that the stagger is below the fold and would only add\n lag. Everything from the fifth on shares the fourth's landing. */\n.lb-mix-match__modal[data-step-transition] .lb-mix-match__modal-list > * {\n animation: lb-multi-step-cascade-fwd 0.22s ease-out 0.2s both;\n}\n\n.lb-mix-match__modal[data-step-transition] .lb-mix-match__modal-list > :nth-child(1) {\n animation-delay: 0.09s;\n}\n\n.lb-mix-match__modal[data-step-transition] .lb-mix-match__modal-list > :nth-child(2) {\n animation-delay: 0.12s;\n}\n\n.lb-mix-match__modal[data-step-transition] .lb-mix-match__modal-list > :nth-child(3) {\n animation-delay: 0.15s;\n}\n\n.lb-mix-match__modal[data-step-transition] .lb-mix-match__modal-list > :nth-child(4) {\n animation-delay: 0.18s;\n}\n\n/* Going back runs the same cascade in the same order — only the offset\n flips, so the shopper still reads which way they moved. Overrides\n the name alone; the staggered delays above must survive. */\n.lb-mix-match__modal[data-step-transition=\"back\"] .lb-mix-match__modal-subtitle,\n.lb-mix-match__modal[data-step-transition=\"back\"] .lb-mix-match__filters,\n.lb-mix-match__modal[data-step-transition=\"back\"] .lb-mix-match__modal-list > * {\n animation-name: lb-multi-step-cascade-back;\n}\n\n@keyframes lb-multi-step-cascade-fade {\n from {\n opacity: 0;\n }\n}\n\n/* Reduced motion keeps the fade and drops the travel and the stagger:\n the step change still has to be perceptible, which is the whole\n point of this block. The card selector is :nth-child(n) rather than\n * on purpose — a plain * loses to the :nth-child(1..4) delays above\n on specificity, which would leave the stagger running with nothing\n moving. :nth-child(n) matches every child at equal specificity and\n wins on order. */\n@media (prefers-reduced-motion: reduce) {\n .lb-mix-match__modal[data-step-transition] .lb-mix-match__modal-subtitle,\n .lb-mix-match__modal[data-step-transition] .lb-mix-match__filters,\n .lb-mix-match__modal[data-step-transition] .lb-mix-match__modal-list > :nth-child(n) {\n animation: lb-multi-step-cascade-fade 0.15s ease-out both;\n }\n}\n`;\n\nexport const BUNDLE_DROPDOWN_CSS = `/**\n * Lime Bundles — Custom variant-picker dropdown styling.\n *\n * Reuses existing CSS variables: no new merchant-configurable surface.\n * --lb-border-width, --lb-border (global border), --lb-radius-sm (derived),\n * --lb-variant-chevron, --lb-bg, --lb-text, --lb-primary-color\n *\n * Mix-match modal context overrides via .lb-mix-match__modal scope to use\n * --lb-picker-variant-* and --lb-picker-bg.\n */\n\n/* Hide the native <select> while keeping it form-serializable and focusable\n programmatically. The .lb-dropdown-state marker is added by JS at bind\n time, so this rule matches every variant-select class (main widget,\n mix-match modal, future bundle types). aria-hidden + tabindex=-1\n (also set in JS) remove it from the accessibility tree. */\n.lb-dropdown-state {\n position: absolute !important;\n width: 1px !important;\n height: 1px !important;\n padding: 0 !important;\n margin: -1px !important;\n overflow: hidden !important;\n clip-path: inset(50%) !important;\n white-space: nowrap !important;\n border: 0 !important;\n pointer-events: none !important;\n}\n\n/* Shell fills its parent column. */\n.lb-dropdown {\n position: relative;\n display: inline-block;\n width: 100%;\n max-width: 100%;\n font-family: inherit;\n}\n\n/* Trigger styled identically to the closed-state native select */\n.lb-dropdown-trigger {\n display: inline-flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n width: 100%;\n border: var(--lb-border-width) solid var(--lb-border);\n border-radius: var(--lb-radius);\n padding: 8px 12px;\n font-size: 12px;\n line-height: 16px;\n color: var(--lb-text);\n background: var(--lb-bg);\n font-family: inherit;\n cursor: pointer;\n appearance: none;\n -webkit-appearance: none;\n text-align: start;\n transition: border-color 0.25s ease;\n}\n\n.lb-dropdown-trigger:focus-visible {\n outline: 2px solid var(--lb-primary-color);\n outline-offset: 2px;\n box-shadow: none;\n}\n\n.lb-dropdown-trigger[aria-expanded=\"true\"] {\n border-color: var(--lb-text);\n}\n\n.lb-dropdown-trigger-value {\n flex: 1 1 auto;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n text-align: start;\n}\n\n.lb-dropdown-chevron {\n flex: 0 0 auto;\n width: 12px;\n height: 12px;\n background: var(--lb-variant-chevron) center / contain no-repeat;\n transition: transform 0.25s ease;\n}\n\n.lb-dropdown-trigger[aria-expanded=\"true\"] .lb-dropdown-chevron {\n transform: rotate(180deg);\n}\n\n/* Popover panel — position: absolute against the .lb-dropdown shell\n (already position: relative). Top/left/width come from CSS so we\n never depend on JS having set inline coords by the time the panel\n becomes visible. JS only sets max-height. */\n.lb-dropdown-listbox {\n position: absolute;\n left: 0;\n /* Default to below-trigger placement so the panel doesn't overlap the\n trigger if data-placement is missing for any reason. The explicit\n [data-placement=\"down\"|\"up\"] rules below override this. */\n top: calc(100% + 4px);\n width: 100%;\n z-index: 9999;\n margin: 0;\n padding: 4px 0;\n list-style: none;\n background: var(--lb-bg);\n color: var(--lb-text);\n border: var(--lb-border-width) solid var(--lb-border);\n border-radius: var(--lb-radius);\n box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);\n overflow-y: auto;\n overflow-x: hidden;\n /* Quiet overlay scrollbar — transparent track, soft rounded thumb. The\n popover has a shadow + border, so it keeps a thumb rather than the inset\n lists' edge fade (a mask would clip the shadow). */\n scrollbar-width: thin;\n scrollbar-color: color-mix(in srgb, var(--lb-text) 18%, transparent) transparent;\n animation: lb-dropdown-in-down 120ms ease-out;\n transform-origin: top center;\n}\n\n.lb-dropdown-listbox[data-placement=\"down\"] {\n top: calc(100% + 4px);\n}\n\n.lb-dropdown-listbox[data-placement=\"up\"] {\n top: auto;\n bottom: calc(100% + 4px);\n animation-name: lb-dropdown-in-up;\n transform-origin: bottom center;\n}\n\n/* When portaled out of the .lb-dropdown shell (mix-match modal context:\n .lb-mix-match__modal applies translateY which would otherwise trap\n position:fixed), switch to fixed and let JS set viewport coords. */\n.lb-dropdown-listbox[data-lb-dropdown-portal] {\n position: fixed;\n top: auto;\n left: auto;\n bottom: auto;\n width: auto;\n}\n\n/* Quiet overlay scrollbar — Webkit/Blink: transparent track, soft rounded\n thumb that darkens on hover. */\n.lb-dropdown-listbox::-webkit-scrollbar {\n width: 6px;\n}\n.lb-dropdown-listbox::-webkit-scrollbar-track {\n background: transparent;\n}\n.lb-dropdown-listbox::-webkit-scrollbar-thumb {\n background: color-mix(in srgb, var(--lb-text) 16%, transparent);\n border-radius: 999px;\n}\n.lb-dropdown-listbox::-webkit-scrollbar-thumb:hover {\n background: color-mix(in srgb, var(--lb-text) 30%, transparent);\n}\n\n/* Options */\n.lb-dropdown-option {\n padding: 8px 12px;\n font-size: 12px;\n line-height: 16px;\n cursor: pointer;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n color: var(--lb-text);\n /* The active/hover tint below fills the option, so it rounds with the rest\n of the widget rather than squaring off against the listbox corner it sits\n in (issue #363). */\n border-radius: var(--lb-radius-sm);\n}\n\n.lb-dropdown-option[aria-selected=\"true\"] {\n font-weight: 600;\n}\n\n.lb-dropdown-option.is-active,\n.lb-dropdown-option:hover:not([aria-disabled=\"true\"]) {\n background: color-mix(in srgb, var(--lb-text) 2%, transparent);\n}\n\n.lb-dropdown-option[aria-disabled=\"true\"] {\n opacity: 0.4;\n cursor: not-allowed;\n}\n\n/* Animations */\n@keyframes lb-dropdown-in-down {\n from { opacity: 0; transform: translateY(-4px) scale(0.98); }\n to { opacity: 1; transform: translateY(0) scale(1); }\n}\n\n@keyframes lb-dropdown-in-up {\n from { opacity: 0; transform: translateY(4px) scale(0.98); }\n to { opacity: 1; transform: translateY(0) scale(1); }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .lb-dropdown-listbox { animation: none; }\n .lb-dropdown-chevron { transition: none; }\n .lb-dropdown-trigger { transition: none; }\n}\n\n/* Forced-colors mode (Windows high-contrast) */\n@media (forced-colors: active) {\n .lb-dropdown-trigger {\n border-color: ButtonBorder;\n color: ButtonText;\n background: ButtonFace;\n }\n .lb-dropdown-listbox {\n border-color: ButtonBorder;\n background: Canvas;\n color: CanvasText;\n }\n .lb-dropdown-option.is-active {\n background: Highlight;\n color: HighlightText;\n }\n}\n\n/* Mix-match modal context — use picker-scoped variables.\n No CSS fallbacks: --lb-picker-* are always emitted by bundle-widget.liquid\n because WidgetConfig.parse() fully hydrates the merchant config.\n See docs/solutions/ui-bugs/widget-css-single-source-defaults.md. */\n.lb-mix-match__modal .lb-dropdown-trigger {\n border-color: var(--lb-picker-border-color);\n border-width: var(--lb-picker-border-width);\n border-radius: var(--lb-picker-radius);\n background: var(--lb-picker-bg);\n color: var(--lb-picker-text);\n /* Match the picker qty stepper + Add button height (border-box so 32px is\n the full height including padding + border). */\n box-sizing: border-box;\n padding: 4px 8px;\n min-height: 32px;\n max-height: 32px;\n}\n\n.lb-mix-match__modal .lb-dropdown-chevron {\n background-image: var(--lb-picker-variant-chevron);\n}\n\n/* Listbox is portaled out of the transformed .lb-mix-match__modal up to\n its [data-modal-overlay] parent, so picker-scoped rules anchor on the\n overlay attribute, not the modal class. */\n[data-modal-overlay] > .lb-dropdown-listbox {\n border-color: var(--lb-picker-border-color);\n border-width: var(--lb-picker-border-width);\n border-radius: var(--lb-picker-radius);\n background: var(--lb-picker-bg);\n color: var(--lb-picker-text);\n scrollbar-color: color-mix(in srgb, var(--lb-picker-text) 15%, transparent)\n color-mix(in srgb, var(--lb-picker-text) 2%, transparent);\n}\n\n[data-modal-overlay] > .lb-dropdown-listbox .lb-dropdown-option {\n border-radius: var(--lb-picker-radius-sm);\n}\n\n[data-modal-overlay] > .lb-dropdown-listbox::-webkit-scrollbar-track {\n background: color-mix(in srgb, var(--lb-picker-text) 2%, transparent);\n border-radius: 2px;\n}\n[data-modal-overlay] > .lb-dropdown-listbox::-webkit-scrollbar-thumb {\n background: color-mix(in srgb, var(--lb-picker-text) 15%, transparent);\n}\n`;\n\n/**\n * Skeleton styles for the web component's loading state. Rendered\n * synchronously in connectedCallback → renderLoading() so the host\n * element has intrinsic size from render-0 and doesn't shift layout\n * when the real bundle paints. Web-component only (see file header).\n */\nexport const BUNDLE_SKELETON_CSS = `/* Lime Bundles — web-component loading skeleton (not mirrored to theme assets) */\n\n.lb-bundle-widget--loading {\n display: block;\n padding: var(--lb-widget-pad, 20px);\n border: 1px solid var(--lb-border, #E5E5E5);\n border-radius: var(--lb-radius, 12px);\n background: var(--lb-bg, #FFFFFF);\n /* Contain layout/paint so the skeleton doesn't influence ancestor\n layout once the real content swaps in. */\n contain: layout paint;\n}\n\n.lb-bundle-widget--loading .lb-skeleton {\n background: linear-gradient(\n 90deg,\n rgba(0, 0, 0, 0.06) 0%,\n rgba(0, 0, 0, 0.10) 50%,\n rgba(0, 0, 0, 0.06) 100%\n );\n background-size: 200% 100%;\n border-radius: 6px;\n animation: lb-skeleton-pulse 1.4s ease-in-out infinite;\n}\n\n.lb-bundle-widget--loading .lb-skeleton--title {\n height: 28px;\n width: 60%;\n margin-bottom: 16px;\n}\n\n.lb-bundle-widget--loading .lb-skeleton--products {\n display: grid;\n gap: 12px;\n margin-bottom: 16px;\n}\n\n.lb-bundle-widget--loading .lb-skeleton--row {\n display: grid;\n grid-template-columns: 56px 1fr 60px;\n gap: 12px;\n align-items: center;\n}\n\n.lb-bundle-widget--loading .lb-skeleton--thumb {\n height: 56px;\n width: 56px;\n border-radius: 8px;\n}\n\n.lb-bundle-widget--loading .lb-skeleton--line {\n height: 14px;\n border-radius: 4px;\n}\n\n.lb-bundle-widget--loading .lb-skeleton--line + .lb-skeleton--line {\n margin-top: 8px;\n width: 70%;\n}\n\n.lb-bundle-widget--loading .lb-skeleton--price {\n height: 20px;\n width: 60px;\n}\n\n.lb-bundle-widget--loading .lb-skeleton--footer {\n margin-top: 16px;\n padding-top: 16px;\n border-top: 1px solid var(--lb-border, #E5E5E5);\n display: grid;\n grid-template-columns: 1fr auto;\n gap: 12px;\n align-items: center;\n}\n\n.lb-bundle-widget--loading .lb-skeleton--total {\n height: 24px;\n width: 40%;\n}\n\n.lb-bundle-widget--loading .lb-skeleton--cta {\n height: 44px;\n width: 140px;\n border-radius: var(--lb-radius, 8px);\n}\n\n@keyframes lb-skeleton-pulse {\n 0% { background-position: 0% 50%; }\n 100% { background-position: -200% 50%; }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .lb-bundle-widget--loading .lb-skeleton {\n animation: none;\n }\n}\n`;\n\n","/**\n * <lime-bundle> Web Component — renders Lime Bundles on any storefront.\n *\n * ## The two modes\n *\n * Single-bundle (pinned):\n * <lime-bundle\n * shop-domain=\"my-shop.myshopify.com\"\n * storefront-token=\"shpat_...\"\n * bundle-gid=\"gid://shopify/Metaobject/42\"\n * ></lime-bundle>\n *\n * Product-aware (matches classic Liquid theme block behaviour — one snippet\n * on the product page renders every active bundle configured against that\n * product):\n * <lime-bundle\n * shop-domain=\"my-shop.myshopify.com\"\n * storefront-token=\"shpat_...\"\n * ></lime-bundle>\n *\n * Product resolution cascade (when no `bundle-gid` is set):\n * 1. explicit `product-handle` attribute\n * 2. <meta name=\"shopify:product-handle\" content=\"...\"> on the page\n * 3. /products/<handle> segment of window.location.pathname\n * 4. fallthrough: renders nothing, fires `lime-bundle:error`\n *\n * Multi-product renderers (volume) bind to the product this embed is\n * standing on: the explicit `product-id` attribute wins, then the handle\n * cascade above, then the bundle's first product when neither names one.\n *\n * ## Add-to-cart behaviour\n *\n * Merchants who do nothing get a default: the widget calls Shopify's\n * Storefront Cart API (tokenless — no extra scopes required) and redirects\n * the browser to the returned checkoutUrl. One-click-to-checkout is the\n * right UX for most merchants pasting the widget into Webflow / Wix /\n * Squarespace / static HTML.\n *\n * Merchants with their own cart state (Hydrogen's useCart, a custom cart\n * drawer, etc.) opt out by attaching a listener that calls\n * `event.preventDefault()`:\n *\n * document.querySelector(\"lime-bundle\").addEventListener(\n * \"lime-bundle:add-to-cart\",\n * (ev) => {\n * ev.preventDefault(); // suppress the default redirect\n * myCart.linesAdd(ev.detail.lines);\n * },\n * );\n *\n * The event is always dispatched; only the default action is conditional.\n */\nimport {\n createStorefrontClient,\n BUNDLE_METAOBJECT_QUERY,\n BUNDLES_FOR_PRODUCT_QUERY,\n CART_CREATE_MUTATION,\n CART_LINES_ADD_MUTATION,\n CART_LINES_QUERY,\n CART_LINES_REMOVE_MUTATION,\n SHOP_SETTINGS_QUERY,\n parseMetaobjectBundle,\n convertBundleToPresentment,\n observeImpression,\n reportImpression,\n reportAddToCart,\n injectCustomCss,\n sanitizeCustomCss,\n type ParsedBundle,\n type BundleMetaobjectResponse,\n type BundlesForProductResponse,\n type CartCreateResponse,\n type CartLinesAddResponse,\n type CartLinesQueryResponse,\n type CartLinesRemoveResponse,\n type CartMutationPayload,\n type ShopSettingsResponse,\n type CartLineInput,\n type StorefrontClient,\n type BuyerResolver,\n type BuyerInput,\n hasInContext,\n withInContext,\n isVisibleToBuyer,\n getVisitorId,\n resolveAbTests,\n} from \"@lime-bundles/core\";\nimport { BUNDLE_GID_ATTRIBUTE } from \"@lime-bundles/render/host\";\nimport { renderFixedBundle } from \"./renderers/fixed\";\nimport { renderMixMatchBundle } from \"./renderers/mix-match\";\nimport { renderVolumeBundle } from \"./renderers/volume\";\nimport { renderBogoBundle } from \"./renderers/bogo\";\nimport { renderMultiStepBundle } from \"./renderers/multi-step\";\nimport { applyWidgetConfigVars } from \"@lime-bundles/core\";\nimport { trackInputMode } from \"./utils/input-mode\";\nimport {\n BUNDLE_BASE_CSS,\n BUNDLE_DROPDOWN_CSS,\n BUNDLE_FIXED_CSS,\n BUNDLE_MIX_MATCH_CSS,\n BUNDLE_MULTI_STEP_CSS,\n BUNDLE_SKELETON_CSS,\n BUNDLE_VOLUME_CSS,\n BUNDLE_BOGO_CSS,\n} from \"./styles/bundle-css\";\n\n/**\n * Per-shop localStorage key for the active cart id. Scoping by shop domain\n * keeps the cart isolated when a single browser visits multiple Lime-\n * Bundles-powered storefronts (rare, but correct).\n */\nconst cartStorageKey = (shopDomain: string) => `lb_cart_id:${shopDomain}`;\n\n/**\n * Resolve the current product handle from the page. Runs the cascade\n * documented on LimeBundleElement and returns null if no source matches.\n */\nfunction resolveProductHandle(explicit: string | null): string | null {\n if (explicit) return explicit.trim() || null;\n\n if (typeof document !== \"undefined\") {\n const meta = document.querySelector<HTMLMetaElement>(\n 'meta[name=\"shopify:product-handle\"]',\n );\n if (meta?.content) return meta.content.trim() || null;\n }\n\n if (typeof window !== \"undefined\") {\n const match = window.location.pathname.match(/\\/products\\/([^/?#]+)/);\n if (match?.[1]) return decodeURIComponent(match[1]);\n }\n\n return null;\n}\n\nexport class LimeBundleElement extends HTMLElement {\n static observedAttributes = [\n \"shop-domain\",\n \"storefront-token\",\n \"bundle-gid\",\n \"product-handle\",\n \"product-id\",\n \"app-url\",\n \"analytics\",\n \"locale\",\n \"country\",\n \"language\",\n \"market-id\",\n \"currency-rate\",\n ];\n\n /**\n * B2B buyer identity. Set programmatically — `element.buyer = {...}` or\n * `element.buyer = () => fetchToken()`. NEVER expose as an HTML attribute\n * because the customer access token would land in DOM snapshots (Sentry,\n * analytics, browser extensions) and Referer headers. See the package\n * README (\"Markets & B2B\").\n */\n buyer: BuyerResolver | undefined = undefined;\n\n private shadow: ShadowRoot;\n private bundles: ParsedBundle[] = [];\n\n /**\n * The product handle this embed is standing on, when one can be resolved\n * (attribute → meta tag → /products/<handle> path). The mix-and-match\n * courtesy seed uses it to pre-add the current product the way the theme\n * widget does; null on pages with no product context.\n */\n private currentProductHandle: string | null = null;\n private abortController: AbortController | null = null;\n private impressionCleanups: Array<() => void> = [];\n /**\n * Tick / observer / listener cleanups registered by individual renderers\n * (e.g. countdown setInterval handles). Flushed in disconnectedCallback\n * so we never leak timers or event listeners when the widget is removed.\n */\n private renderCleanups: Array<() => void> = [];\n /**\n * Merchant custom CSS fetched from the shop metafield. Injected inside\n * the shadow root alongside the bundle stylesheets so selectors like\n * `.lb-bundle-widget { ... }` reach the widget's DOM.\n */\n private shopCustomCss: string | null = null;\n\n constructor() {\n super();\n this.shadow = this.attachShadow({ mode: \"open\" });\n }\n\n connectedCallback() {\n // `fetchBundle()` calls `renderLoading()` synchronously before any\n // await — the skeleton paints on the first frame, reserving\n // layout space before the Storefront API call resolves.\n this.fetchBundle();\n }\n\n disconnectedCallback() {\n this.abortController?.abort();\n this.teardownImpressions();\n this.teardownRenderers();\n }\n\n private teardownImpressions() {\n for (const cleanup of this.impressionCleanups) cleanup();\n this.impressionCleanups = [];\n }\n\n private teardownRenderers() {\n for (const cleanup of this.renderCleanups) cleanup();\n this.renderCleanups = [];\n }\n\n attributeChangedCallback(\n name: string,\n oldValue: string | null,\n newValue: string | null,\n ) {\n if (oldValue === newValue || !this.isConnected) return;\n if (\n name === \"bundle-gid\" ||\n name === \"product-handle\" ||\n name === \"product-id\" ||\n name === \"shop-domain\" ||\n name === \"storefront-token\" ||\n name === \"currency-rate\"\n ) {\n if (this.shopDomain && this.storefrontToken) {\n this.fetchBundle();\n }\n }\n }\n\n private get shopDomain(): string {\n return this.getAttribute(\"shop-domain\") ?? \"\";\n }\n\n private get storefrontToken(): string {\n return this.getAttribute(\"storefront-token\") ?? \"\";\n }\n\n private get bundleGid(): string {\n return this.getAttribute(\"bundle-gid\") ?? \"\";\n }\n\n private get productHandleAttr(): string {\n return this.getAttribute(\"product-handle\") ?? \"\";\n }\n\n /**\n * The current product's id — full GID (`gid://shopify/Product/123`) or\n * bare numeric id. Tells multi-product renderers (volume) which of the\n * bundle's products this embed is standing on; takes precedence over the\n * handle cascade. Without it (and with no resolvable handle) they bind to\n * the bundle's first product.\n */\n private get productIdAttr(): string {\n return this.getAttribute(\"product-id\") ?? \"\";\n }\n\n private get appUrl(): string {\n return this.getAttribute(\"app-url\") ?? \"\";\n }\n\n private get analyticsEnabled(): boolean {\n return this.getAttribute(\"analytics\") !== \"false\";\n }\n\n private get country(): string | undefined {\n return this.getAttribute(\"country\") ?? undefined;\n }\n\n private get language(): string | undefined {\n return this.getAttribute(\"language\") ?? undefined;\n }\n\n private get marketId(): string | undefined {\n return this.getAttribute(\"market-id\") ?? undefined;\n }\n\n /**\n * Store→presentment currency rate. Merchant-configured amounts\n * (fixed_amount / flat_price values, volume tier amounts) are stored in\n * the shop's store currency; set this to the buyer's currency rate so\n * quoted sale prices match what the checkout's discount function\n * charges. Anything unparseable or non-positive falls back to 1 —\n * no conversion — via `normalizeCurrencyRate`.\n */\n private get currencyRate(): string | undefined {\n return this.getAttribute(\"currency-rate\") ?? undefined;\n }\n\n /**\n * Returns the @inContext-wrapped query when any context field is set;\n * otherwise the plain query. Keeps responses publicly cacheable when\n * no buyer/country/language is set.\n */\n private wrapQueryForContext(query: string): string {\n return hasInContext({\n shopDomain: this.shopDomain,\n accessToken: this.storefrontToken,\n country: this.country,\n language: this.language,\n buyer: this.buyer,\n })\n ? withInContext(query)\n : query;\n }\n\n /** Buyer resolved once per fetch so the market gate can read the\n * company location without invoking a callback resolver twice. */\n private resolvedBuyer: BuyerInput | undefined = undefined;\n\n /**\n * Returns true if this bundle should be hidden for the current buyer.\n * For \"all\" bundles, always returns false (visible). For \"specific\"\n * bundles, visibility needs a matching signal: the `country` attribute\n * against the bundle's projected country codes (retail markets), the\n * resolved buyer's companyLocationId against its projected company\n * locations (B2B markets), or a legacy `market-id` match.\n */\n private isMarketHidden(bundle: ParsedBundle): boolean {\n return !isVisibleToBuyer(bundle, {\n marketId: this.marketId,\n countryCode: this.country,\n companyLocationId: this.resolvedBuyer?.companyLocationId,\n });\n }\n\n private async fetchBundle() {\n if (!this.shopDomain || !this.storefrontToken) {\n this.renderError(\n \"Missing required attributes: shop-domain, storefront-token\",\n );\n return;\n }\n\n this.abortController?.abort();\n const controller = new AbortController();\n this.abortController = controller;\n\n this.renderLoading();\n\n // Resolve the buyer once: the client needs it for @inContext and the\n // market gate needs its companyLocationId.\n try {\n this.resolvedBuyer =\n typeof this.buyer === \"function\" ? await this.buyer() : this.buyer;\n } catch {\n // A failing resolver shouldn't kill the widget — fall back to an\n // uncontextualized fetch, exactly as if no buyer was set.\n this.resolvedBuyer = undefined;\n }\n\n const client = createStorefrontClient({\n shopDomain: this.shopDomain,\n accessToken: this.storefrontToken,\n country: this.country,\n language: this.language,\n buyer: this.resolvedBuyer,\n });\n\n try {\n // Fire bundle query BEFORE CSS so order-sensitive call logs (and any\n // test harness that mocks fetch with sequential mockResolvedValueOnce)\n // see the bundle as call #1, CSS as call #2. Parallelism preserved\n // via Promise.all at the await site below.\n let bundlePromise: Promise<void>;\n let singleBundleMode = false;\n // Resolved in both modes: a pinned bundle-gid embed sitting on a\n // product page still gets the theme's courtesy-seed behaviour.\n this.currentProductHandle = resolveProductHandle(this.productHandleAttr);\n if (this.bundleGid) {\n singleBundleMode = true;\n bundlePromise = this.fetchSingleBundle(client, controller.signal);\n } else {\n const handle = this.currentProductHandle;\n if (!handle) {\n this.teardownImpressions();\n this.renderError(\n \"No bundle-gid or product-handle provided, and the current URL doesn't match /products/<handle>.\",\n );\n return;\n }\n bundlePromise = this.fetchProductBundles(\n client,\n controller.signal,\n handle,\n );\n }\n\n // Shop settings fetch kicks off AFTER bundle fetch for deterministic\n // call order. Best-effort — the widget renders with default styling and\n // the default out-of-stock behaviour if it fails. Both values are\n // awaited before `renderBundles()` below, which matters for the\n // out-of-stock behaviour specifically: a renderer can't retroactively\n // un-render a bundle, so the value has to be in hand before first paint.\n const shopSettingsPromise = client\n .query<ShopSettingsResponse>(SHOP_SETTINGS_QUERY, undefined, {\n signal: controller.signal,\n })\n .catch(() => null);\n\n await bundlePromise;\n if (controller.signal.aborted) return;\n\n // Single-bundle mode with an explicit GID that didn't resolve is a\n // genuine error — the merchant pinned a specific bundle and it's\n // missing. Product-handle mode with zero bundles is NOT an error:\n // the product legitimately has no bundles configured; widget stays\n // invisible (mirrors classic-theme Liquid block behaviour).\n if (singleBundleMode && this.bundles.length === 0) {\n this.teardownImpressions();\n this.renderError(\"Bundle not found\");\n return;\n }\n\n const shopSettings = await shopSettingsPromise;\n const customCss = shopSettings?.shop?.customCss?.value;\n if (customCss) {\n // Classic storefronts get the CSS in document.head; the web\n // component also stores the sanitized CSS for its own shadow\n // root injection below, since shadow DOM blocks inherited styles.\n injectCustomCss(this.shopDomain, customCss);\n const sanitized = sanitizeCustomCss(customCss);\n if (sanitized.ok) this.shopCustomCss = sanitized.css;\n }\n\n this.renderBundles();\n } catch (err) {\n if (controller.signal.aborted) return;\n this.bundles = [];\n this.teardownImpressions();\n this.renderError(\n err instanceof Error ? err.message : \"Failed to load bundle\",\n );\n }\n }\n\n private async fetchSingleBundle(\n client: StorefrontClient,\n signal: AbortSignal,\n ): Promise<void> {\n const data = await client.query<BundleMetaobjectResponse>(\n this.wrapQueryForContext(BUNDLE_METAOBJECT_QUERY),\n { id: this.bundleGid },\n { signal },\n );\n if (!data.metaobject) {\n this.bundles = [];\n return;\n }\n const parsed = parseMetaobjectBundle(\n data.metaobject.id,\n data.metaobject.fields,\n );\n this.bundles =\n parsed && !this.isMarketHidden(parsed)\n ? [convertBundleToPresentment(parsed, this.currencyRate)]\n : [];\n }\n\n private async fetchProductBundles(\n client: StorefrontClient,\n signal: AbortSignal,\n productHandle: string,\n ): Promise<void> {\n // fetchBundlesForProduct re-creates its own client; skip that indirection\n // and reuse the one we already built so the request shares the same\n // AbortSignal and header config.\n const data = await client.query<BundlesForProductResponse>(\n this.wrapQueryForContext(BUNDLES_FOR_PRODUCT_QUERY),\n { handle: productHandle },\n { signal },\n );\n if (!data.product) {\n this.bundles = [];\n return;\n }\n const refs = data.product.metafield?.references?.nodes ?? [];\n const bundles: ParsedBundle[] = [];\n for (const ref of refs) {\n const parsed = parseMetaobjectBundle(ref.id, ref.fields);\n if (parsed && !this.isMarketHidden(parsed)) {\n bundles.push(convertBundleToPresentment(parsed, this.currencyRate));\n }\n }\n // Two bundles in the same A/B test are alternatives, not a pair to stack.\n // Applied after the market filter so a side hidden in this market leaves\n // its sibling showing alone rather than eliminating the offer entirely.\n this.bundles = resolveAbTests(bundles, getVisitorId());\n }\n\n /**\n * Dispatch add-to-cart with a cancelable event, then — unless a listener\n * called preventDefault — execute the default cart-and-checkout flow.\n *\n * `fire-and-forget` against `reportAddToCart` runs regardless so merchants\n * with BYO cart still get analytics.\n */\n private handleAddToCart = async (\n bundle: ParsedBundle,\n lines: CartLineInput[],\n ): Promise<void> => {\n const ev = new CustomEvent(\"lime-bundle:add-to-cart\", {\n detail: { lines },\n bubbles: true,\n composed: true,\n cancelable: true,\n });\n // dispatchEvent returns false if preventDefault() was called on a\n // cancelable event. That's how merchants opt out of the default flow.\n const allowDefault = this.dispatchEvent(ev);\n\n // Analytics fire regardless of which cart path runs.\n this.reportAddToCartEvent(bundle, lines);\n\n if (allowDefault) {\n await this.defaultAddToCart(lines);\n }\n };\n\n /**\n * Default cart flow: Shopify's Storefront Cart API is tokenless, so we\n * don't need any additional scopes. Persist the cart ID in localStorage\n * so subsequent adds on the same browser session join the existing cart\n * instead of creating a new one every click.\n */\n private async defaultAddToCart(lines: CartLineInput[]): Promise<void> {\n if (typeof window === \"undefined\") return;\n\n const client = createStorefrontClient({\n shopDomain: this.shopDomain,\n accessToken: this.storefrontToken,\n });\n const storage = window.localStorage;\n const key = cartStorageKey(this.shopDomain);\n let cartId = storage?.getItem(key) ?? null;\n\n /** Errors whose only complaint is the cart id itself: the stored GID\n * expired or was merged on Shopify's side (~10 days of inactivity).\n * ONLY these fall through to cartCreate — any other error names a real\n * problem with the lines, and retrying the same lines into a fresh\n * cart used to silently drop them on the way to checkout. */\n const isStaleCartError = (\n errors: Array<{ field: string[] | null; message: string }>,\n ): boolean =>\n errors.length > 0 &&\n errors.every((e) => (e.field ?? []).includes(\"cartId\"));\n\n /** A capped line means the bundle is incomplete and its discount will\n * never apply — checkout at full price is worse than no checkout. */\n const stockCapped = (\n warnings: CartMutationPayload[\"warnings\"],\n ): boolean =>\n (warnings ?? []).some((w) => w.code === \"MERCHANDISE_NOT_ENOUGH_STOCK\");\n\n const fail = (message: string): void => {\n this.dispatchEvent(\n new CustomEvent(\"lime-bundle:error\", {\n detail: { message, code: \"CART_ERROR\" },\n bubbles: true,\n composed: true,\n }),\n );\n };\n\n try {\n let checkoutUrl: string | null = null;\n\n // The default flow sends every add straight to checkout, so a\n // lime-tagged line already sitting in the saved cart can only be an\n // attempt the shopper walked away from. Clear those before adding —\n // otherwise every abandoned attempt rides into the next checkout.\n // Returns null (after dropping the saved id) when the cart is gone\n // or couldn't be cleaned; the cartCreate path below takes over.\n if (cartId) {\n cartId = await this.clearAbandonedBundleLines(client, cartId, () =>\n storage?.removeItem(key),\n );\n }\n\n if (cartId) {\n const res = await client.query<CartLinesAddResponse>(\n CART_LINES_ADD_MUTATION,\n { cartId, lines },\n );\n const payload = res.cartLinesAdd;\n if (payload?.userErrors?.length) {\n if (!isStaleCartError(payload.userErrors)) {\n fail(payload.userErrors[0].message);\n return;\n }\n storage?.removeItem(key);\n } else if (stockCapped(payload?.warnings)) {\n fail(\"Some items in this bundle are out of stock.\");\n return;\n } else if (payload?.cart) {\n checkoutUrl = payload.cart.checkoutUrl;\n }\n }\n\n if (!checkoutUrl) {\n const res = await client.query<CartCreateResponse>(\n CART_CREATE_MUTATION,\n { input: { lines } },\n );\n const payload = res.cartCreate;\n // The retry's own result gets the same scrutiny the first attempt\n // did; a cart WITH userErrors is a partial cart, not a success.\n if (payload?.userErrors?.length) {\n fail(payload.userErrors[0].message);\n return;\n }\n if (stockCapped(payload?.warnings)) {\n fail(\"Some items in this bundle are out of stock.\");\n return;\n }\n if (payload?.cart) {\n storage?.setItem(key, payload.cart.id);\n checkoutUrl = payload.cart.checkoutUrl;\n }\n }\n\n if (checkoutUrl) {\n window.location.assign(checkoutUrl);\n } else {\n fail(\"Cart creation failed\");\n }\n } catch (err) {\n this.dispatchEvent(\n new CustomEvent(\"lime-bundle:error\", {\n detail: {\n message:\n err instanceof Error ? err.message : \"Cart mutation failed\",\n code: \"CART_ERROR\",\n },\n bubbles: true,\n composed: true,\n }),\n );\n }\n }\n\n /**\n * Remove this widget's own earlier lines — the ones tagged with the\n * `_lime_bundle_gid` attribute — from the saved cart, so a shopper who\n * clicked add three times while deciding checks out with only what they\n * just built. Lines without the tag were put in the cart by something\n * else (the host site, a previous non-widget purchase flow) and are\n * left alone.\n *\n * Returns the cart id when the cart is still usable. Returns null after\n * calling `dropSavedCart` when it isn't: either the id no longer\n * resolves (expired or merged on Shopify's side), or the removal itself\n * failed — in both cases a fresh cart is the only way to guarantee the\n * checkout matches the shopper's current build.\n */\n private async clearAbandonedBundleLines(\n client: StorefrontClient,\n cartId: string,\n dropSavedCart: () => void,\n ): Promise<string | null> {\n const res = await client.query<CartLinesQueryResponse>(CART_LINES_QUERY, {\n cartId,\n });\n if (!res.cart) {\n dropSavedCart();\n return null;\n }\n\n const abandonedLineIds = res.cart.lines.nodes\n .filter((line) =>\n line.attributes.some((attr) => attr.key === BUNDLE_GID_ATTRIBUTE),\n )\n .map((line) => line.id);\n if (abandonedLineIds.length === 0) return cartId;\n\n const removal = await client.query<CartLinesRemoveResponse>(\n CART_LINES_REMOVE_MUTATION,\n { cartId, lineIds: abandonedLineIds },\n );\n if (removal.cartLinesRemove?.userErrors?.length) {\n dropSavedCart();\n return null;\n }\n return cartId;\n }\n\n private reportAddToCartEvent(\n bundle: ParsedBundle,\n lines: CartLineInput[],\n ): void {\n if (!this.analyticsEnabled || !this.appUrl) return;\n\n const quantity = lines.reduce((sum, l) => sum + l.quantity, 0);\n const totalPrice = lines.reduce((sum, line) => {\n const product = bundle.products.find((p) =>\n p.variants.nodes.some((v) => v.id === line.merchandiseId),\n );\n const variant = product?.variants.nodes.find(\n (v) => v.id === line.merchandiseId,\n );\n const price = variant ? parseFloat(variant.price.amount) : 0;\n return sum + price * line.quantity;\n }, 0);\n\n reportAddToCart(\n { shopDomain: this.shopDomain, appUrl: this.appUrl },\n {\n bundleGid: bundle.id,\n bundleType: bundle.bundleType,\n productId: bundle.products[0]?.id ?? \"\",\n quantity,\n totalPrice: Math.round(totalPrice * 100) / 100,\n },\n );\n }\n\n private renderBundles() {\n this.teardownImpressions();\n this.teardownRenderers();\n this.shadow.innerHTML = \"\";\n\n // Bundle the base stylesheet and every type-specific sheet in one <style>.\n // This matches the Liquid theme app block's output (base.css +\n // bundle-{fixed,mix-match,volume}.css) so the shipped widget honours the\n // same selectors a merchant already styled against. Shadow DOM scope\n // keeps these rules from leaking out; the ported styles all target\n // `.lb-bundle-widget` descendants so there's no global bleed.\n const style = document.createElement(\"style\");\n style.textContent = [\n BUNDLE_BASE_CSS,\n BUNDLE_FIXED_CSS,\n BUNDLE_MIX_MATCH_CSS,\n BUNDLE_MULTI_STEP_CSS,\n BUNDLE_VOLUME_CSS,\n BUNDLE_BOGO_CSS,\n BUNDLE_DROPDOWN_CSS,\n ].join(\"\\n\");\n this.shadow.appendChild(style);\n\n // Merchant custom CSS — injected AFTER the built-in stylesheets so\n // the merchant's rules override defaults. Sanitized upstream via\n // sanitizeCustomCss (strips < > and script-safety patterns).\n if (this.shopCustomCss) {\n const customStyle = document.createElement(\"style\");\n customStyle.setAttribute(\"data-lime-bundles\", \"shop-custom-css\");\n customStyle.textContent = this.shopCustomCss;\n this.shadow.appendChild(customStyle);\n }\n\n // Empty state: the shadow root holds only the <style> tag — nothing\n // visible. Matches Liquid theme UX where a product with no bundles\n // simply shows no block. We still fire `lime-bundle:loaded` below so\n // consumers know the async work completed (important for test\n // synchronisation and for merchants who want to hide a parent\n // placeholder once the widget has decided whether to render).\n\n for (const bundle of this.bundles) {\n const container = document.createElement(\"div\");\n container.className = \"lb-bundle-widget\";\n container.setAttribute(\"role\", \"region\");\n container.setAttribute(\"aria-label\", bundle.title);\n container.setAttribute(\"data-bundle-type\", bundle.bundleType);\n container.setAttribute(\"data-bundle-gid\", bundle.id);\n // The countdown reads its end date off the wrapper, exactly where the\n // theme's Liquid writes it — without this the strip self-hides.\n if (bundle.endsAt) container.setAttribute(\"data-ends-at\", bundle.endsAt);\n\n // Emit every merchant-configurable --lb-* property so the ported CSS\n // renders the widget with the look the merchant set in the admin\n // editor.\n applyWidgetConfigVars(container, bundle.widgetConfig);\n\n // Toggle using-mouse / using-keyboard on this container so the CSS\n // focus-ring rules match the customer's current input device.\n this.renderCleanups.push(trackInputMode(container));\n\n const dispatch = (lines: CartLineInput[]) =>\n this.handleAddToCart(bundle, lines);\n const registerCleanup = (fn: () => void) =>\n this.renderCleanups.push(fn);\n\n switch (bundle.bundleType) {\n case \"fixed\":\n renderFixedBundle(container, bundle, dispatch, registerCleanup);\n break;\n case \"mix_match\":\n renderMixMatchBundle(\n container,\n bundle,\n dispatch,\n registerCleanup,\n this.currentProductHandle,\n );\n break;\n case \"volume\":\n // Volume has no teardown: it binds only to elements it owns, which\n // go with the shadow root on re-render.\n renderVolumeBundle(container, bundle, dispatch, {\n productId: this.productIdAttr || null,\n productHandle: this.currentProductHandle,\n });\n break;\n case \"bogo\":\n renderBogoBundle(container, bundle, dispatch, registerCleanup);\n break;\n case \"multi_step\":\n renderMultiStepBundle(container, bundle, dispatch, registerCleanup);\n break;\n }\n\n this.shadow.appendChild(container);\n this.setupImpressionFor(bundle, container);\n }\n\n const first = this.bundles[0];\n this.dispatchEvent(\n new CustomEvent(\"lime-bundle:loaded\", {\n detail: {\n bundleCount: this.bundles.length,\n bundleTypes: this.bundles.map((b) => b.bundleType),\n // Legacy fields — meaningful only in single-bundle mode. Preserved\n // for merchants who attached listeners against the pre-1.0 shape.\n bundleType: first?.bundleType,\n title: first?.title,\n },\n bubbles: true,\n composed: true,\n }),\n );\n }\n\n private setupImpressionFor(bundle: ParsedBundle, element: Element) {\n if (!this.analyticsEnabled || !this.appUrl) return;\n const cleanup = observeImpression(element, () => {\n reportImpression(\n { shopDomain: this.shopDomain, appUrl: this.appUrl },\n {\n bundleGid: bundle.id,\n bundleType: bundle.bundleType,\n },\n );\n });\n this.impressionCleanups.push(cleanup);\n }\n\n private renderLoading() {\n // Synchronous paint on every mount (before the Storefront fetch\n // resolves) so the host element has intrinsic size from render-0\n // and doesn't shift layout when the real bundle swaps in.\n this.shadow.innerHTML = `\n <style>${BUNDLE_BASE_CSS}</style>\n <style>${BUNDLE_SKELETON_CSS}</style>\n <div class=\"lb-bundle-widget lb-bundle-widget--loading\" aria-busy=\"true\" aria-label=\"Loading bundle\">\n <div class=\"lb-skeleton lb-skeleton--title\"></div>\n <div class=\"lb-skeleton--products\">\n <div class=\"lb-skeleton--row\">\n <div class=\"lb-skeleton lb-skeleton--thumb\"></div>\n <div>\n <div class=\"lb-skeleton lb-skeleton--line\"></div>\n <div class=\"lb-skeleton lb-skeleton--line\"></div>\n </div>\n <div class=\"lb-skeleton lb-skeleton--price\"></div>\n </div>\n <div class=\"lb-skeleton--row\">\n <div class=\"lb-skeleton lb-skeleton--thumb\"></div>\n <div>\n <div class=\"lb-skeleton lb-skeleton--line\"></div>\n <div class=\"lb-skeleton lb-skeleton--line\"></div>\n </div>\n <div class=\"lb-skeleton lb-skeleton--price\"></div>\n </div>\n <div class=\"lb-skeleton--row\">\n <div class=\"lb-skeleton lb-skeleton--thumb\"></div>\n <div>\n <div class=\"lb-skeleton lb-skeleton--line\"></div>\n <div class=\"lb-skeleton lb-skeleton--line\"></div>\n </div>\n <div class=\"lb-skeleton lb-skeleton--price\"></div>\n </div>\n </div>\n <div class=\"lb-skeleton--footer\">\n <div class=\"lb-skeleton lb-skeleton--total\"></div>\n <div class=\"lb-skeleton lb-skeleton--cta\"></div>\n </div>\n </div>\n `;\n }\n\n private renderError(message: string) {\n this.shadow.innerHTML = \"\";\n this.dispatchEvent(\n new CustomEvent(\"lime-bundle:error\", {\n detail: { message, code: \"LOAD_ERROR\" },\n bubbles: true,\n composed: true,\n }),\n );\n }\n\n}\n\n// Re-export the helper so consumers of the widget package can import it\n// when they want to share the URL-detection logic with their own code.\n"],"mappings":"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S,GAAGS,GAAgB,CAC1C,CACF,CCpEO,IAAMG,GAAkB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAusBlBC,GAAmB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA8FnBC,GAAuB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAwiCvBC,GAAoB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAyLpBC,GAAkB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA2ElBC,GAAwB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAgWxBC,GAAsB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;A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","names":["src_exports","__export","LimeBundleElement","trackInputMode","StorefrontApiError","errors","e","DEFAULT_API_VERSION","resolveBuyer","buyer","hasInContext","config","createStorefrontClient","version","endpoint","query","variables","options","headers","mergedVariables","response","json","withInContext","_match","name","args","trimmed","extra","combined","BUNDLE_METAOBJECT_QUERY","SHOP_SETTINGS_QUERY","BUNDLES_FOR_PRODUCT_QUERY","CART_CREATE_MUTATION","CART_LINES_ADD_MUTATION","CART_LINES_QUERY","CART_LINES_REMOVE_MUTATION","BundleParseError","message","reason","RADIUS_PRESET_PX","WIDGET_CONFIG_DEFAULTS","mergeWidgetConfig","raw","input","sanitizeDefaultTier","sanitizeRadiusPreset","flattenWidgetConfig","config","CSS_VAR_MAP","PX_KEYS","applyWidgetConfigVars","el","flat","flatKey","cssVar","value","serialized","variantChevronUrl","ratioVars","thumbnailRatioVars","pickerRatioVars","strokeHex","ratio","VALID_BUNDLE_TYPES","ACTIVE_STATUSES","parseMetaobjectBundle","metaobjectId","fields","parseMetaobjectBundleStrict","err","fieldMap","f","title","rawBundleType","rawStatus","bundleType","status","startsAt","endsAt","now","start","end","products","resolveProducts","discountConfig","parseDiscountConfig","widgetConfig","parseWidgetConfig","base","parseSelectedVariantIds","parseNumericRecord","parseMarketVisibility","parseMarketIds","parseStringArrayField","parseAbWeight","parseVolumeTiers","parseIntField","parseRuleMap","parseBogoConfig","parseMultiSteps","parseCollectionProductIds","parseJsonField","stringList","v","x","steps","entry","s","minRaw","minQuantity","maxQuantity","out","collectionField","node","p","cfg","buyProductId","getProductId","clampQty","n","sideVariants","id","productsField","result","key","record","k","num","pid","rule","min","max","required","minN","maxN","minClamped","maxClamped","parsed","t","parseOneVolumeTier","minQty","tier","options","normalizeCurrencyRate","rate","convertBundleToPresentment","bundle","r","converted","bestValueTierIndex","tiers","discountType","best","bestIdx","i","savings","resolveDefaultTierIndex","defaultTier","tierCount","bestIndex","resolvePopularTierIndex","tierIndex","visible","WILDCARD","idKey","id","listMatches","list","candidate","compareByIdSuffix","key","isVisibleToBuyer","bundle","context","productIdMatches","gid","ref","resolveContextProduct","products","context","id","byId","p","handle","byHandle","resolveBundleQty","bundle","productId","variantId","vq","productsForStep","bundle","stepIndex","step","pinned","fromCollections","collectionId","productId","seen","result","product","reportImpression","config","event","sendEvent","reportAddToCart","observeImpression","element","callback","observer","entries","entry","payload","url","body","r","MAX_CSS_LENGTH","BLOCKED_PATTERNS","SAFE_URL_VALUE","sanitizeCustomCss","raw","css","_","hex","codePoint","pattern","label","urlPattern","urlMatch","urlValue","STYLE_ID_PREFIX","injectCustomCss","shopDomain","rawCss","sanitized","id","simpleHash","style","input","hash","i","formatMoney","amount","currencyCode","num","calculateDiscount","price","discountType","discountValue","UNIT_LABEL","formatUnitPrice","unitPrice","measurement","fallbackCurrency","currency","formatted","value","measurementText","dropdown_exports","__export","computePosition","emptyTypeAheadState","firstEnabled","handleKey","pushTypeAheadChar","DEFAULT_TRIGGER_MARGIN","trigger","viewportHeight","desiredHeight","margin","clip","upperBound","lowerBound","spaceBelow","spaceAbove","placement","maxHeight","offsetTop","PASSTHROUGH","options","i","lastEnabled","nextEnabled","from","step","idx","prevEnabled","isPrintable","key","event","state","isOpen","activeIndex","selectedIndex","TYPE_AHEAD_RESET_MS","char","now","resetMs","buffer","newState","opt","picker_exports","findVariantByOptions","isOptionValueAvailable","toPickerVariant","isUntrackedStock","variant","isFulfillable","requiredQty","stockCapForVariant","findVariantByOptions","variants","optionValues","v","match","j","isOptionValueAvailable","optionIndex","value","selected","ok","toPickerVariant","variant","requiredQty","o","isFulfillable","VISITOR_ID_KEY","inMemoryVisitorId","hashString","input","hash","i","getVisitorId","stored","fresh","randomId","pickAbSide","members","visitorId","testId","ordered","b","roll","acc","member","resolveAbTests","bundles","groups","bundle","existing","chosen","intlFormatMoney","currencyCode","cents","BUNDLE_GID_ATTRIBUTE","BUNDLE_TYPE_ATTRIBUTE","BUNDLE_ROLE_ATTRIBUTE","BUNDLE_ROLE_GET","bundleLineAttributes","bundleGid","bundleType","numericId","gid","tail","toCents","amount","n","imageAspectRatio","image","w","h","adaptVariant","v","requiredQty","fallbackCurrency","o","isFulfillable","formatUnitPrice","stockCapForVariant","adaptProduct","product","bundleId","opts","allowed","nodes","currency","productQty","resolveBundleQty","variants","qty","adapted","initial","adaptProducts","bundle","idx","findVariant","product","variantId","i","resolveSellableVariant","seeded","findVariantByOptions","optionValues","v","match","j","isOptionValueAvailable","variants","optionIndex","value","selected","ok","buildCartItems","products","items","i","variant","findVariant","qty","computeBundlePricing","products","discountType","discountValue","totalPrice","salePrice","variant","findVariant","qty","linePrice","unitDiscount","flatCents","recalcPricing","container","formatMoney","fixed","savings","saleEl","compareEl","savingsBar","savingsAmountEl","savingsPercentEl","pct","bindVariantSelects","row","getProduct","onSelected","optionSelects","positionOf","sel","collectOptionValues","values","i","pos","syncSelectsToVariant","variant","expected","recomputeDisabledState","product","selected","opts","o","isOptionValueAvailable","seeded","initialVariant","findVariant","e","findVariantByOptions","prev","hydrateRowControls","row","product","selected","badge","groups","buildOptionGroups","container","optionNames","i","group","label","select","seen","v","value","opt","productImgSrc","updateThumbnail","row","variant","productImage","thumb","img","seed","nextSrc","newImg","updateInlineQuantity","qty","el","applyVariant","product","formatMoney","priceEl","compareEl","compare","unitEl","box","tag","className","attrs","el","k","buildRowSkeleton","product","opts","eligible","row","thumb","ratio","info","title","oos","prices","qty","badge","buildWidgetShell","typeClass","root","header","heading","sub","countdown","label","products","summary","text","savings","cta","ctaLabel","buildVolumeTiers","tiers","groupLabel","showCompare","showPriceEach","group","tier","i","radio","price","unit","right","buildProgressBar","segmentCount","wrap","segments","labels","pad","n","initCountdown","container","countdown","widget","endsAt","endTime","timerEl","interval","update","remaining","days","hours","mins","secs","DEFAULT_STRINGS","formatItemsOutOfStock","t","count","raw","tpl","form","findScrollableAncestor","el","doc","win","cur","overflowY","openInstances","documentListenersAttached","eventInsideDropdown","event","inst","path","p","target","closeAllOutside","i","onWindowResize","attachDocListeners","detachDocListeners","registerOpen","instance","registerClosed","idx","closeOthers","except","ITEM_HEIGHT","LIST_PADDING_Y","MAX_VISIBLE_ITEMS","instances","instanceFor","select","bindDropdown","selectEl","doc","labelText","idBase","shell","trigger","listboxId","triggerLabel","chevron","listbox","modalOverlay","isOpen","activeIndex","typeAhead","dropdown_exports","optionEls","readOptionsState","sel","i","o","syncFromSelect","idx","li","updateActive","newIndex","liTop","liBottom","visTop","visBottom","position","rect","desiredHeight","scrollable","findScrollableAncestor","clipRect","pos","open","closeOthers","instance","selIdx","registerOpen","close","restoreFocus","registerClosed","commit","index","opt","onKeydown","event","optsState","action","r","onTriggerClick","optionIndexFrom","target","onListboxClick","onListboxMousemove","onShellFocusout","onSelectChange","observer","onListboxMousedown","destroy","BIND_SELECTOR","bindAll","root","selects","BIND_SELECTOR","i","bindDropdown","unbindAll","bound","instanceFor","bindDropdowns","root","bindAll","unbindAll","renderFixedBundle","container","bundle","onAddToCart","onCleanup","wc","products","adaptProducts","oosProducts","p","v","currency","formatMoney","intlFormatMoney","t","DEFAULT_STRINGS","shell","buildWidgetShell","product","isOos","row","buildRowSkeleton","sellable","resolveSellableVariant","selected","findVariant","hydrateRowControls","applyVariant","bindVariantSelects","variant","recalcPricing","label","formatItemsOutOfStock","buildCartItems","item","bundleLineAttributes","initCountdown","bindDropdowns","fill","template","values","out","key","formatNeedsSpots","min","map","form","formatOfSelected","count","total","t","fill","formatMoreToGo","remaining","formatSubtitle","discountType","discountValue","pct","DEFAULT_RULE","ruleFor","productId","rulesMap","variantId","vRule","rule","variantRuleFor","mergeRuleMaps","productRules","variantRules","products","keys","merged","key","defaultPickQuantity","totalUnits","items","qty","i","indexOfVariantInBundle","committedQtyForVariant","idx","isRowAddable","remainingSpots","stepperCeiling","stock","ownQty","productOtherUnits","variantMax","ceiling","THUMB_WIDTHS","PICKER_WIDTHS","THUMB_SIZES","PICKER_SIZES","imgWidth","url","w","srcset","widths","debounce","fn","delay","timer","args","normalizeText","str","PICKER_BASE_WIDTH","buildPickerRow","p","rowIndex","ctx","t","rulesMap","capacity","formatMoney","pRule","ruleFor","row","normalizeText","rowDisabled","stepperDisabled","thumbDiv","addedBadge","firstAvailable","v","initialThumbSrc","img","imgWidth","srcset","PICKER_WIDTHS","PICKER_SIZES","infoDiv","titleEl","titleLink","currentVariant","ruleForVariant","modalQty","priceEl","priceSaleEl","priceCompareEl","paintRowCompare","el","variant","qty","cmp","price","unitPriceEl","initialUnit","qtyGroup","qtyValueEl","current","effectiveMax","paint","setBoundsForVariant","stepperWrap","qtyMinusBtn","qtyPlusBtn","valueEl","alreadyInBundle","variantId","ownQty","otherUnits","swapUnits","stock","othersInBundle","stockCap","vMax","variantRuleFor","rMin","stepperCeiling","commitQtyToBundle","availVariants","optionSelects","optionNames","findMatchingVariant","values","s","o","j","syncSelectsToVariant","i","isValueAvailable","optionIndex","value","selected","ok","recomputeDisabled","sel","onOptionChange","resolvedQty","rowImg","imgSrc","refresh","groupsContainer","on","optName","groupEl","labelEl","seenValues","currentVal","av","val","opt","varLabel","needsSpotsEl","addBtn","actionsDiv","soldOutLabel","lastVariantId","already","inBundle","committed","rowSwapUnits","vRule","inSwapState","remaining","needsMoreSpots","isRowAddable","formatNeedsSpots","swapLabel","label","noStock","productMaxed","FOCUSABLE","getFocusable","container","nodes","result","i","savedY","lockCount","lock","body","unlock","sinkAddedRows","list","rowEls","isAdded","front","back","el","ALL_TYPES","CLOSE_TRANSITION_MS","SEARCH_DEBOUNCE_MS","createPickerModal","deps","container","data","t","eligibleProducts","items","overlay","q","sel","modalDialog","searchInput","searchClear","modalList","modalEmpty","modalLive","closeBtn","doneBtn","footerCountEl","subtitleEl","filtersContainer","showTypeFilters","activeType","productListBuilt","filtersBuilt","modalOpen","rows","refreshAllStepperBounds","r","updateModalMeta","count","totalUnits","remaining","formatOfSelected","formatSubtitle","refresh","buildProductList","i","row","buildPickerRow","onListClick","e","addBtn","prodIndex","prod","selectedId","selectedVariant","rowQtyEl","pickedQty","defaultPickQuantity","newItem","swapUnits","buildFilters","types","seen","p","ty","value","pill","syncActiveFilterPill","filterProducts","pills","on","query","normalizedQuery","normalizeText","listItems","visibleCount","title","type","matches","fill","handleSearch","debounce","open","selectedProductIds","it","sinkAddedRows","lock","close","unlock","elToFocus","mouseDownTarget","focusable","getFocusable","first","last","remainingUnits","items","requiredQty","totalUnits","atCapacity","alreadyInBundleForVariant","variantId","qty","i","productUnitsInOtherSlots","productId","excludeVariantId","it","canRemovePick","item","rulesMap","variantRuleFor","rule","ruleFor","unitsElsewhere","ci","cs","swapUnitsFor","indexOfVariantInBundle","removeIcon","NS","svg","x1","y1","x2","y2","line","buildSlotCard","item","trans","formatMoney","removable","onRemove","onEdit","card","thumb","img","imgWidth","srcset","THUMB_WIDTHS","THUMB_SIZES","info","title","variant","priceRow","unitCompare","strikeEl","saleEl","qtyInline","unitPriceEl","removeBtn","e","requiredChip","updateSlotUI","cont","items","reqQty","d","deps","trans","slotsWrap","i","index","buildSlotCard","canRemovePick","trigger","totalUnits","ctaBtn","ctaLabel","updatePricing","calcDiscount","paint","pricingSection","totalPrice","updateProgress","overlay","count","remaining","roots","root","segments","s","progressCount","formatOfSelected","progressRemaining","formatMoreToGo","segWrap","updatePricing","formatMoney","container","salePrice","comparePrice","saleEl","compareEl","savingsBar","savingsAmountEl","savingsPercentEl","savings","pct","seedSelection","eligibleProducts","rulesMap","requiredQty","courtesy","firstAvailable","p","v","pick","quantity","required","seededVariant","vRule","variantRuleFor","rule","ruleFor","qualifies","seed","icon","paths","NS","svg","x1","y1","x2","y2","line","el","tag","className","attrs","node","k","buildPickerModal","opts","titleId","overlay","dialog","header","top","heading","title","close","progress","segments","i","search","input","clear","empty","emptyText","footer","back","next","done","renderMixMatchBundle","container","bundle","onAddToCart","onCleanup","currentProductHandle","wc","t","DEFAULT_STRINGS","requiredQty","rulesMap","mergeRuleMaps","eligibleProducts","adaptProducts","p","i","v","formatMoney","intlFormatMoney","shell","buildWidgetShell","buildProgressBar","slots","addTrigger","overlay","buildPickerModal","currentIdx","selectedItems","seedSelection","rowCapacity","remainingUnits","committedQtyForVariant","alreadyInBundleForVariant","productUnitsInOtherSlots","swapUnitsFor","indexOfVariantInBundle","canRemovePick","atCapacity","qty","updateAll","updateSlotUI","modal","updatePricing","total","type","value","calculateDiscount","updateProgress","createPickerModal","item","totalUnits","vMax","variantRuleFor","headroom","ruleFor","freed","it","take","bundleLineAttributes","initCountdown","bindDropdowns","TIER_OOS_CLASS","tierQty","el","tierUnavailable","applyTierStockState","tierEls","cap","i","resolveAvailableTierIndex","preferred","p","stepToAvailableTier","from","direction","calcTierPrice","basePrice","discountType","tierEl","amt","pct","computeTierTotals","qty","priceEach","totalPrice","undiscountedTotal","formatItemCount","translations","form","updateAllTierPrices","allTiers","formatMoney","i","priceEachEl","compareEl","selectTier","container","index","totalSavings","itemCountEl","label","totalPriceEl","savingsBar","savingsAmountEl","savingsPercentEl","savingsPct","renderVolumeBundle","container","bundle","onAddToCart","productContext","wc","product","resolveContextProduct","variants","minTierQty","tier","pricingVariant","v","isFulfillable","basePrice","toCents","discountType","formatMoney","intlFormatMoney","t","DEFAULT_STRINGS","bestIdx","bestValueTierIndex","defaultIdx","resolveDefaultTierIndex","popularIdx","resolvePopularTierIndex","tiers","i","percent","amountCents","priceEach","savedPct","fill","shell","buildWidgetShell","group","buildVolumeTiers","tierEls","updateAllTierPrices","applyTierStockState","stockCapForVariant","selectedIndex","resolveAvailableTierIndex","noTierAvailable","apply","selectTier","e","el","tierUnavailable","stepToAvailableTier","label","qty","bundleLineAttributes","initCountdown","discountedUnit","priceCents","percent","computeBogoTotals","sides","buyQty","getQty","buyVariant","findVariant","getVariant","totalPrice","salePrice","repriceRow","row","variant","isGet","formatMoney","priceEl","compareEl","compare","recalcPricing","container","savings","saleEl","savingsBar","savingsAmountEl","savingsPercentEl","pct","buildBogoCartItems","sides","percent","buyQty","getQty","buyVariant","findVariant","getVariant","buyLine","getPriceCents","discountedUnit","BUNDLE_ROLE_ATTRIBUTE","BUNDLE_ROLE_GET","buildBogoPlus","plus","renderBogoBundle","container","bundle","onAddToCart","onCleanup","wc","buyProduct","p","getProduct","buyQty","getQty","percent","quantities","buyAllowed","getAllowed","sides","adaptProduct","oosCount","s","v","formatMoney","intlFormatMoney","t","DEFAULT_STRINGS","shell","buildWidgetShell","badgeText","side","i","isGet","isOos","row","buildRowSkeleton","initial","findVariant","hydrateRowControls","updateThumbnail","repriceRow","bindVariantSelects","variant","recalcPricing","label","formatItemsOutOfStock","buildBogoCartItems","item","bundleLineAttributes","initCountdown","bindDropdowns","stepOfLabel","i","total","t","fill","ofSelectedLabel","count","stepsCompletedLabel","completed","moreToGoLabel","remaining","fillPlural","map","locale","form","doorLabel","anyPicks","doorSubLabel","quick","unitsInStep","selections","i","qty","j","stepSatisfied","steps","stepHeadroom","max","allSatisfied","canRemovePick","selections","item","rulesMap","variantRuleFor","rule","ruleFor","unitsElsewhere","step","pick","swapUnitsForStep","i","productId","variantId","indexOfVariantInStep","qty","it","j","updateStepGroups","deps","container","steps","selections","t","min","section","countLabel","ofSelectedLabel","unitsInStep","met","stepSatisfied","countEls","c","slotsWrap","j","stepIdx","slotIdx","item","buildSlotCard","canRemovePick","updateDoor","door","firstUnsatisfied","remaining","anyPicks","i","parent","label","doorLabel","labelEl","subEl","doorSubLabel","stepName","target","updatePricing","data","calcDiscount","paint","pricingSection","allSatisfied","totalPrice","step","pick","updateProgress","completed","segments","s","progressCount","stepsCompletedLabel","progressRemaining","moreToGoLabel","segWrap","updateCta","ctaBtn","ctaLabel","mergeCartLines","selections","byVariant","order","step","item","qty","key","ALL_TYPES","CLOSE_TRANSITION_MS","SEARCH_DEBOUNCE_MS","STEP_CASCADE_MS","AUTO_ADVANCE_HOLD_MS","createWizard","deps","container","steps","selections","t","overlay","q","sel","modalDialog","modalTitle","searchInput","searchClear","modalList","modalEmpty","modalLive","closeBtn","backBtn","nextBtn","doneBtn","footerCountEl","subtitleEl","segmentsEl","filtersContainer","activeType","modalOpen","stepIndex","rows","cascadeTimer","autoAdvanceTimer","productsFor","i","rowCapacity","stepHeadroom","variantId","idx","indexOfVariantInStep","qty","step","pick","productId","it","swapUnitsForStep","buildStepSegments","min","s","seg","paintStepSegments","count","unitsInStep","segs","updateModalMeta","ofSelectedLabel","stepOfLabel","satisfied","stepSatisfied","isLast","refresh","r","maybeAutoAdvance","from","goToStep","buildProductList","forStep","pool","row","buildPickerRow","stepPicks","selectedProductIds","sinkAddedRows","onListClick","e","addBtn","prod","selectedId","selectedVariant","v","existing","rowQtyEl","pickedQty","defaultPickQuantity","newItem","swapUnits","room","buildFilters","types","seen","p","ty","value","pill","syncActiveFilterPill","filterProducts","pills","on","query","normalizedQuery","normalizeText","listItems","visibleCount","title","type","matches","fill","handleSearch","debounce","opts","target","dialog","open","atStep","lock","close","unlock","visible","el","elToFocus","mouseDownTarget","focusable","getFocusable","first","last","buildPlusIcon","NS","svg","x1","y1","x2","y2","line","renderMultiStepBundle","container","bundle","onAddToCart","onCleanup","wc","t","DEFAULT_STRINGS","rulesMap","mergeRuleMaps","steps","quantities","pools","_","i","productsForStep","product","p","adaptProduct","v","pool","formatMoney","intlFormatMoney","shell","buildWidgetShell","buildProgressBar","groups","door","doorIcon","doorCopy","doorLabel","doorSub","step","group","buildCount","count","row","ix","rowName","heading","name","slots","overlay","buildPickerModal","selections","bodyDeps","s","index","item","canRemovePick","updateAll","wizard","updateStepGroups","updateDoor","updatePricing","total","type","value","calculateDiscount","updateProgress","updateCta","createWizard","stepIdx","qty","freed","j","it","take","el","allSatisfied","mergeCartLines","bundleLineAttributes","initCountdown","bindDropdowns","NAV_KEYS","targets","listenersAttached","setAll","on","off","el","onKeyDown","onPointerDown","attachListeners","detachListeners","trackInputMode","target","BUNDLE_BASE_CSS","BUNDLE_FIXED_CSS","BUNDLE_MIX_MATCH_CSS","BUNDLE_VOLUME_CSS","BUNDLE_BOGO_CSS","BUNDLE_MULTI_STEP_CSS","BUNDLE_DROPDOWN_CSS","BUNDLE_SKELETON_CSS","cartStorageKey","shopDomain","resolveProductHandle","explicit","meta","match","LimeBundleElement","cleanup","name","oldValue","newValue","query","hasInContext","withInContext","bundle","isVisibleToBuyer","controller","client","createStorefrontClient","bundlePromise","singleBundleMode","handle","shopSettingsPromise","SHOP_SETTINGS_QUERY","customCss","injectCustomCss","sanitized","sanitizeCustomCss","err","signal","data","BUNDLE_METAOBJECT_QUERY","parsed","parseMetaobjectBundle","convertBundleToPresentment","productHandle","BUNDLES_FOR_PRODUCT_QUERY","refs","bundles","ref","resolveAbTests","getVisitorId","lines","ev","allowDefault","storage","key","cartId","isStaleCartError","errors","e","stockCapped","warnings","w","fail","message","checkoutUrl","payload","CART_LINES_ADD_MUTATION","CART_CREATE_MUTATION","dropSavedCart","res","CART_LINES_QUERY","abandonedLineIds","line","attr","BUNDLE_GID_ATTRIBUTE","CART_LINES_REMOVE_MUTATION","quantity","sum","l","totalPrice","variant","p","v","price","reportAddToCart","style","BUNDLE_BASE_CSS","BUNDLE_FIXED_CSS","BUNDLE_MIX_MATCH_CSS","BUNDLE_MULTI_STEP_CSS","BUNDLE_VOLUME_CSS","BUNDLE_BOGO_CSS","BUNDLE_DROPDOWN_CSS","customStyle","container","applyWidgetConfigVars","trackInputMode","dispatch","registerCleanup","fn","renderFixedBundle","renderMixMatchBundle","renderVolumeBundle","renderBogoBundle","renderMultiStepBundle","first","b","element","observeImpression","reportImpression","BUNDLE_SKELETON_CSS","LimeBundleElement"]}
1
+ {"version":3,"sources":["../src/index.ts","../../core/src/storefront-api/client.ts","../../core/src/storefront-api/queries.ts","../../core/src/storefront-api/cache-key.ts","../../core/src/bundle/types.ts","../../core/src/bundle/widget-config.ts","../../core/src/bundle/parser.ts","../../core/src/bundle/presentment.ts","../../core/src/bundle/fetch-by-product.ts","../../core/src/bundle/tier-calculator.ts","../../core/src/bundle/market.ts","../../core/src/bundle/context-product.ts","../../core/src/bundle/validator.ts","../../core/src/bundle/qty.ts","../../core/src/bundle/multi-step.ts","../../core/src/analytics/reporter.ts","../../core/src/styles/sanitize.ts","../../core/src/styles/custom-css-injector.ts","../../core/src/utils/money.ts","../../core/src/utils/pricing.ts","../../core/src/utils/image.ts","../../core/src/utils/countdown.ts","../../core/src/dropdown/index.ts","../../core/src/dropdown/compute-position.ts","../../core/src/dropdown/keyboard.ts","../../core/src/dropdown/type-ahead.ts","../../core/src/variants/picker.ts","../../core/src/inventory/predicate.ts","../../core/src/ab-test/assign.ts","../../render/src/host.ts","../../render/src/adapt.ts","../../render/src/product/variants.ts","../../render/src/fixed/cart.ts","../../render/src/fixed/pricing.ts","../../render/src/product/option-selects.ts","../../render/src/product/hydrate.ts","../../render/src/product/row.ts","../../render/src/skeleton.ts","../../render/src/countdown.ts","../../render/src/strings.ts","../../render/src/dropdown/scroll.ts","../../render/src/dropdown/open-registry.ts","../../render/src/dropdown/bind.ts","../../render/src/dropdown/auto-bind.ts","../src/renderers/dropdown-host.ts","../src/renderers/fixed.ts","../../render/src/picker/i18n.ts","../../render/src/mix-match/i18n.ts","../../render/src/picker/rules.ts","../../render/src/picker/images.ts","../../render/src/utils.ts","../../render/src/picker/row.ts","../../render/src/picker/focus-trap.ts","../../render/src/picker/scroll-lock.ts","../../render/src/picker/sort.ts","../../render/src/mix-match/modal.ts","../../render/src/mix-match/rules.ts","../../render/src/picker/slot-card.ts","../../render/src/mix-match/slots.ts","../src/renderers/pricing-paint.ts","../../render/src/mix-match/seed.ts","../../render/src/picker-modal.ts","../src/renderers/mix-match.ts","../../render/src/volume/stock.ts","../../render/src/volume/pricing.ts","../src/renderers/volume.ts","../../render/src/bogo/pricing.ts","../../render/src/bogo/cart.ts","../src/renderers/bogo.ts","../../render/src/multi-step/i18n.ts","../../render/src/multi-step/rules.ts","../../render/src/multi-step/body.ts","../../render/src/multi-step/cart.ts","../../render/src/multi-step/wizard.ts","../src/renderers/multi-step.ts","../src/utils/input-mode.ts","../src/styles/bundle-css.ts","../src/lime-bundle.ts"],"sourcesContent":["/**\n * @lime-bundles/widget — Vanilla JS Web Component for headless storefronts.\n *\n * Usage:\n * <script src=\"https://unpkg.com/@lime-bundles/widget/dist/lime-bundle.js\"></script>\n * <lime-bundle shop-domain=\"...\" storefront-token=\"...\" bundle-gid=\"...\"></lime-bundle>\n */\nimport { LimeBundleElement } from \"./lime-bundle\";\n\n// Register custom element\nif (\n typeof customElements !== \"undefined\" &&\n !customElements.get(\"lime-bundle\")\n) {\n customElements.define(\"lime-bundle\", LimeBundleElement);\n}\n\nexport { LimeBundleElement };\nexport { trackInputMode } from \"./utils/input-mode\";\n","/**\n * Lightweight Storefront API client.\n * No framework dependencies — just fetch.\n *\n * SSR and Trusted Publishing environments (Hydrogen, Next.js server\n * components) MUST forward the end-buyer's IP via the `buyerIp` config\n * field. Without it Shopify may return `430 Shopify Security Rejection`\n * for server-originated traffic. Pass the IP in IPv4 or IPv6 string form.\n */\n\nexport class StorefrontApiError extends Error {\n constructor(\n public readonly errors: Array<{ message: string; locations?: unknown[] }>,\n ) {\n super(errors.map((e) => e.message).join(\"; \"));\n this.name = \"StorefrontApiError\";\n }\n}\n\nexport interface StorefrontClient {\n query<T = unknown>(\n query: string,\n variables?: Record<string, unknown>,\n options?: QueryOptions,\n ): Promise<T>;\n}\n\nexport interface QueryOptions {\n /** Abort the request mid-flight. */\n signal?: AbortSignal;\n}\n\n/**\n * B2B buyer identity for Storefront API `@inContext(buyer: ...)`.\n * `customerAccessToken` is required when `buyer` is set (per Shopify's\n * BuyerInput spec). `companyLocationId` selects a specific B2B location\n * for catalog pricing when the customer has access to multiple.\n *\n * Security: this token MUST come from a same-origin authenticated endpoint\n * (Customer Account API PKCE flow) and MUST NOT be passed via URL params,\n * localStorage shared with third-party scripts, or static prop trees that\n * land in DOM snapshots. See each package's README (\"Markets & B2B\").\n */\nexport interface BuyerInput {\n customerAccessToken: string;\n companyLocationId?: string;\n}\n\n/** Lazy buyer resolver — called per request so tokens stay off static props. */\nexport type BuyerResolver = BuyerInput | (() => BuyerInput | Promise<BuyerInput>);\n\nexport interface StorefrontClientConfig {\n shopDomain: string;\n accessToken: string;\n apiVersion?: string;\n /**\n * End-buyer's IP, forwarded as `Shopify-Storefront-Buyer-IP`. Required\n * for server-side calls with private tokens and recommended for all SSR.\n * See the class doc-comment.\n */\n buyerIp?: string;\n /**\n * ISO-3166 alpha-2 country code passed as `@inContext(country: ...)`.\n * Drives Shopify Markets pricing, currency, and availability. Omit for\n * the shop's default market.\n */\n country?: string;\n /**\n * ISO-639-1 language code passed as `@inContext(language: ...)`.\n * Translations come from the shop's locale settings. Omit for default.\n */\n language?: string;\n /**\n * B2B buyer identity. Either a plain object (token captured at config\n * time) or a callback that resolves per request (recommended for\n * security). When set, the SDK adds `@inContext(buyer: ...)` to queries\n * and the response carries B2B catalog prices for that customer.\n */\n buyer?: BuyerResolver;\n}\n\n// Keep in sync with CLAUDE.md → Shopify API Version Alignment.\nexport const DEFAULT_API_VERSION = \"2026-07\";\n\nasync function resolveBuyer(buyer?: BuyerResolver): Promise<BuyerInput | undefined> {\n if (!buyer) return undefined;\n if (typeof buyer === \"function\") {\n return await buyer();\n }\n return buyer;\n}\n\n/**\n * Returns true if any `@inContext` field is set. Used by the query layer\n * to decide whether to emit the directive-wrapped or directive-free\n * variant — keeps cache fragmentation off for the no-context case.\n */\nexport function hasInContext(config: StorefrontClientConfig): boolean {\n return Boolean(config.country || config.language || config.buyer);\n}\n\nexport function createStorefrontClient(\n config: StorefrontClientConfig,\n): StorefrontClient {\n const version = config.apiVersion ?? DEFAULT_API_VERSION;\n const endpoint = `https://${config.shopDomain}/api/${version}/graphql.json`;\n\n return {\n async query<T = unknown>(\n query: string,\n variables?: Record<string, unknown>,\n options?: QueryOptions,\n ): Promise<T> {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n \"X-Shopify-Storefront-Access-Token\": config.accessToken,\n };\n if (config.buyerIp) {\n headers[\"Shopify-Storefront-Buyer-IP\"] = config.buyerIp;\n }\n\n // Inject @inContext variables when set. The query string template\n // already declares the optional `$country`, `$language`, and\n // `$buyer` variables; the directive is emitted via the caller\n // selecting the *_INCONTEXT query variant.\n const buyer = await resolveBuyer(config.buyer);\n const mergedVariables: Record<string, unknown> = { ...(variables ?? {}) };\n if (config.country) mergedVariables.country = config.country;\n if (config.language) mergedVariables.language = config.language;\n if (buyer) mergedVariables.buyer = buyer;\n\n const response = await fetch(endpoint, {\n method: \"POST\",\n headers,\n body: JSON.stringify({ query, variables: mergedVariables }),\n signal: options?.signal,\n });\n\n if (!response.ok) {\n throw new StorefrontApiError([\n {\n message: `Storefront API error: ${response.status} ${response.statusText}`,\n },\n ]);\n }\n\n const json = (await response.json()) as {\n data?: T;\n errors?: Array<{ message: string }>;\n };\n\n if (json.errors?.length) {\n throw new StorefrontApiError(json.errors);\n }\n\n return json.data as T;\n },\n };\n}\n","/**\n * Storefront API GraphQL queries.\n *\n * Pricing-sensitive queries (BUNDLE_METAOBJECT_QUERY, BUNDLES_FOR_PRODUCT_QUERY)\n * are wrapped with `@inContext(country:, language:, buyer:)` by\n * `withInContext()` below when the SDK config has any context field set.\n * The unwrapped variant stays publicly cacheable; the wrapped variant\n * must be marked `Cache-Control: private` per Hydrogen guidance for\n * buyer-contextual responses.\n *\n * The SDK fetcher selects the variant at request time via `hasInContext()`.\n */\nimport type { MetaobjectField } from \"./types\";\n\n/**\n * Returns the `@inContext`-wrapped variant of `query`. Adds the three\n * optional variables to the parameter list and the directive to the\n * operation root. Variable substitution itself is handled by the\n * Storefront client which injects `country`, `language`, `buyer` into\n * the request variables.\n *\n * Handles both arg-list (`query Name($x: Int) { ... }`) and zero-arg\n * (`query Name { ... }`) operations. Mutations are not supported —\n * adjust if one ever needs `@inContext`.\n */\nexport function withInContext(query: string): string {\n return query.replace(\n /query\\s+(\\w+)\\s*(?:\\(([^)]*)\\))?\\s*\\{/,\n (_match, name, args) => {\n const trimmed = (args || \"\").trim();\n const extra = \"$country: CountryCode, $language: LanguageCode, $buyer: BuyerInput\";\n const combined = trimmed ? `${trimmed}, ${extra}` : extra;\n return `query ${name}(${combined}) @inContext(country: $country, language: $language, buyer: $buyer) {`;\n },\n );\n}\n\nexport const BUNDLE_METAOBJECT_QUERY = `#graphql\n query BundleMetaobject($id: ID!) {\n metaobject(id: $id) {\n id\n type\n fields {\n key\n value\n reference {\n ... on Product {\n id\n title\n handle\n productType\n featuredImage { url altText width height }\n priceRange {\n minVariantPrice { amount currencyCode }\n maxVariantPrice { amount currencyCode }\n }\n variants(first: 100) {\n nodes {\n id\n title\n availableForSale\n currentlyNotInStock\n quantityAvailable\n price { amount currencyCode }\n compareAtPrice { amount currencyCode }\n unitPrice { amount currencyCode }\n unitPriceMeasurement { referenceValue referenceUnit }\n selectedOptions { name value }\n image { url altText width height }\n }\n }\n }\n }\n references(first: 50) {\n nodes {\n ... on Product {\n id\n title\n handle\n productType\n featuredImage { url altText width height }\n priceRange {\n minVariantPrice { amount currencyCode }\n maxVariantPrice { amount currencyCode }\n }\n variants(first: 100) {\n nodes {\n id\n title\n availableForSale\n currentlyNotInStock\n quantityAvailable\n price { amount currencyCode }\n compareAtPrice { amount currencyCode }\n unitPrice { amount currencyCode }\n unitPriceMeasurement { referenceValue referenceUnit }\n selectedOptions { name value }\n image { url altText width height }\n }\n }\n }\n ... on Collection {\n id\n title\n handle\n products(first: 50) {\n nodes {\n id\n title\n handle\n productType\n featuredImage { url altText width height }\n priceRange {\n minVariantPrice { amount currencyCode }\n maxVariantPrice { amount currencyCode }\n }\n variants(first: 100) {\n nodes {\n id\n title\n availableForSale\n currentlyNotInStock\n quantityAvailable\n price { amount currencyCode }\n compareAtPrice { amount currencyCode }\n unitPrice { amount currencyCode }\n unitPriceMeasurement { referenceValue referenceUnit }\n selectedOptions { name value }\n image { url altText width height }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n`;\n\n/**\n * Shop-level settings the storefront needs, in one round trip.\n *\n * `customCss` is auto-injected as a scoped <style> tag so headless storefronts\n * get CSS parity with classic-theme merchants.\n */\nexport const SHOP_SETTINGS_QUERY = `#graphql\n query ShopSettings {\n shop {\n customCss: metafield(namespace: \"$app\", key: \"custom_css\") {\n value\n }\n }\n }\n`;\n\nexport interface ShopSettingsResponse {\n shop: {\n customCss: { value: string | null } | null;\n } | null;\n}\n\n/**\n * Product-aware bundle lookup. Mirrors the pattern the classic Liquid theme\n * block uses (`product.metafields[\"$app\"].active_bundle_ids`): one query\n * returns every bundle that applies to the current product, fully resolved\n * (nested product/collection references included) so the caller gets the\n * same shape as `parseMetaobjectBundleStrict` expects.\n *\n * The product handle is the natural input on a headless storefront — every\n * canonical Shopify product URL is `/products/<handle>`, so the widget can\n * often derive it from `window.location` without merchant input.\n *\n * Complexity: the nested references (bundle → products → variants) are deep\n * but stay within the Storefront API budget for token-based queries at\n * realistic bundle sizes (≤10 bundles per product, ≤50 products per bundle).\n */\nexport const BUNDLES_FOR_PRODUCT_QUERY = `#graphql\n query BundlesForProduct($handle: String!) {\n product(handle: $handle) {\n id\n handle\n title\n metafield(namespace: \"$app\", key: \"active_bundle_ids\") {\n references(first: 10) {\n nodes {\n ... on Metaobject {\n id\n type\n fields {\n key\n value\n reference {\n ... on Product {\n id\n title\n handle\n productType\n featuredImage { url altText width height }\n priceRange {\n minVariantPrice { amount currencyCode }\n maxVariantPrice { amount currencyCode }\n }\n variants(first: 100) {\n nodes {\n id\n title\n availableForSale\n currentlyNotInStock\n quantityAvailable\n price { amount currencyCode }\n compareAtPrice { amount currencyCode }\n unitPrice { amount currencyCode }\n unitPriceMeasurement { referenceValue referenceUnit }\n selectedOptions { name value }\n image { url altText width height }\n }\n }\n }\n }\n references(first: 50) {\n nodes {\n ... on Product {\n id\n title\n handle\n productType\n featuredImage { url altText width height }\n priceRange {\n minVariantPrice { amount currencyCode }\n maxVariantPrice { amount currencyCode }\n }\n variants(first: 100) {\n nodes {\n id\n title\n availableForSale\n currentlyNotInStock\n quantityAvailable\n price { amount currencyCode }\n compareAtPrice { amount currencyCode }\n unitPrice { amount currencyCode }\n unitPriceMeasurement { referenceValue referenceUnit }\n selectedOptions { name value }\n image { url altText width height }\n }\n }\n }\n ... on Collection {\n id\n title\n handle\n products(first: 50) {\n nodes {\n id\n title\n handle\n productType\n featuredImage { url altText width height }\n priceRange {\n minVariantPrice { amount currencyCode }\n maxVariantPrice { amount currencyCode }\n }\n variants(first: 100) {\n nodes {\n id\n title\n availableForSale\n currentlyNotInStock\n quantityAvailable\n price { amount currencyCode }\n compareAtPrice { amount currencyCode }\n unitPrice { amount currencyCode }\n unitPriceMeasurement { referenceValue referenceUnit }\n selectedOptions { name value }\n image { url altText width height }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n`;\n\n/**\n * Return shape for BUNDLES_FOR_PRODUCT_QUERY. The inner `fields` array\n * reuses `MetaobjectField` verbatim so callers can pass it straight to\n * `parseMetaobjectBundleStrict` without a type cast — the same shape the\n * BUNDLE_METAOBJECT_QUERY response produces for a single bundle.\n */\nexport interface BundlesForProductResponse {\n product: {\n id: string;\n handle: string;\n title: string;\n metafield: {\n references: {\n nodes: Array<{\n id: string;\n type: string;\n fields: MetaobjectField[];\n }>;\n };\n } | null;\n } | null;\n}\n\n/**\n * Tokenless cart mutations. Storefront API docs: \"Cart (read/write)\" is\n * available on the Tokenless Access tier. The widget's default-cart path\n * uses these so merchants don't need a cart implementation.\n */\nexport const CART_CREATE_MUTATION = `#graphql\n mutation CartCreate($input: CartInput!) {\n cartCreate(input: $input) {\n cart { id checkoutUrl }\n userErrors { field message code }\n warnings { code message target }\n }\n }\n`;\n\nexport const CART_LINES_ADD_MUTATION = `#graphql\n mutation CartLinesAdd($cartId: ID!, $lines: [CartLineInput!]!) {\n cartLinesAdd(cartId: $cartId, lines: $lines) {\n cart { id checkoutUrl }\n userErrors { field message code }\n warnings { code message target }\n }\n }\n`;\n\n/**\n * Current lines of a saved cart, with their line-item attributes. The\n * widget's default-cart flow runs this before reusing a persisted cart id\n * so it can find (and remove) its own earlier lines — the ones tagged with\n * the `_lime_bundle_gid` attribute — instead of stacking a second copy of\n * an abandoned attempt on top. `first: 250` is the Storefront API's page\n * maximum; a default-flow cart holds a handful of bundle lines, so one\n * page is always enough in practice.\n */\nexport const CART_LINES_QUERY = `#graphql\n query CartLines($cartId: ID!) {\n cart(id: $cartId) {\n id\n lines(first: 250) {\n nodes {\n id\n attributes { key value }\n }\n }\n }\n }\n`;\n\n/** Top-level response shape for `CART_LINES_QUERY`. `cart` is null when\n * the id no longer resolves (expired or merged on Shopify's side). */\nexport interface CartLinesQueryResponse {\n cart: {\n id: string;\n lines: {\n nodes: Array<{\n id: string;\n attributes: Array<{ key: string; value: string | null }>;\n }>;\n };\n } | null;\n}\n\nexport const CART_LINES_REMOVE_MUTATION = `#graphql\n mutation CartLinesRemove($cartId: ID!, $lineIds: [ID!]!) {\n cartLinesRemove(cartId: $cartId, lineIds: $lineIds) {\n cart { id checkoutUrl }\n userErrors { field message code }\n warnings { code message target }\n }\n }\n`;\n\n/**\n * Inner payload shape shared by both cart mutations. Not a GraphQL\n * response on its own — see `CartCreateResponse` and `CartLinesAddResponse`\n * for the correctly-wrapped top-level response shapes.\n *\n * `warnings` is where Shopify reports lines it silently adjusted —\n * `MERCHANDISE_NOT_ENOUGH_STOCK` when a quantity was capped below what the\n * bundle needs. A capped bundle checks out at full price with no discount,\n * so callers must treat those warnings as failures, not footnotes.\n */\nexport interface CartMutationPayload {\n cart: { id: string; checkoutUrl: string } | null;\n userErrors: Array<{ field: string[] | null; message: string; code?: string | null }>;\n warnings?: Array<{ code?: string | null; message: string; target?: string | null }>;\n}\n\n/** Top-level response shape for `CART_CREATE_MUTATION`. */\nexport interface CartCreateResponse {\n cartCreate: CartMutationPayload;\n}\n\n/** Top-level response shape for `CART_LINES_ADD_MUTATION`. */\nexport interface CartLinesAddResponse {\n cartLinesAdd: CartMutationPayload;\n}\n\n/** Top-level response shape for `CART_LINES_REMOVE_MUTATION`. */\nexport interface CartLinesRemoveResponse {\n cartLinesRemove: CartMutationPayload;\n}\n","/**\n * Stable cache-key helper for SWR / TanStack Query / Hydrogen consumers.\n *\n * Buyer-contextual queries (Storefront `@inContext(buyer:)`) must NEVER\n * be shared across users — per Hydrogen's caching guidance, set\n * Cache-Control: private on those responses and ensure your cache key\n * includes a buyer fingerprint, never the raw token.\n *\n * Example usage with TanStack Query:\n * queryKey: getCacheKey(\"BundleMetaobject\", { id }, {\n * shopDomain, country, language, buyer,\n * })\n */\nimport type { BuyerInput } from \"./client\";\n\nexport interface CacheKeyContext {\n shopDomain: string;\n country?: string;\n language?: string;\n /**\n * Pass the resolved BuyerInput or undefined. Never pass a function —\n * the consumer is responsible for resolving lazy buyers before keying.\n * The raw `customerAccessToken` is hashed before being included in the\n * key so it doesn't appear in devtools / log output.\n */\n buyer?: BuyerInput;\n}\n\nexport function getCacheKey(\n queryName: string,\n variables: Record<string, unknown>,\n ctx: CacheKeyContext,\n): string[] {\n return [\n ctx.shopDomain,\n queryName,\n stableStringify(variables),\n ctx.country ?? \"_\",\n ctx.language ?? \"_\",\n ctx.buyer ? buyerFingerprint(ctx.buyer) : \"_\",\n ];\n}\n\n/**\n * Deterministic JSON.stringify with sorted keys. Avoids cache misses caused\n * by object property order varying across consumers.\n */\nfunction stableStringify(value: unknown): string {\n if (value === null || typeof value !== \"object\") return JSON.stringify(value);\n if (Array.isArray(value)) {\n return \"[\" + value.map(stableStringify).join(\",\") + \"]\";\n }\n const entries = Object.entries(value as Record<string, unknown>).sort(\n ([a], [b]) => a.localeCompare(b),\n );\n return (\n \"{\" +\n entries.map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`).join(\",\") +\n \"}\"\n );\n}\n\n/**\n * Lightweight non-cryptographic hash of the buyer token + optional company\n * location. Stable across runs, fast, and good enough to discriminate\n * cache entries per buyer without leaking the raw token. Don't use this\n * for security purposes — it's purely for cache keying.\n */\nfunction buyerFingerprint(buyer: BuyerInput): string {\n const input = `${buyer.customerAccessToken}:${buyer.companyLocationId ?? \"\"}`;\n // FNV-1a 32-bit\n let hash = 2166136261;\n for (let i = 0; i < input.length; i++) {\n hash ^= input.charCodeAt(i);\n hash = Math.imul(hash, 16777619);\n }\n // Unsigned hex, 8 chars\n return (hash >>> 0).toString(16).padStart(8, \"0\");\n}\n","/**\n * Bundle types for headless rendering.\n *\n * `ParsedBundle` is a discriminated union keyed on `bundleType` so consumers\n * can narrow the type via a switch statement or conditional check:\n *\n * function render(bundle: ParsedBundle) {\n * switch (bundle.bundleType) {\n * case \"fixed\": // bundle.products is load-bearing\n * case \"volume\": // bundle.volumeTiers is load-bearing\n * case \"mix_match\": // bundle.minQuantity / maxQuantity are load-bearing\n * case \"bogo\": // bundle.buyProductId / getProductId / quantities are load-bearing\n * case \"multi_step\": // bundle.steps / productRules are load-bearing\n * }\n * }\n *\n * Each variant carries ONLY the fields it actually uses. Fields that are\n * always present (id, title, status, schedule, widget config, A/B test\n * metadata) live on the shared `BundleBase` type.\n *\n * Locked for v3.0.0 — widening or narrowing these discriminants is a\n * breaking change. v3.0.0 widened the union with the \"bogo\" and\n * \"multi_step\" variants. `WidgetConfig` widened in 2.0.0 to expose the full\n * admin-editor shape; see WidgetConfig for the eight sub-section structure.\n */\nimport type { Product } from \"../storefront-api/types\";\n\nexport type BundleType = \"fixed\" | \"mix_match\" | \"volume\" | \"bogo\" | \"multi_step\";\n/**\n * `archived` is a legacy system-owned status that is no longer produced. The\n * SDK parser filters archived bundles upstream — they never reach a rendered\n * component — but the union must list the value so raw metaobject reads\n * round-trip cleanly through `parseMetaobjectBundleStrict`.\n *\n * `sync_failed` represents a bundle whose three-layer Metaobject→Discount→Prisma\n * write didn't complete; the admin form shows a \"didn't save properly\" banner\n * and merchants resave to retry. The SDK skips rendering these too.\n */\nexport type BundleStatus =\n | \"active\"\n | \"draft\"\n | \"inactive\"\n | \"out_of_stock\"\n | \"archived\"\n | \"sync_failed\";\n\nexport interface DiscountConfig {\n discountType: \"percentage\" | \"fixed_amount\";\n discountValue: number;\n allowStacking: boolean;\n}\n\n/**\n * Widget configuration as stored in the bundle metaobject's `widget_config`\n * field. Structured by visual area to mirror the in-app editor's accordion.\n *\n * The web component (`<lime-bundle>`) applies every value as a `--lb-*` CSS\n * custom property so the rendered widget matches what the merchant saw in\n * the editor. The React SDK exposes this object unchanged on\n * `ParsedBundle.widgetConfig`; whether and how to apply it is up to the\n * developer.\n */\n/**\n * Widget header section — countdown timer colours only.\n *\n * The colored/gradient header band and the \"-X%\" save badge were retired in the\n * manifest-and-receipt redesign: the bundle title now reads as a plain section\n * heading in the host theme's own type (no app-chrome band), and savings are\n * stated once on the receipt summary line. The interface key stays `header` for\n * config back-compat; only the countdown timer's colours remain.\n */\nexport interface HeaderConfig {\n countdownBgColor: string;\n countdownTextColor: string;\n}\n\n/**\n * Global corner-rounding preset. One choice sets the base radius (`--lb-radius`);\n * every element derives its own from it (cards + CTA use the base, smaller\n * controls use `--lb-radius-sm`). Mirrors `RadiusPreset` in the admin's\n * `app/lib/bundle-types.ts`.\n */\nexport type RadiusPreset = \"none\" | \"subtle\" | \"rounded\" | \"round\";\n\nexport interface LayoutConfig {\n backgroundColor: string;\n borderColor: string;\n borderWidth: number;\n borderRadius: RadiusPreset;\n}\n\n/**\n * Thumbnail aspect-ratio choice. Drives a derived block in\n * applyWidgetConfigVars that sets three CSS vars\n * (--lb-thumbnail-aspect-ratio, --lb-thumbnail-img-fit,\n * --lb-thumbnail-img-height). \"original\" disables the aspect crop and\n * lets each image render at its intrinsic ratio.\n */\nexport type ThumbnailRatio = \"square\" | \"tall\" | \"wide\" | \"original\";\n\nexport interface ProductListConfig {\n textColor: string;\n thumbnailRatio: ThumbnailRatio;\n showPrice: boolean;\n showCompareAtPrice: boolean;\n /**\n * The per-unit line (\"$4.99/kg\") under a product's price. Distinct from\n * `PricingConfig.showPerUnitPrice`, which governs the bundle summary's\n * per-item figure rather than the product row.\n */\n showUnitPrice: boolean;\n /** The \"×N\" count on a product row, whether chip or inline. */\n showQuantity: boolean;\n}\n\nexport interface PricingConfig {\n showComparePrice: boolean;\n showPerUnitPrice: boolean;\n showItemCount: boolean;\n showCompareAtPrice: boolean;\n}\n\n/**\n * Widget savings line — toggles the quiet \"You save {amount} ({percent}%)\"\n * line on the receipt summary. The line inherits the widget's own text colour\n * (lime is reserved for the CTA), so no colour/border fields remain. The\n * interface key stays `savingsBar` for config back-compat.\n */\nexport interface SavingsBarConfig {\n visible: boolean;\n}\n\nexport interface CtaConfig {\n ctaText: string;\n primaryColor: string;\n buttonTextColor: string;\n borderWidth: number;\n borderColor: string;\n}\n\nexport interface CountdownConfig {\n showCountdown: boolean;\n}\n\nexport interface PopularBadgeConfig {\n visible: boolean;\n text: string;\n /** Optional — merchant may not pick a tier. */\n tierIndex?: number;\n bgColor: string;\n textColor: string;\n /** Keeps its own border width + color; the corner radius follows the global\n * radius preset, so there is no badge radius field. */\n borderWidth: number;\n borderColor: string;\n}\n\nexport interface WidgetConfig {\n header: HeaderConfig;\n layout: LayoutConfig;\n productList: ProductListConfig;\n pricing: PricingConfig;\n cta: CtaConfig;\n savingsBar: SavingsBarConfig;\n countdown: CountdownConfig;\n popularBadge: PopularBadgeConfig;\n /** Show \"Only X left\" badge when remaining stock is at or below\n * `lowStockThreshold`. Hidden silently when the Storefront token\n * lacks `unauthenticated_read_product_inventory`. */\n showLowStockBadge: boolean;\n /** Threshold (1-99) below which the low-stock badge appears. */\n lowStockThreshold: number;\n lowStockBgColor: string;\n lowStockTextColor: string;\n\n // --- Mix-match picker section ---\n showSearch: boolean;\n /** Show product-type filter pills above the picker grid (derived from the\n * eligible products' Shopify product types). */\n mixMatchShowTypeFilters: boolean;\n /**\n * When true (default), the picker modal renders a per-pick qty stepper so\n * the shopper can choose how many of each product to add. When false, the\n * stepper is hidden and Add adds qty = `productRules[productId]?.min ?? 1`.\n */\n mixMatchShowQuantitySelector: boolean;\n pickerBgColor: string;\n pickerTextColor: string;\n // Picker modal \"Borders\" — the single source for every border in the modal\n // (modal, search, product tiles, variant select, quantity stepper). Thumbnails\n // take the radius only; the Add button keeps its own width/color but this radius.\n pickerBorderWidth: number;\n pickerBorderColor: string;\n pickerBorderRadius: RadiusPreset;\n /** Aspect ratio for product thumbnails inside the picker modal —\n * independent of the main widget's `productList.thumbnailRatio`. */\n pickerThumbnailRatio: ThumbnailRatio;\n pickerAddBgColor: string;\n pickerAddLabelColor: string;\n pickerAddBorderWidth: number;\n pickerAddBorderColor: string;\n\n // --- Multi-step wizard section ---\n /**\n * When true, the wizard moves to the next step on its own once the\n * shopper's picks reach the step's maximum. Triggered by the maximum,\n * never the minimum; the last step and steps without a maximum never\n * auto-advance. Off by default.\n */\n multiStepAutoAdvance: boolean;\n\n // --- Volume tier section ---\n // Tier cards inherit the global border (width/color) and radius — no fields.\n defaultTier: \"first\" | \"best_value\" | number;\n}\n\n/**\n * A single volume-bundle tier row. `discountType` lives at the bundle\n * level (`bundle.discountConfig.discountType`) — each tier carries only\n * the magnitude in whichever field matches that type:\n * - `percentage` is a whole-number (e.g. 10 for 10% off)\n * - `amount` is the per-unit fixed amount off, in the shop's currency\n *\n * Exactly one of `percentage` or `amount` is present per tier, determined\n * by the parent bundle's `discountType`. This mirrors the on-metaobject\n * shape — see `app/lib/bundle-types.ts` VolumeTierSchema.\n */\nexport interface VolumeTier {\n minQuantity: number;\n percentage?: number;\n amount?: number;\n}\n\n/** Fields present on every bundle regardless of type. */\ninterface BundleBase {\n id: string;\n title: string;\n /** Customer-facing subtitle rendered under the bundle title. */\n description: string | null;\n status: BundleStatus;\n products: Product[];\n discountConfig: DiscountConfig;\n widgetConfig: WidgetConfig;\n startsAt: string | null;\n endsAt: string | null;\n discountLabel: string | null;\n\n /**\n * Per-product allowed-variant lists, positionally aligned with\n * `products`. When the merchant restricted a product to specific\n * variants, only those GIDs should appear in the picker and dropdown.\n * Inner array empty or whole entry null means \"all available variants\".\n */\n selectedVariantIds: string[][] | null;\n\n /** Per-product bundle quantity, keyed by Shopify Product GID. */\n productQuantities: Record<string, number>;\n /** Per-variant quantity override, keyed by Shopify ProductVariant GID. */\n variantQuantities: Record<string, number>;\n\n /**\n * Market visibility scoping.\n * - \"all\" (default): bundle is visible in every Shopify market.\n * - \"specific\": bundle is visible only in the markets listed in marketIds.\n *\n * Consumers querying with a marketId can use isVisibleInMarket() to filter.\n */\n marketVisibility: \"all\" | \"specific\";\n /**\n * Allowed Shopify Market GIDs when marketVisibility = \"specific\".\n * Empty when marketVisibility = \"all\".\n * Example: [\"gid://shopify/Market/1\", \"gid://shopify/Market/2\"]\n */\n marketIds: string[];\n /**\n * Retail projection of the selected markets: ISO-3166-1 alpha-2 country\n * codes from each market's region condition, written by the admin at save\n * time. May contain the \"*\" wildcard when a selected market targets all\n * regions (\"Worldwide\"). Empty when marketVisibility = \"all\" or for\n * metaobjects written before this field existed.\n *\n * Prefer gating on this (via isVisibleToBuyer) over marketIds: Shopify has\n * deprecated market IDs as buyer-targeting signals, and a B2B buyer's\n * market never surfaces through storefront localization at all.\n */\n countryCodes: string[];\n /**\n * B2B projection of the selected markets: CompanyLocation GIDs from each\n * market's company-location condition, written by the admin at save time.\n * May contain the \"*\" wildcard when a selected market targets all company\n * locations. Empty when marketVisibility = \"all\", when no selected market\n * is a B2B market, or for metaobjects written before this field existed.\n */\n companyLocationIds: string[];\n\n /**\n * Id of the A/B test this bundle is one side of, or null when it is not in\n * one.\n *\n * Two bundles sharing an `abTestId` are alternatives, not a pair to render\n * together: exactly one of them should ever be shown to a given shopper.\n * Pass the fetched list through `resolveAbTests` to make that choice.\n */\n abTestId: string | null;\n\n /**\n * This side's share of shoppers, 0–100. Only meaningful alongside\n * `abTestId`, and 0 when there is no test.\n */\n abWeight: number;\n}\n\nexport interface FixedBundleData extends BundleBase {\n bundleType: \"fixed\";\n}\n\nexport interface VolumeBundleData extends BundleBase {\n bundleType: \"volume\";\n volumeTiers: VolumeTier[];\n}\n\n/** Per-product unit-count rule for mix & match bundles. */\nexport interface ProductRule {\n /** Smallest qty the shopper must add when picking this product. */\n min: number;\n /** Largest qty the shopper may add for this pick. >= min. */\n max: number;\n /**\n * When true the product is always part of the bundle: renderers pre-seed\n * it at `min` units and block any removal that would drop its total below\n * `min` (shoppers may still swap variants and adjust quantity). The\n * checkout discount only applies while the cart holds >= `min` units of\n * the product. Absent = not required.\n */\n required?: boolean;\n}\n\n/** Default rule applied to products without an explicit entry (e.g. collection-sourced). */\nexport const DEFAULT_PRODUCT_RULE: ProductRule = { min: 1, max: 99 };\n\nexport interface MixMatchBundleData extends BundleBase {\n bundleType: \"mix_match\";\n /** Total number of units the shopper must select to qualify for the discount, summed across all chosen products. */\n minQuantity: number | null;\n /**\n * @deprecated removed from the data model — always null on parsed bundles.\n * The bundle no longer has a top-level total cap; per-product max lives in productRules.\n */\n maxQuantity: number | null;\n /** Per-product unit-count rules keyed by product GID. Missing entries default to DEFAULT_PRODUCT_RULE. */\n productRules: Record<string, ProductRule>;\n /**\n * Optional per-variant rules keyed by variant GID, nested inside the owning\n * product's rule: the product rule caps that product's total across every\n * variant, and a variant rule refines one slot inside it. Variants without\n * an entry inherit the product rule. Empty on every bundle saved before\n * variant rules existed.\n */\n variantRules: Record<string, ProductRule>;\n}\n\n/**\n * BOGO (Buy X Get Y) bundle: the shopper buys `buyQuantity` units of the\n * buy product and gets `getQuantity` units of the get product (which may be\n * the same product) at `discountConfig.discountValue` percent off\n * (100 = free). The reward repeats per full multiple in the cart. The\n * shared `products` list holds the deduped [buy, get] pair.\n */\nexport interface BogoBundleData extends BundleBase {\n bundleType: \"bogo\";\n /** Product GID the shopper must buy to qualify. */\n buyProductId: string;\n /** Product GID the shopper gets discounted; may equal buyProductId. */\n getProductId: string;\n /** Units of the buy product required per reward (N). */\n buyQuantity: number;\n /** Units of the get product discounted per reward (M). */\n getQuantity: number;\n /**\n * Variant GIDs the buy side is narrowed to; empty = every variant.\n * Per-side lists exist because the positional `selectedVariantIds`\n * cannot hold two different lists for a same-product BOGO, which\n * silently dropped the get side's choice (issue #436).\n */\n buyVariantIds: string[];\n /** Variant GIDs the get side is narrowed to; see buyVariantIds. */\n getVariantIds: string[];\n}\n\n/**\n * One step of a multi-step bundle (\"Pick a shampoo\" → \"Pick a conditioner\").\n * Parsed from the metaobject's `steps` JSON field. Each step scopes the\n * picker to its own product pool (pinned `productIds` plus the products of\n * its `collectionIds`) and carries its own unit requirement:\n * - `minQuantity` (>= 1) — units the shopper must pick in this step.\n * - `maxQuantity` — step unit ceiling, or null for unlimited.\n */\nexport interface ParsedMultiStep {\n name: string;\n minQuantity: number;\n maxQuantity: number | null;\n /** Product GIDs pinned to this step (subset of the cross-step union in `products`). */\n productIds: string[];\n /** Collection GIDs whose products belong to this step's pool. */\n collectionIds: string[];\n}\n\n/**\n * Multi-step bundle: the shopper walks named steps as a wizard, each with\n * its own product pool and min/max unit bounds; the whole-bundle discount\n * applies once every step's minimum is met. The shared `products` list is\n * the flattened cross-step union (pinned products in `selectedVariantIds`\n * order, then collection-resolved products). Use `productsForStep` to\n * recover each step's pool — attribution is exact because the parser\n * records `collectionProductIds` while resolving the `collection` field.\n *\n * `productRules` are step-agnostic: `rule.max` caps the product's TOTAL\n * units across every slot in every step.\n */\nexport interface MultiStepBundleData extends BundleBase {\n bundleType: \"multi_step\";\n steps: ParsedMultiStep[];\n /** Per-product unit-count rules keyed by product GID. Missing entries default to DEFAULT_PRODUCT_RULE. */\n productRules: Record<string, ProductRule>;\n /**\n * Optional per-variant rules keyed by variant GID, nested inside the owning\n * product's rule and step-agnostic for the same reason `productRules` are:\n * one rule per variant, however many steps its product appears in.\n */\n variantRules: Record<string, ProductRule>;\n /**\n * Exact per-collection membership recorded at parse time:\n * collection GID → the product GIDs that collection resolved to. Lets\n * `productsForStep` attribute collection-sourced products to the right\n * step without a separate collection-membership query.\n */\n collectionProductIds: Record<string, string[]>;\n}\n\nexport type ParsedBundle =\n | FixedBundleData\n | VolumeBundleData\n | MixMatchBundleData\n | BogoBundleData\n | MultiStepBundleData;\n\n/**\n * Thrown when metaobject fields cannot be parsed into a valid ParsedBundle.\n * Distinct from StorefrontApiError so callers can handle \"bundle not\n * renderable\" (inactive, expired, schema mismatch) differently from\n * \"network failure\".\n */\nexport class BundleParseError extends Error {\n constructor(message: string, public readonly reason: BundleParseFailReason) {\n super(message);\n this.name = \"BundleParseError\";\n }\n}\n\nexport type BundleParseFailReason =\n | \"not_found\"\n | \"invalid_type\"\n | \"inactive\"\n | \"not_started\"\n | \"expired\";\n","/**\n * Default widget config + CSS-variable flattener.\n *\n * Mirrors the Zod defaults and `flattenWidgetConfig` in the admin package's\n * `app/lib/bundle-types.ts`. Kept in plain TypeScript so the SDK packages\n * stay Zod-free; merchant-authored config is validated at the admin\n * boundary before it reaches the metaobject, so consumers of this module\n * can trust the JSON shape and just merge against these defaults.\n *\n * If a future merchant-editor field is added:\n * 1. Update `WidgetConfig` in types.ts\n * 2. Add the default here\n * 3. Add the flattened CSS-var key below\n * 4. Consume the var in packages/widget/src/styles/*.css\n */\nimport type { LayoutConfig, RadiusPreset, WidgetConfig } from \"./types\";\n\n/**\n * Pixel value each global radius preset resolves to for the base `--lb-radius`.\n * Mirrors `RADIUS_PRESET_PX` in the admin's `app/lib/bundle-types.ts`.\n */\nexport const RADIUS_PRESET_PX: Record<RadiusPreset, number> = {\n none: 0,\n subtle: 6,\n rounded: 12,\n round: 18,\n};\n\nexport const WIDGET_CONFIG_DEFAULTS: WidgetConfig = {\n header: {\n countdownBgColor: \"#B4DC7F47\",\n countdownTextColor: \"#555555\",\n },\n layout: {\n backgroundColor: \"#FCFCFC\",\n borderColor: \"#E5E5E5\",\n borderWidth: 1,\n borderRadius: \"subtle\",\n },\n productList: {\n textColor: \"#555555\",\n thumbnailRatio: \"square\",\n showPrice: true,\n showCompareAtPrice: true,\n showUnitPrice: true,\n showQuantity: true,\n },\n pricing: {\n showComparePrice: true,\n showPerUnitPrice: true,\n showItemCount: true,\n showCompareAtPrice: true,\n },\n cta: {\n ctaText: \"Add to cart\",\n primaryColor: \"#B4DC7F\",\n buttonTextColor: \"#555555\",\n borderWidth: 0,\n borderColor: \"#B4DC7F\",\n },\n savingsBar: {\n visible: true,\n },\n countdown: {\n showCountdown: true,\n },\n popularBadge: {\n visible: true,\n text: \"Most Popular\",\n bgColor: \"#1A1A1A\",\n textColor: \"#FFFFFF\",\n borderWidth: 0,\n borderColor: \"#1A1A1A\",\n },\n showLowStockBadge: true,\n lowStockThreshold: 10,\n lowStockBgColor: \"#FEF3C7\",\n lowStockTextColor: \"#92400E\",\n\n showSearch: true,\n mixMatchShowTypeFilters: true,\n mixMatchShowQuantitySelector: true,\n pickerBgColor: \"#FCFCFC\",\n pickerTextColor: \"#555555\",\n pickerBorderWidth: 1,\n pickerBorderColor: \"#E5E5E5\",\n pickerBorderRadius: \"subtle\",\n pickerThumbnailRatio: \"square\",\n pickerAddBgColor: \"#B4DC7F\",\n pickerAddLabelColor: \"#555555\",\n pickerAddBorderWidth: 0,\n pickerAddBorderColor: \"#B4DC7F\",\n\n multiStepAutoAdvance: false,\n\n defaultTier: \"first\",\n};\n\n/**\n * Merge a partial (possibly stale-schema) widget config against the\n * defaults, so downstream renderers can trust every field. Top-level and\n * nested-section fields both merge shallow; unknown keys on nested sections\n * are preserved (but ignored by the renderer). This is what the parser\n * calls after pulling `widget_config` JSON from the metaobject.\n */\nexport function mergeWidgetConfig(raw: unknown): WidgetConfig {\n if (!raw || typeof raw !== \"object\") {\n return WIDGET_CONFIG_DEFAULTS;\n }\n const input = raw as Partial<WidgetConfig> & Record<string, unknown>;\n return {\n ...WIDGET_CONFIG_DEFAULTS,\n ...input,\n defaultTier: sanitizeDefaultTier(input.defaultTier),\n // Coerce a legacy stored numeric picker radius (pre-preset configs) to a preset.\n pickerBorderRadius: sanitizeRadiusPreset(input.pickerBorderRadius),\n header: { ...WIDGET_CONFIG_DEFAULTS.header, ...(input.header ?? {}) },\n layout: {\n ...WIDGET_CONFIG_DEFAULTS.layout,\n ...(input.layout ?? {}),\n // Coerce a legacy stored numeric radius (pre-preset configs) to a preset.\n borderRadius: sanitizeRadiusPreset(\n (input.layout as Partial<LayoutConfig> | undefined)?.borderRadius,\n ),\n },\n productList: {\n ...WIDGET_CONFIG_DEFAULTS.productList,\n ...(input.productList ?? {}),\n },\n pricing: {\n ...WIDGET_CONFIG_DEFAULTS.pricing,\n ...(input.pricing ?? {}),\n },\n cta: { ...WIDGET_CONFIG_DEFAULTS.cta, ...(input.cta ?? {}) },\n savingsBar: {\n ...WIDGET_CONFIG_DEFAULTS.savingsBar,\n ...(input.savingsBar ?? {}),\n },\n countdown: {\n ...WIDGET_CONFIG_DEFAULTS.countdown,\n ...(input.countdown ?? {}),\n },\n popularBadge: {\n ...WIDGET_CONFIG_DEFAULTS.popularBadge,\n ...(input.popularBadge ?? {}),\n },\n };\n}\n\nfunction sanitizeDefaultTier(\n raw: unknown,\n): \"first\" | \"best_value\" | number {\n if (raw === \"first\" || raw === \"best_value\") return raw;\n if (typeof raw === \"number\" && Number.isInteger(raw) && raw >= 0) return raw;\n return WIDGET_CONFIG_DEFAULTS.defaultTier;\n}\n\nfunction sanitizeRadiusPreset(raw: unknown): RadiusPreset {\n if (raw === \"none\" || raw === \"subtle\" || raw === \"rounded\" || raw === \"round\") {\n return raw;\n }\n return WIDGET_CONFIG_DEFAULTS.layout.borderRadius;\n}\n\n/**\n * Flatten the nested WidgetConfig to the CSS-custom-property keys consumed\n * by `packages/widget/src/styles/*.css`. Keys match the `--lb-*` variable\n * names (without the `--lb-` prefix). Keep in lockstep with the matching\n * function in `app/lib/bundle-types.ts` — the admin's in-app preview and\n * the shipped web component both read the same variable names, so drift\n * between the two maps is a merchant-visible rendering bug.\n */\nexport function flattenWidgetConfig(\n config: WidgetConfig,\n): Record<string, string | number | boolean> {\n return {\n primaryColor: config.cta.primaryColor,\n backgroundColor: config.layout.backgroundColor,\n textColor: config.productList.textColor,\n borderColor: config.layout.borderColor,\n borderWidth: config.layout.borderWidth,\n buttonTextColor: config.cta.buttonTextColor,\n ctaBorderWidth: config.cta.borderWidth,\n ctaBorderColor: config.cta.borderColor,\n // Global radius preset → px for the base --lb-radius; every element derives\n // its own radius from this (smaller controls via --lb-radius-sm in CSS).\n borderRadius: RADIUS_PRESET_PX[config.layout.borderRadius],\n countdownBgColor: config.header.countdownBgColor,\n countdownTextColor: config.header.countdownTextColor,\n thumbnailRatio: config.productList.thumbnailRatio,\n showProductPrice: config.productList.showPrice,\n showProductCompareAtPrice: config.productList.showCompareAtPrice,\n showProductUnitPrice: config.productList.showUnitPrice,\n showProductQuantity: config.productList.showQuantity,\n showCountdown: config.countdown.showCountdown,\n ctaText: config.cta.ctaText,\n showLowStockBadge: config.showLowStockBadge,\n lowStockThreshold: config.lowStockThreshold,\n lowStockBgColor: config.lowStockBgColor,\n lowStockTextColor: config.lowStockTextColor,\n showComparePrice: config.pricing.showComparePrice,\n showPerUnitPrice: config.pricing.showPerUnitPrice,\n showItemCount: config.pricing.showItemCount,\n showCompareAtPrice: config.pricing.showCompareAtPrice,\n showMostPopular: config.popularBadge.visible,\n popularBadgeText: config.popularBadge.text,\n popularBadgeBgColor: config.popularBadge.bgColor,\n popularBadgeTextColor: config.popularBadge.textColor,\n popularBadgeBorderWidth: config.popularBadge.borderWidth,\n popularBadgeBorderColor: config.popularBadge.borderColor,\n showSearch: config.showSearch,\n mixMatchShowTypeFilters: config.mixMatchShowTypeFilters,\n pickerBgColor: config.pickerBgColor,\n pickerTextColor: config.pickerTextColor,\n pickerBorderWidth: config.pickerBorderWidth,\n pickerBorderColor: config.pickerBorderColor,\n // Picker radius preset → px, same mapping as the global radius.\n pickerBorderRadius: RADIUS_PRESET_PX[config.pickerBorderRadius],\n pickerThumbnailRatio: config.pickerThumbnailRatio,\n pickerAddBgColor: config.pickerAddBgColor,\n pickerAddLabelColor: config.pickerAddLabelColor,\n pickerAddBorderWidth: config.pickerAddBorderWidth,\n pickerAddBorderColor: config.pickerAddBorderColor,\n defaultTier: config.defaultTier,\n ...(config.popularBadge.tierIndex !== undefined && {\n mostPopularTierIndex: config.popularBadge.tierIndex,\n }),\n };\n}\n\n/**\n * Map from `flattenWidgetConfig` keys to the actual CSS custom property\n * names consumed by the `bundle-*.css` stylesheets. The names are bespoke,\n * not mechanical, so we can't derive them with a naive camel-to-kebab\n * transform. Mirror of `CSS_VAR_MAP` in\n * `app/components/WidgetEditorPreview.tsx`.\n */\nexport const CSS_VAR_MAP: Record<string, string> = {\n primaryColor: \"--lb-primary-color\",\n backgroundColor: \"--lb-bg\",\n textColor: \"--lb-text\",\n borderColor: \"--lb-border\",\n borderWidth: \"--lb-border-width\",\n buttonTextColor: \"--lb-btn-text\",\n borderRadius: \"--lb-radius\",\n countdownBgColor: \"--lb-countdown-bg\",\n countdownTextColor: \"--lb-countdown-text\",\n ctaBorderWidth: \"--lb-cta-border-width\",\n ctaBorderColor: \"--lb-cta-border-color\",\n popularBadgeBgColor: \"--lb-popular-badge-bg\",\n popularBadgeTextColor: \"--lb-popular-badge-text\",\n popularBadgeBorderWidth: \"--lb-popular-badge-border-width\",\n popularBadgeBorderColor: \"--lb-popular-badge-border-color\",\n pickerBgColor: \"--lb-picker-bg\",\n pickerTextColor: \"--lb-picker-text\",\n pickerBorderWidth: \"--lb-picker-border-width\",\n pickerBorderColor: \"--lb-picker-border-color\",\n pickerBorderRadius: \"--lb-picker-radius\",\n pickerAddBgColor: \"--lb-picker-add-bg\",\n pickerAddLabelColor: \"--lb-picker-add-label\",\n pickerAddBorderWidth: \"--lb-picker-add-border-width\",\n pickerAddBorderColor: \"--lb-picker-add-border-color\",\n};\n\n/** Flat keys whose numeric value should be serialized with a `px` unit. */\nexport const PX_KEYS: ReadonlySet<string> = new Set([\n \"borderRadius\",\n \"borderWidth\",\n \"ctaBorderWidth\",\n \"popularBadgeBorderWidth\",\n \"pickerBorderWidth\",\n \"pickerBorderRadius\",\n \"pickerAddBorderWidth\",\n]);\n\n/**\n * Apply a WidgetConfig to a DOM element as CSS custom properties.\n *\n * Sets one `--lb-*` property per entry in CSS_VAR_MAP, px-suffixing numeric\n * dimensions, plus the boolean toggles that the Liquid template emits\n * as display values (`block`, `inline`, `flex`, or `none`):\n *\n * - `--lb-product-price-display` from productList.showPrice\n * - `--lb-product-compare-display` from productList.showCompareAtPrice\n * - `--lb-product-unit-price-display` from productList.showUnitPrice\n * - `--lb-product-qty-chip-display` from productList.showQuantity\n * - `--lb-product-qty-inline-display` from productList.showQuantity\n *\n * Anything the merchant didn't explicitly configure falls through to the\n * CSS file's own fallback value, so this call is safe on any element.\n */\nexport function applyWidgetConfigVars(\n el: { style: CSSStyleDeclaration },\n config: WidgetConfig,\n): void {\n const flat = flattenWidgetConfig(config);\n\n for (const [flatKey, cssVar] of Object.entries(CSS_VAR_MAP)) {\n const value = flat[flatKey];\n if (value === undefined || value === null) continue;\n const serialized = PX_KEYS.has(flatKey) ? `${value}px` : String(value);\n el.style.setProperty(cssVar, serialized);\n }\n\n // Conditional display toggles — merchant-configurable booleans that the\n // Liquid template emits as explicit display-mode CSS vars. We mirror that\n // exactly so the shipped bundle-*.css rules (which read these vars) hide\n // or show the right elements.\n el.style.setProperty(\n \"--lb-product-price-display\",\n config.productList.showPrice ? \"block\" : \"none\",\n );\n el.style.setProperty(\n \"--lb-product-compare-display\",\n config.productList.showCompareAtPrice ? \"inline\" : \"none\",\n );\n el.style.setProperty(\n \"--lb-product-unit-price-display\",\n config.productList.showUnitPrice ? \"block\" : \"none\",\n );\n // One merchant toggle, two elements: the fixed bundle's right-slot chip is a\n // flex box, every other row's count is inline text. A single var cannot carry\n // both \"on\" values, so the boolean drives one per element.\n el.style.setProperty(\n \"--lb-product-qty-chip-display\",\n config.productList.showQuantity ? \"inline-flex\" : \"none\",\n );\n el.style.setProperty(\n \"--lb-product-qty-inline-display\",\n config.productList.showQuantity ? \"inline\" : \"none\",\n );\n\n // Variant dropdown chevrons — embed the border color into the SVG stroke\n // so the arrow auto-matches. Mirrors the Liquid template in\n // `extensions/bundle-theme/blocks/bundle-widget.liquid` and the admin\n // preview helper in `app/components/WidgetEditorPreview.tsx`.\n el.style.setProperty(\n \"--lb-variant-chevron\",\n variantChevronUrl(config.layout.borderColor),\n );\n el.style.setProperty(\n \"--lb-picker-variant-chevron\",\n variantChevronUrl(config.pickerBorderColor),\n );\n\n // Thumbnail ratio — one merchant enum drives three CSS vars. \"original\"\n // disables aspect-ratio cropping (img renders at intrinsic ratio); the\n // other three values force a fixed aspect with object-fit: cover.\n const ratioVars = thumbnailRatioVars(config.productList.thumbnailRatio);\n el.style.setProperty(\"--lb-thumbnail-aspect-ratio\", ratioVars.aspectRatio);\n el.style.setProperty(\"--lb-thumbnail-img-fit\", ratioVars.imgFit);\n el.style.setProperty(\"--lb-thumbnail-img-height\", ratioVars.imgHeight);\n\n // Picker-modal thumbnail ratio — independent from the main widget so\n // merchants can e.g. show tall picker thumbs while keeping square main\n // thumbs. Reuses `thumbnailRatioVars` with a different CSS-var prefix.\n const pickerRatioVars = thumbnailRatioVars(config.pickerThumbnailRatio);\n el.style.setProperty(\n \"--lb-picker-thumbnail-aspect-ratio\",\n pickerRatioVars.aspectRatio,\n );\n el.style.setProperty(\n \"--lb-picker-thumbnail-img-fit\",\n pickerRatioVars.imgFit,\n );\n el.style.setProperty(\n \"--lb-picker-thumbnail-img-height\",\n pickerRatioVars.imgHeight,\n );\n}\n\n/**\n * Build an inline-SVG `url(...)` value for the variant dropdown chevron,\n * with the given hex color embedded as the stroke. `#` must be URL-encoded\n * inside a data URI. Kept in lockstep with the Liquid template and admin\n * preview helper — any change to the path/size/stroke-width must update\n * all three.\n */\nfunction variantChevronUrl(strokeHex: string): string {\n const encoded = strokeHex.replace(/#/g, \"%23\");\n return `url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath d='M3 4.5l3 3 3-3' fill='none' stroke='${encoded}' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E\")`;\n}\n\n/**\n * Map the merchant-facing thumbnail-ratio choice to the three CSS vars\n * the bundle stylesheets read. Exported so the Liquid template's mirror\n * of this logic stays in lockstep — any change to the value strings here\n * must be reflected in `bundle-widget.liquid` and the admin preview.\n */\nexport function thumbnailRatioVars(ratio: WidgetConfig[\"productList\"][\"thumbnailRatio\"]): {\n aspectRatio: string;\n imgFit: string;\n imgHeight: string;\n} {\n switch (ratio) {\n case \"tall\":\n return { aspectRatio: \"3 / 4\", imgFit: \"cover\", imgHeight: \"100%\" };\n case \"wide\":\n return { aspectRatio: \"4 / 3\", imgFit: \"cover\", imgHeight: \"100%\" };\n case \"original\":\n return { aspectRatio: \"auto\", imgFit: \"contain\", imgHeight: \"auto\" };\n case \"square\":\n default:\n return { aspectRatio: \"1 / 1\", imgFit: \"cover\", imgHeight: \"100%\" };\n }\n}\n","/**\n * Parse metaobject fields into a typed ParsedBundle (discriminated union).\n * Returns null if the bundle is not renderable (invalid type, inactive,\n * outside schedule) — callers who want structured reasons for \"not\n * renderable\" should use parseMetaobjectBundleStrict, which throws a\n * BundleParseError with a reason code.\n */\nimport type { MetaobjectField, Product } from \"../storefront-api/types\";\nimport {\n BundleParseError,\n type ParsedBundle,\n type BundleType,\n type BundleStatus,\n type DiscountConfig,\n type WidgetConfig,\n type VolumeTier,\n type FixedBundleData,\n type VolumeBundleData,\n type MixMatchBundleData,\n type BogoBundleData,\n type MultiStepBundleData,\n type ParsedMultiStep,\n} from \"./types\";\nimport { mergeWidgetConfig } from \"./widget-config\";\n\nconst VALID_BUNDLE_TYPES = new Set<BundleType>([\n \"fixed\",\n \"mix_match\",\n \"volume\",\n \"bogo\",\n \"multi_step\",\n]);\nconst ACTIVE_STATUSES = new Set<BundleStatus>([\"active\"]);\n\n/**\n * Parse a metaobject's fields array into a structured ParsedBundle.\n * Returns null if the bundle is not renderable.\n */\nexport function parseMetaobjectBundle(\n metaobjectId: string,\n fields: MetaobjectField[],\n): ParsedBundle | null {\n try {\n return parseMetaobjectBundleStrict(metaobjectId, fields);\n } catch (err) {\n if (err instanceof BundleParseError) return null;\n throw err;\n }\n}\n\n/**\n * Same as parseMetaobjectBundle but throws BundleParseError with a\n * structured reason instead of returning null.\n */\nexport function parseMetaobjectBundleStrict(\n metaobjectId: string,\n fields: MetaobjectField[],\n): ParsedBundle {\n const fieldMap = new Map(fields.map((f) => [f.key, f]));\n\n const title = fieldMap.get(\"title\")?.value ?? \"Bundle\";\n const rawBundleType = fieldMap.get(\"bundle_type\")?.value;\n const rawStatus = fieldMap.get(\"status\")?.value;\n\n if (!rawBundleType || !VALID_BUNDLE_TYPES.has(rawBundleType as BundleType)) {\n throw new BundleParseError(\n `Invalid or missing bundle_type: ${rawBundleType ?? \"null\"}`,\n \"invalid_type\",\n );\n }\n const bundleType = rawBundleType as BundleType;\n\n if (!rawStatus || !ACTIVE_STATUSES.has(rawStatus as BundleStatus)) {\n throw new BundleParseError(\n `Bundle is not active: status=${rawStatus ?? \"null\"}`,\n \"inactive\",\n );\n }\n const status = rawStatus as BundleStatus;\n\n const startsAt = fieldMap.get(\"starts_at\")?.value ?? null;\n const endsAt = fieldMap.get(\"ends_at\")?.value ?? null;\n const now = new Date();\n\n if (startsAt) {\n const start = new Date(startsAt);\n if (Number.isNaN(start.getTime())) {\n throw new BundleParseError(\n `Invalid starts_at: ${startsAt}`,\n \"invalid_type\",\n );\n }\n if (start > now) {\n throw new BundleParseError(\n `Bundle not yet started: starts_at=${startsAt}`,\n \"not_started\",\n );\n }\n }\n if (endsAt) {\n const end = new Date(endsAt);\n if (Number.isNaN(end.getTime())) {\n throw new BundleParseError(\n `Invalid ends_at: ${endsAt}`,\n \"invalid_type\",\n );\n }\n if (end < now) {\n throw new BundleParseError(\n `Bundle has expired: ends_at=${endsAt}`,\n \"expired\",\n );\n }\n }\n\n const products = resolveProducts(fieldMap);\n const discountConfig = parseDiscountConfig(fieldMap);\n const widgetConfig = parseWidgetConfig(fieldMap);\n\n const base = {\n id: metaobjectId,\n title,\n description: fieldMap.get(\"description\")?.value || null,\n status,\n products,\n discountConfig,\n widgetConfig,\n startsAt,\n endsAt,\n discountLabel: fieldMap.get(\"discount_label\")?.value ?? null,\n selectedVariantIds: parseSelectedVariantIds(fieldMap),\n productQuantities: parseNumericRecord(fieldMap, \"product_quantities\"),\n variantQuantities: parseNumericRecord(fieldMap, \"variant_quantities\"),\n marketVisibility: parseMarketVisibility(fieldMap),\n marketIds: parseMarketIds(fieldMap),\n countryCodes: parseStringArrayField(fieldMap, \"country_codes\"),\n companyLocationIds: parseStringArrayField(fieldMap, \"company_location_ids\"),\n abTestId: fieldMap.get(\"ab_test_id\")?.value || null,\n abWeight: parseAbWeight(fieldMap),\n };\n\n switch (bundleType) {\n case \"fixed\": {\n const result: FixedBundleData = { ...base, bundleType: \"fixed\" };\n return result;\n }\n case \"volume\": {\n const result: VolumeBundleData = {\n ...base,\n bundleType: \"volume\",\n volumeTiers: parseVolumeTiers(fieldMap),\n };\n return result;\n }\n case \"mix_match\": {\n const result: MixMatchBundleData = {\n ...base,\n bundleType: \"mix_match\",\n minQuantity: parseIntField(fieldMap, \"min_quantity\", { min: 1 }),\n maxQuantity: null,\n productRules: parseRuleMap(fieldMap, \"product_rules\"),\n variantRules: parseRuleMap(fieldMap, \"variant_rules\"),\n };\n return result;\n }\n case \"bogo\": {\n const result: BogoBundleData = {\n ...base,\n bundleType: \"bogo\",\n ...parseBogoConfig(fieldMap),\n };\n return result;\n }\n case \"multi_step\": {\n const result: MultiStepBundleData = {\n ...base,\n bundleType: \"multi_step\",\n steps: parseMultiSteps(fieldMap),\n productRules: parseRuleMap(fieldMap, \"product_rules\"),\n variantRules: parseRuleMap(fieldMap, \"variant_rules\"),\n collectionProductIds: parseCollectionProductIds(fieldMap),\n };\n return result;\n }\n }\n}\n\n/**\n * Parse the `steps` JSON metaobject field into `ParsedMultiStep[]`.\n * Fail-closed like `parseBogoConfig`: a missing / non-array / empty `steps`\n * field makes the bundle unrenderable (`invalid_type`) — a multi-step\n * bundle without steps has no picker to walk. Per-step hygiene:\n * - entries that aren't objects, or whose `name` isn't a string, are\n * dropped as malformed;\n * - `minQuantity` clamps to >= 1 (default 1);\n * - `maxQuantity` is null (unlimited) unless a finite number, and never\n * drops below the clamped minimum;\n * - `productIds` / `collectionIds` keep string entries only.\n */\nfunction parseMultiSteps(\n fieldMap: Map<string, MetaobjectField>,\n): ParsedMultiStep[] {\n const raw = parseJsonField(fieldMap, \"steps\");\n if (!Array.isArray(raw)) {\n throw new BundleParseError(\"Invalid or missing steps\", \"invalid_type\");\n }\n const stringList = (v: unknown): string[] =>\n Array.isArray(v) ? v.filter((x): x is string => typeof x === \"string\") : [];\n const steps: ParsedMultiStep[] = [];\n for (const entry of raw) {\n if (!entry || typeof entry !== \"object\" || Array.isArray(entry)) continue;\n const s = entry as Record<string, unknown>;\n if (typeof s.name !== \"string\") continue;\n const minRaw = typeof s.minQuantity === \"number\" ? s.minQuantity : Number(s.minQuantity);\n const minQuantity =\n Number.isFinite(minRaw) && minRaw >= 1 ? Math.floor(minRaw) : 1;\n let maxQuantity: number | null = null;\n if (typeof s.maxQuantity === \"number\" && Number.isFinite(s.maxQuantity)) {\n maxQuantity = Math.max(minQuantity, Math.floor(s.maxQuantity));\n }\n steps.push({\n name: s.name,\n minQuantity,\n maxQuantity,\n productIds: stringList(s.productIds),\n collectionIds: stringList(s.collectionIds),\n });\n }\n if (steps.length === 0) {\n throw new BundleParseError(\n \"multi_step bundle has no valid steps\",\n \"invalid_type\",\n );\n }\n return steps;\n}\n\n/**\n * Record exact per-collection membership while the `collection` field's\n * references are in hand: each collection node carries its own `products`\n * connection, so collection GID → product GIDs attribution is lossless.\n * Consumed by `productsForStep` to scope each wizard step's product pool.\n */\nfunction parseCollectionProductIds(\n fieldMap: Map<string, MetaobjectField>,\n): Record<string, string[]> {\n const out: Record<string, string[]> = {};\n const collectionField = fieldMap.get(\"collection\");\n if (!collectionField?.references?.nodes) return out;\n for (const node of collectionField.references.nodes) {\n if (!(\"products\" in node) || !node.products?.nodes) continue;\n out[node.id] = node.products.nodes.map((p) => p.id);\n }\n return out;\n}\n\n/**\n * Parse the `bogo_config` JSON metaobject field. Both product GIDs are\n * required — a malformed or missing config makes the bundle unrenderable\n * (invalid_type), matching the fail-closed posture of the discount\n * function. Quantities clamp to 1..99 with a default of 1.\n */\nfunction parseBogoConfig(fieldMap: Map<string, MetaobjectField>): {\n buyProductId: string;\n getProductId: string;\n buyQuantity: number;\n getQuantity: number;\n buyVariantIds: string[];\n getVariantIds: string[];\n} {\n const raw = parseJsonField(fieldMap, \"bogo_config\");\n if (!raw || typeof raw !== \"object\" || Array.isArray(raw)) {\n throw new BundleParseError(\"Invalid or missing bogo_config\", \"invalid_type\");\n }\n const cfg = raw as Record<string, unknown>;\n const buyProductId = typeof cfg.buyProductId === \"string\" ? cfg.buyProductId : \"\";\n const getProductId = typeof cfg.getProductId === \"string\" ? cfg.getProductId : \"\";\n if (!buyProductId || !getProductId) {\n throw new BundleParseError(\"bogo_config is missing buy/get product ids\", \"invalid_type\");\n }\n const clampQty = (v: unknown): number => {\n const n = typeof v === \"number\" ? v : Number(v);\n if (!Number.isFinite(n) || n < 1) return 1;\n return Math.min(99, Math.floor(n));\n };\n // Per-side variant narrowing (issue #436): positional selected_variant_ids\n // holds the union for a same-product bundle, so each side carries its own\n // list. Empty (or malformed) = that side is not narrowed.\n const sideVariants = (v: unknown): string[] =>\n Array.isArray(v) ? v.filter((id): id is string => typeof id === \"string\") : [];\n return {\n buyProductId,\n getProductId,\n buyQuantity: clampQty(cfg.buyQuantity),\n getQuantity: clampQty(cfg.getQuantity),\n buyVariantIds: sideVariants(cfg.buyVariantIds),\n getVariantIds: sideVariants(cfg.getVariantIds),\n };\n}\n\nfunction resolveProducts(fieldMap: Map<string, MetaobjectField>): Product[] {\n const products: Product[] = [];\n\n const productsField = fieldMap.get(\"products\");\n if (productsField?.reference && \"variants\" in productsField.reference) {\n products.push(productsField.reference as Product);\n }\n\n if (productsField?.references?.nodes) {\n for (const node of productsField.references.nodes) {\n if (\"variants\" in node) {\n products.push(node as Product);\n } else if (\"products\" in node && node.products?.nodes) {\n products.push(...node.products.nodes);\n }\n }\n }\n\n const collectionField = fieldMap.get(\"collection\");\n if (collectionField?.references?.nodes) {\n for (const node of collectionField.references.nodes) {\n if (\"products\" in node && node.products?.nodes) {\n products.push(...node.products.nodes);\n }\n }\n }\n\n return products;\n}\n\nfunction parseDiscountConfig(\n fieldMap: Map<string, MetaobjectField>,\n): DiscountConfig {\n return {\n discountType:\n (fieldMap.get(\"discount_type\")?.value as \"percentage\" | \"fixed_amount\") ??\n \"percentage\",\n discountValue: parseFloat(fieldMap.get(\"discount_value\")?.value ?? \"0\"),\n allowStacking: fieldMap.get(\"allow_stacking\")?.value === \"true\",\n };\n}\n\n/**\n * Parse the merchant's widget_config metaobject field. The JSON is authored\n * by the Zod schema in app/lib/bundle-types.ts, so consumers can trust the\n * shape — but old bundles may be missing fields added after they were\n * created. mergeWidgetConfig fills those gaps from WIDGET_CONFIG_DEFAULTS,\n * so every renderer downstream gets a fully populated config.\n */\nfunction parseWidgetConfig(\n fieldMap: Map<string, MetaobjectField>,\n): WidgetConfig {\n return mergeWidgetConfig(parseJsonField(fieldMap, \"widget_config\"));\n}\n\n/**\n * Parse `selected_variant_ids`: a list of per-product variant-GID arrays,\n * positionally aligned with `products`. Returns null when the merchant\n * didn't restrict variants (any entry means full list allowed).\n */\nfunction parseSelectedVariantIds(\n fieldMap: Map<string, MetaobjectField>,\n): string[][] | null {\n const raw = parseJsonField(fieldMap, \"selected_variant_ids\");\n if (!Array.isArray(raw)) return null;\n const result: string[][] = [];\n for (const entry of raw) {\n if (Array.isArray(entry)) {\n result.push(\n entry.filter((x): x is string => typeof x === \"string\"),\n );\n } else {\n result.push([]);\n }\n }\n return result;\n}\n\n/**\n * Parse a JSON record of `{ [GID]: number }`. Used for\n * `product_quantities` and `variant_quantities`. Non-numeric values are\n * dropped; returns an empty object when the field is absent or malformed.\n */\nfunction parseNumericRecord(\n fieldMap: Map<string, MetaobjectField>,\n key: string,\n): Record<string, number> {\n const raw = parseJsonField(fieldMap, key);\n // Arrays pass `typeof === \"object\"`. Reject them explicitly so a\n // malformed metaobject field (e.g. `[1, 2, 3]` where an object was\n // expected) doesn't produce a `{ \"0\": 1, \"1\": 2, ... }` record.\n if (!raw || typeof raw !== \"object\" || Array.isArray(raw)) return {};\n const record: Record<string, number> = {};\n for (const [k, v] of Object.entries(raw)) {\n const num = typeof v === \"number\" ? v : Number(v);\n // Defense-in-depth: Zod enforces 0/1–99 at the write path. The parser\n // also clamps at read time so a metafield that bypasses Zod (seed script,\n // direct Admin API edit, manual Shopify CLI) can't feed `Number.MAX_SAFE_INTEGER`\n // downstream into `CartLineInput.quantity` — which would crash Hydrogen\n // or Storefront Cart API with an opaque error. Zero is preserved\n // (merchant-set opt-out); renderers filter zero-qty rows. Negative and\n // out-of-range values are dropped as malformed.\n if (Number.isFinite(num) && num >= 0 && num <= 99) {\n record[k] = Math.floor(num);\n }\n }\n return record;\n}\n\n/**\n * Parse a JSON rule-map metaobject field into a record of\n * `{ [gid]: { min, max, required? } }`. Drops malformed entries.\n * Clamps min/max to 1..99 and ensures `min <= max`. `required` is kept only\n * when strictly `true` (absent otherwise — no `false` noise). Returns an\n * empty object when the field is absent or unparseable.\n *\n * Shared by `product_rules` (keyed by product GID) and `variant_rules` (keyed\n * by variant GID). The two levels are the same shape by design — a variant\n * rule refines a product rule rather than being a different kind of thing —\n * so the parse, the clamping and the malformed-entry handling are written\n * once.\n */\nfunction parseRuleMap(\n fieldMap: Map<string, MetaobjectField>,\n key: \"product_rules\" | \"variant_rules\",\n): Record<string, { min: number; max: number; required?: boolean }> {\n const raw = parseJsonField(fieldMap, key);\n if (!raw || typeof raw !== \"object\" || Array.isArray(raw)) return {};\n const out: Record<string, { min: number; max: number; required?: boolean }> =\n {};\n for (const [pid, rule] of Object.entries(raw)) {\n if (!rule || typeof rule !== \"object\" || Array.isArray(rule)) continue;\n const { min, max, required } = rule as {\n min?: unknown;\n max?: unknown;\n required?: unknown;\n };\n const minN = typeof min === \"number\" ? min : Number(min);\n const maxN = typeof max === \"number\" ? max : Number(max);\n if (!Number.isFinite(minN) || !Number.isFinite(maxN)) continue;\n const minClamped = Math.max(1, Math.min(99, Math.floor(minN)));\n const maxClamped = Math.max(minClamped, Math.min(99, Math.floor(maxN)));\n out[pid] =\n required === true\n ? { min: minClamped, max: maxClamped, required: true }\n : { min: minClamped, max: maxClamped };\n }\n return out;\n}\n\nfunction parseMarketVisibility(\n fieldMap: Map<string, MetaobjectField>,\n): \"all\" | \"specific\" {\n const raw = fieldMap.get(\"market_visibility\")?.value;\n return raw === \"specific\" ? \"specific\" : \"all\";\n}\n\nfunction parseMarketIds(fieldMap: Map<string, MetaobjectField>): string[] {\n return parseStringArrayField(fieldMap, \"markets\");\n}\n\n/** Shared shape for the three market-scoping list fields: a json field\n * holding an array of strings, absent on pre-projection metaobjects. */\nfunction parseStringArrayField(\n fieldMap: Map<string, MetaobjectField>,\n key: string,\n): string[] {\n const raw = fieldMap.get(key)?.value;\n if (!raw) return [];\n try {\n const parsed = JSON.parse(raw);\n if (!Array.isArray(parsed)) return [];\n return parsed.filter((v): v is string => typeof v === \"string\");\n } catch {\n return [];\n }\n}\n\n/**\n * This side's share of traffic, 0–100.\n *\n * Clamped rather than trusted: the field is merchant-adjacent data coming back\n * from a metaobject, and a weight outside the range would make `isInBucket`\n * silently always- or never-true.\n */\nfunction parseAbWeight(fieldMap: Map<string, MetaobjectField>): number {\n const raw = fieldMap.get(\"ab_weight\")?.value;\n if (!raw) return 0;\n const parsed = Number.parseInt(raw, 10);\n if (Number.isNaN(parsed)) return 0;\n return Math.min(100, Math.max(0, parsed));\n}\n\nfunction parseVolumeTiers(\n fieldMap: Map<string, MetaobjectField>,\n): VolumeTier[] {\n const raw = parseJsonField(fieldMap, \"volume_tiers\");\n if (!Array.isArray(raw)) return [];\n return raw\n .filter(\n (t): t is Record<string, unknown> => typeof t === \"object\" && t !== null,\n )\n .map(parseOneVolumeTier);\n}\n\n// Volume tiers are stored as `{ minQuantity, percentage?, amount? }` on the\n// metaobject (see admin VolumeTierSchema in app/lib/bundle-types.ts). The\n// parent bundle's `discountConfig.discountType` tells renderers which of the\n// two magnitude fields to use for each tier.\nfunction parseOneVolumeTier(t: Record<string, unknown>): VolumeTier {\n const minQty = Number(t.minQuantity ?? 0);\n const tier: VolumeTier = {\n // Reject NaN / Infinity / negatives — same discipline as\n // percentage/amount below. Malformed tiers collapse to 0 so the\n // renderer's bounds checks can drop them deterministically.\n minQuantity: Number.isFinite(minQty) && minQty >= 0 ? minQty : 0,\n };\n if (typeof t.percentage === \"number\" && Number.isFinite(t.percentage)) {\n tier.percentage = t.percentage;\n }\n if (typeof t.amount === \"number\" && Number.isFinite(t.amount)) {\n tier.amount = t.amount;\n }\n return tier;\n}\n\nfunction parseJsonField(\n fieldMap: Map<string, MetaobjectField>,\n key: string,\n): unknown {\n const value = fieldMap.get(key)?.value;\n if (!value) return null;\n try {\n return JSON.parse(value);\n } catch {\n return null;\n }\n}\n\nfunction parseIntField(\n fieldMap: Map<string, MetaobjectField>,\n key: string,\n options: { min?: number } = {},\n): number | null {\n const value = fieldMap.get(key)?.value;\n if (!value) return null;\n const num = parseInt(value, 10);\n if (isNaN(num)) return null;\n if (options.min !== undefined && num < options.min) return null;\n return num;\n}\n","/**\n * Store→presentment currency conversion for merchant-configured amounts.\n *\n * Merchant-configured money (fixed_amount values, flat_price targets,\n * volume tier amounts) is stored in the shop's STORE currency, while every\n * price the SDK renders comes from the Storefront API in the buyer's\n * presentment currency. The checkout-side Rust discount function converts\n * configured amounts by its input's `presentmentCurrencyRate`; a headless\n * preview that skips the same conversion quotes a sale price the checkout\n * won't charge on any \"local currencies\" market.\n *\n * The rate itself is consumer-supplied — a headless storefront knows its\n * own currency context (Hydrogen's `localization`, a cart's\n * `presentmentCurrencyRate`, or a custom FX source); the SDK cannot guess\n * it. Percentages are currency-agnostic and never converted.\n */\nimport type { ParsedBundle } from \"./types\";\n\n/**\n * Clamp a consumer-supplied rate to something safe to multiply by.\n *\n * Mirrors the Rust function's `presentment_rate`: anything non-finite or\n * not strictly positive falls back to 1 — no conversion is the safest\n * failure mode. Accepts strings because Shopify surfaces the rate as one\n * (`window.Shopify.currency.rate`, Money scalar fields).\n */\nexport function normalizeCurrencyRate(\n rate: number | string | null | undefined,\n): number {\n const num = typeof rate === \"string\" ? parseFloat(rate) : rate;\n return typeof num === \"number\" && Number.isFinite(num) && num > 0 ? num : 1;\n}\n\n/**\n * Return a copy of `bundle` whose merchant-configured store-currency\n * amounts are converted to presentment currency at `rate`.\n *\n * Converts `discountConfig.discountValue` for every non-percentage\n * discount type (fixed_amount today, plus flat_price which reaches\n * runtime through the parser's pass-through) and each volume tier's\n * `amount`. Amounts stay in unrounded major units — rendering code\n * rounds to cents at the same point it always has, matching the Rust\n * function, which multiplies unrounded f64 majors.\n *\n * A rate of 1 (or anything `normalizeCurrencyRate` rejects) returns the\n * input object unchanged, so store-currency callers pay nothing.\n */\nexport function convertBundleToPresentment<T extends ParsedBundle>(\n bundle: T,\n rate: number | string | null | undefined,\n): T {\n const r = normalizeCurrencyRate(rate);\n if (r === 1) return bundle;\n\n const converted = { ...bundle };\n\n if (bundle.discountConfig.discountType !== \"percentage\") {\n converted.discountConfig = {\n ...bundle.discountConfig,\n discountValue: bundle.discountConfig.discountValue * r,\n };\n }\n\n if (\"volumeTiers\" in bundle && Array.isArray(bundle.volumeTiers)) {\n (converted as { volumeTiers: typeof bundle.volumeTiers }).volumeTiers =\n bundle.volumeTiers.map((tier) =>\n typeof tier.amount === \"number\"\n ? { ...tier, amount: tier.amount * r }\n : tier,\n );\n }\n\n return converted;\n}\n","/**\n * Product-aware bundle fetch — the headless analogue of the classic Liquid\n * theme block.\n *\n * Classic-theme widget reads `product.metafields[\"$app\"].active_bundle_ids`\n * inside Liquid and renders every active bundle that applies. Headless\n * storefronts get the same behaviour via this helper: pass a product\n * handle, receive `ParsedBundle[]` for every active bundle configured\n * against that product.\n *\n * The caller is responsible for knowing the handle — either because their\n * storefront template already knows it (Hydrogen loader, Next.js param),\n * or because the widget auto-detects from `/products/<handle>` URLs.\n *\n * Non-active bundles (draft / inactive / scheduled / expired) are silently\n * skipped rather than throwing — the calling surface is a list, not a\n * single lookup. Surface errors only for genuine failures (network /\n * GraphQL / invalid schema).\n */\nimport { createStorefrontClient } from \"../storefront-api/client\";\nimport {\n BUNDLES_FOR_PRODUCT_QUERY,\n type BundlesForProductResponse,\n} from \"../storefront-api/queries\";\nimport { parseMetaobjectBundleStrict } from \"./parser\";\nimport { convertBundleToPresentment } from \"./presentment\";\nimport { BundleParseError, type ParsedBundle } from \"./types\";\nimport type { MetaobjectField } from \"../storefront-api/types\";\n\nexport interface FetchBundlesForProductOptions {\n shopDomain: string;\n storefrontAccessToken: string;\n productHandle: string;\n /** Forwarded as `Shopify-Storefront-Buyer-IP` — required for SSR. */\n buyerIp?: string;\n signal?: AbortSignal;\n apiVersion?: string;\n /**\n * Store→presentment currency rate. Merchant-configured amounts\n * (fixed_amount / flat_price values, volume tier amounts) are stored in\n * the shop's store currency; pass the rate for the buyer's currency so\n * quoted sale prices match what the checkout's discount function\n * charges. Omit (or pass 1) on store-currency storefronts. See\n * `convertBundleToPresentment`.\n */\n currencyRate?: number;\n}\n\n/**\n * Reasons a bundle is expected-not-renderable (skipped silently, not\n * surfaced as an error). Matches `BundleParseFailReason` minus\n * `not_found`, which indicates a genuine data problem worth propagating.\n *\n * `invalid_type` is skippable for forward compatibility: when the app\n * ships a bundle type this SDK version doesn't know yet (as happened when\n * \"bogo\" was added), the unknown bundle is skipped rather than failing the\n * whole product's bundle list.\n */\nconst SKIPPABLE_REASONS = new Set([\"inactive\", \"not_started\", \"expired\", \"invalid_type\"]);\n\nexport async function fetchBundlesForProduct(\n options: FetchBundlesForProductOptions,\n): Promise<ParsedBundle[]> {\n const client = createStorefrontClient({\n shopDomain: options.shopDomain,\n accessToken: options.storefrontAccessToken,\n buyerIp: options.buyerIp,\n apiVersion: options.apiVersion,\n });\n\n const data = await client.query<BundlesForProductResponse>(\n BUNDLES_FOR_PRODUCT_QUERY,\n { handle: options.productHandle },\n { signal: options.signal },\n );\n\n if (!data.product) {\n // Product not found for this handle. The widget's caller (merchant's\n // storefront) is presumably on a valid product page, so this likely\n // means the handle is wrong or the product is unpublished. Return\n // empty rather than throwing — the widget renders nothing in that\n // case, which is the right UX (bundle section invisible).\n return [];\n }\n\n const refs = data.product.metafield?.references?.nodes ?? [];\n const bundles: ParsedBundle[] = [];\n\n for (const ref of refs) {\n try {\n // The nested references query resolves products + collections to the\n // same shape BUNDLE_METAOBJECT_QUERY returns, so parser.ts doesn't\n // need a new code path.\n const bundle = parseMetaobjectBundleStrict(\n ref.id,\n ref.fields as MetaobjectField[],\n );\n bundles.push(convertBundleToPresentment(bundle, options.currencyRate));\n } catch (err) {\n if (err instanceof BundleParseError && SKIPPABLE_REASONS.has(err.reason)) {\n continue;\n }\n throw err;\n }\n }\n\n return bundles;\n}\n","// Volume tier savings calculation. `discountType` lives on the parent\n// bundle (bundle.discountConfig.discountType); each tier carries the\n// magnitude in either `percentage` (whole-number, e.g. 10 = 10%) or\n// `amount` (currency units, e.g. 5.00 = $5 off per unit).\nimport type { DiscountConfig, VolumeTier } from \"./types\";\n\nexport interface TierSavings {\n tier: VolumeTier;\n unitPrice: number;\n savings: number;\n savingsPercent: number;\n isActive: boolean;\n}\n\n/**\n * Calculate savings for each tier at a given base price and quantity.\n *\n * Output order matches input order. Tier order is merchant-controlled in the\n * admin and is the storefront display order, so renderers can map the result\n * positionally onto `bundle.volumeTiers`.\n */\nexport function calculateTierSavings(\n tiers: VolumeTier[],\n basePrice: number,\n currentQuantity: number,\n discountType: DiscountConfig[\"discountType\"],\n): TierSavings[] {\n // Work in integer cents with the discount floored per unit — the same\n // arithmetic the Liquid widget and the checkout apply. Float-dollar math\n // here drifted a cent from the theme ($902.45 vs $902.46 on a 5% tier of\n // $949.95), and two surfaces showing different unit prices for the same\n // tier reads as a pricing bug to the shopper.\n const baseCents = Math.round(basePrice * 100);\n return tiers.map((tier) => {\n const discountCents =\n discountType === \"percentage\"\n ? Math.floor((baseCents * (tier.percentage ?? 0)) / 100)\n : Math.round((tier.amount ?? 0) * 100);\n\n const unitCents = Math.max(0, baseCents - discountCents);\n const unitPrice = unitCents / 100;\n const savings = (baseCents - unitCents) / 100;\n const savingsPercent =\n baseCents > 0 ? ((baseCents - unitCents) / baseCents) * 100 : 0;\n const isActive = currentQuantity >= tier.minQuantity;\n\n return { tier, unitPrice, savings, savingsPercent, isActive };\n });\n}\n\n/**\n * Get the active tier for a given quantity.\n * Returns the highest tier where quantity >= minQuantity.\n * Order-independent: matches how the discount function picks a tier.\n */\nexport function getActiveTier(\n tiers: VolumeTier[],\n quantity: number,\n): VolumeTier | null {\n const sorted = [...tiers].sort((a, b) => b.minQuantity - a.minQuantity);\n return sorted.find((t) => quantity >= t.minQuantity) ?? null;\n}\n\n/**\n * Smallest `minQuantity` across tiers — the floor below which no tier can be\n * bought. Renderers use it to decide whether a variant has enough stock to be\n * worth showing. Derived with a min rather than read off tiers[0] because tier\n * order is the merchant's display order, not a quantity ranking.\n */\nexport function getMinTierQuantity(tiers: VolumeTier[]): number {\n if (tiers.length === 0) return 1;\n return Math.min(...tiers.map((t) => t.minQuantity));\n}\n\n/** The tier fields the index-resolution helpers need. */\nexport interface TierDiscountValue {\n percentage?: number | null;\n amount?: number | null;\n}\n\n/**\n * Index of the tier with the largest savings value, first one winning ties.\n * Mirrors the `best_index` loop in `lb-volume.liquid` (strict `>` — the\n * earliest maximum keeps the crown), including its rule that fixed-amount\n * bundles rank by `amount` and percentage bundles by `percentage`.\n *\n * Shared by the \"best_value\" default tier and the popular badge's fallback\n * placement, so every surface lands on the same tier.\n */\nexport function bestValueTierIndex(\n tiers: TierDiscountValue[],\n discountType: string,\n): number {\n let best = 0;\n let bestIdx = 0;\n for (let i = 0; i < tiers.length; i++) {\n const savings =\n discountType === \"fixed_amount\"\n ? tiers[i].amount ?? 0\n : tiers[i].percentage ?? 0;\n if (savings > best) {\n best = savings;\n bestIdx = i;\n }\n }\n return bestIdx;\n}\n\n/**\n * Resolve the merchant's `defaultTier` setting to a tier index, exactly as\n * `lb-volume.liquid` does: `\"best_value\"` takes the best-savings index,\n * `\"first\"` (and anything non-numeric, which Liquid's `plus: 0` coerces to 0)\n * takes 0, and a number is clamped into `[0, tierCount - 1]`.\n */\nexport function resolveDefaultTierIndex(\n defaultTier: unknown,\n tierCount: number,\n bestIndex: number,\n): number {\n if (tierCount <= 0) return 0;\n if (defaultTier === \"best_value\") return Math.min(bestIndex, tierCount - 1);\n const n = typeof defaultTier === \"number\" ? defaultTier : 0;\n return Math.max(0, Math.min(Math.floor(n), tierCount - 1));\n}\n\n/**\n * Resolve where the \"Most popular\" badge sits: the pinned `tierIndex` when the\n * merchant set one, else the best-savings tier. Returns -1 when the badge is\n * hidden. A pinned index is deliberately NOT clamped — Liquid compares it\n * against each row's index, so an out-of-range pin renders no badge rather\n * than a wrong one, and every surface must fail the same way.\n */\nexport function resolvePopularTierIndex(\n tierIndex: unknown,\n visible: boolean,\n bestIndex: number,\n): number {\n if (!visible) return -1;\n if (typeof tierIndex === \"number\" && Number.isFinite(tierIndex)) {\n return Math.floor(tierIndex);\n }\n return bestIndex;\n}\n","// Market visibility gate, shared by every storefront consumer.\n//\n// Mirrors the checkout-side gate in the Rust discount function: a bundle\n// scoped to specific markets is shown when the buyer's country matches the\n// projected `countryCodes` (retail markets) OR the buyer is purchasing for\n// a company location in the projected `companyLocationIds` (B2B markets).\n// Either list may carry the \"*\" wildcard, projected from markets whose\n// condition targets all regions / all company locations.\n//\n// Market GIDs are matched too, for callers that still pass one — but they\n// are a legacy signal: Shopify deprecated market IDs for buyer targeting\n// (nested markets return only the most specific match), and a B2B buyer's\n// company-location market never surfaces through storefront localization\n// at all. Prefer supplying `countryCode` / `companyLocationId`.\n//\n// Mirrors the market gate in bundle-widget.liquid.\n\n/** The market scoping any bundle carries. Structural so it accepts a\n * `ParsedBundle` without widening the package's exported types. The two\n * projection lists are optional so pre-projection callers still compile. */\nexport interface MarketScoped {\n marketVisibility: \"all\" | \"specific\";\n marketIds: readonly string[];\n countryCodes?: readonly string[];\n companyLocationIds?: readonly string[];\n}\n\n/** Who is asking. All fields optional; supply what the host knows. */\nexport interface BuyerMarketContext {\n /** Legacy: Shopify Market id (GID or bare id) of the storefront context. */\n marketId?: string | null;\n /** ISO-3166-1 alpha-2 country code of the buyer, e.g. \"CA\". */\n countryCode?: string | null;\n /** CompanyLocation the buyer purchases for (GID or bare id), B2B only. */\n companyLocationId?: string | null;\n}\n\n/** Wildcard entry meaning \"any country\" / \"any company location\". */\nconst WILDCARD = \"*\";\n\n/** Trailing id of a GID-shaped identifier, accepting a GID or a bare id. */\nfunction idKey(id: string): string {\n return id.split(\"/\").pop() ?? id;\n}\n\nfunction listMatches(\n list: readonly string[] | undefined,\n candidate: string | null | undefined,\n compareByIdSuffix: boolean,\n): boolean {\n if (!list || list.length === 0) return false;\n if (list.includes(WILDCARD)) return true;\n if (!candidate) return false;\n if (!compareByIdSuffix) return list.includes(candidate);\n const key = idKey(candidate);\n return list.some((id) => idKey(id) === key);\n}\n\n/**\n * Whether a bundle should be shown to the buyer described by `context`.\n *\n * A bundle scoped to specific markets stays hidden when the caller supplies\n * no matching signal at all, since showing it would advertise a discount the\n * checkout-side gate will refuse to apply.\n *\n * The company-location wildcard requires the buyer to actually be purchasing\n * for a company location — it scopes the bundle to B2B buyers, not everyone.\n */\nexport function isVisibleToBuyer(\n bundle: MarketScoped,\n context: BuyerMarketContext,\n): boolean {\n if (bundle.marketVisibility !== \"specific\") return true;\n // Retail: buyer country vs projected country codes. The wildcard matches\n // even when the caller supplied no country — \"Worldwide\" means everyone.\n if (listMatches(bundle.countryCodes, context.countryCode, false)) {\n return true;\n }\n // B2B: purchasing company location vs projected locations. The wildcard\n // still requires a company location on the buyer.\n if (\n context.companyLocationId &&\n listMatches(bundle.companyLocationIds, context.companyLocationId, true)\n ) {\n return true;\n }\n // Legacy: direct market-id match for callers that only know the market.\n if (context.marketId) {\n const key = idKey(context.marketId);\n if (bundle.marketIds.some((id) => idKey(id) === key)) return true;\n }\n return false;\n}\n\n/**\n * Whether a bundle should be shown to a shopper in `marketId`.\n *\n * Legacy entry point — cannot see B2B buyers or country codes; prefer\n * `isVisibleToBuyer`. Kept because it is public API.\n */\nexport function isVisibleInMarket(\n bundle: MarketScoped,\n marketId: string | null | undefined,\n): boolean {\n return isVisibleToBuyer(bundle, { marketId });\n}\n","/**\n * Which of a bundle's products is the shopper actually looking at?\n *\n * A volume bundle can span several products, but the surface rendering it\n * sits on one product page and must price and sell that product — the theme\n * widget gets this for free from Liquid's `product` object. The headless\n * surfaces receive the same context explicitly (a `product-id` attribute on\n * `<lime-bundle>`, a `productId` prop on `<VolumeBundle>`) and resolve it\n * here, falling back to the bundle's first product only when the host never\n * said which product the shopper is on.\n */\n\nexport interface ProductContext {\n /**\n * The current product's id — either the full GID\n * (`gid://shopify/Product/123`) or the bare numeric id (`\"123\"`).\n */\n productId?: string | null;\n /** The current product's handle, e.g. from the page URL. */\n productHandle?: string | null;\n}\n\n/** True when `ref` (a full GID or bare numeric id) names the product `gid`. */\nfunction productIdMatches(gid: string, ref: string): boolean {\n if (gid === ref) return true;\n return /^\\d+$/.test(ref) && gid.endsWith(`/${ref}`);\n}\n\n/**\n * Resolve the product a bundle surface should price and sell.\n *\n * Precedence: explicit `productId` → `productHandle` → the bundle's first\n * product. A context that names a product outside the bundle falls through\n * to the next source rather than rendering nothing — the merchant may have\n * embedded the widget on an unrelated page, and the first-product fallback\n * is the documented pre-context behaviour.\n *\n * Returns `undefined` only when the bundle has no products at all.\n */\nexport function resolveContextProduct<\n P extends { id: string; handle: string },\n>(products: readonly P[], context?: ProductContext): P | undefined {\n const id = context?.productId?.trim();\n if (id) {\n const byId = products.find((p) => productIdMatches(p.id, id));\n if (byId) return byId;\n }\n\n const handle = context?.productHandle?.trim();\n if (handle) {\n const byHandle = products.find((p) => p.handle === handle);\n if (byHandle) return byHandle;\n }\n\n return products[0];\n}\n","/**\n * Quantity constraint validation for mix-and-match bundles.\n */\n\nexport interface QuantityValidation {\n valid: boolean;\n totalQuantity: number;\n message: string | null;\n}\n\nexport function validateQuantity(\n totalQuantity: number,\n minQuantity: number | null,\n maxQuantity: number | null,\n): QuantityValidation {\n if (minQuantity !== null && totalQuantity < minQuantity) {\n return {\n valid: false,\n totalQuantity,\n message: `Select at least ${minQuantity} item${minQuantity !== 1 ? \"s\" : \"\"}`,\n };\n }\n if (maxQuantity !== null && totalQuantity > maxQuantity) {\n return {\n valid: false,\n totalQuantity,\n message: `Select at most ${maxQuantity} item${maxQuantity !== 1 ? \"s\" : \"\"}`,\n };\n }\n return { valid: true, totalQuantity, message: null };\n}\n","/**\n * Canonical resolver for a bundle's per-line quantity.\n *\n * Bundle configs carry two qty fields: `productQuantities` (keyed by product\n * GID) and `variantQuantities` (keyed by variant GID, overrides product-level\n * when the customer picks that variant). The admin form strips product-level\n * entries that equal the default 1, so downstream consumers must consult\n * variantQuantities first and fall back to productQuantities then to 1.\n *\n * Before this helper existed, the fallback chain was reimplemented in five\n * places (React FixedBundle, React MixMatchBundle, headless widget fixed\n * renderer, Liquid mix-match IIFE, and the Rust discount function). They\n * drifted — PR #147 fixed the resulting cart-line split. Use this helper\n * everywhere the config is available to keep the TS/React/widget runtimes\n * in lockstep. The Liquid IIFE and Rust function have their own\n * implementations pinned by parity tests.\n */\n\nexport interface BundleQtySource {\n variantQuantities: Record<string, number>;\n productQuantities: Record<string, number>;\n}\n\n/**\n * Returns the configured qty for a cart line: `variantQuantities[variantId]`\n * if present, else `productQuantities[productId]`, else 1.\n *\n * Using `!== undefined` (not `??`) for the variant check so that a merchant\n * explicitly setting `0` is honoured — caller is expected to filter zero-qty\n * lines out, matching the headless widget's opt-out semantics.\n */\nexport function resolveBundleQty(\n bundle: BundleQtySource,\n productId: string,\n variantId: string,\n): number {\n const vq = bundle.variantQuantities[variantId];\n if (vq !== undefined) return vq;\n return bundle.productQuantities[productId] ?? 1;\n}\n","/**\n * Pure helpers for multi-step bundles, shared by the widget renderer and\n * the React components so the wizard math lives in one place.\n */\nimport type { Product } from \"../storefront-api/types\";\nimport type { MultiStepBundleData } from \"./types\";\n\n/**\n * Open units left in a step: `Infinity` when the step has no maximum,\n * otherwise `max - unitsInStep` floored at 0. Mirrors `stepHeadroom` in\n * `extensions/bundle-theme/assets/bundle-multi-step.js`.\n */\nexport function stepHeadroom(\n maxQuantity: number | null,\n unitsInStep: number,\n): number {\n if (maxQuantity == null) return Number.POSITIVE_INFINITY;\n return Math.max(0, maxQuantity - unitsInStep);\n}\n\n/**\n * The products belonging to one wizard step, in the flat `bundle.products`\n * order (pinned union first, then collection-resolved products — the order\n * `resolveProducts` emits).\n *\n * A product belongs to the step when its GID is in the step's pinned\n * `productIds`, or when it was resolved from one of the step's\n * `collectionIds`. Collection attribution is EXACT: the parser records\n * `bundle.collectionProductIds` (collection GID → product GIDs) while\n * resolving the `collection` metaobject field, because each collection\n * reference node carries its own `products` connection. No client-side\n * collection-membership query is needed.\n *\n * Duplicates (a product both pinned to the step and present in one of its\n * collections, or repeated across the flat array) are deduped by product\n * id, keeping the first occurrence.\n */\nexport function productsForStep(\n bundle: MultiStepBundleData,\n stepIndex: number,\n): Product[] {\n const step = bundle.steps[stepIndex];\n if (!step) return [];\n\n const pinned = new Set(step.productIds);\n const fromCollections = new Set<string>();\n for (const collectionId of step.collectionIds) {\n for (const productId of bundle.collectionProductIds[collectionId] ?? []) {\n fromCollections.add(productId);\n }\n }\n\n const seen = new Set<string>();\n const result: Product[] = [];\n for (const product of bundle.products) {\n if (seen.has(product.id)) continue;\n if (!pinned.has(product.id) && !fromCollections.has(product.id)) continue;\n seen.add(product.id);\n result.push(product);\n }\n return result;\n}\n","/**\n * Analytics event reporter for headless widgets.\n * Fires events to /api/analytics with single retry on failure.\n */\n\nexport interface AnalyticsConfig {\n shopDomain: string;\n appUrl: string; // Base URL of the Lime Bundles app\n}\n\nexport async function reportImpression(\n config: AnalyticsConfig,\n event: {\n bundleGid: string;\n bundleType: string;\n productId?: string;\n },\n): Promise<void> {\n await sendEvent(config, {\n shopDomain: config.shopDomain,\n eventType: \"bundle_impression\",\n bundleGid: event.bundleGid,\n bundleType: event.bundleType,\n productId: event.productId,\n occurredAt: new Date().toISOString(),\n });\n}\n\nexport async function reportAddToCart(\n config: AnalyticsConfig,\n event: {\n bundleGid: string;\n bundleType: string;\n /**\n * Product GID the widget is rendered on. Optional because multi-product\n * bundles (fixed, mix-match) have no single \"page product\" — the event\n * is already attributed to the bundle via `bundleGid`. Volume bundles\n * typically pass the anchor product's GID. Previously this was required\n * and consumers defaulted to `products[0]?.id ?? \"\"` for multi-product\n * bundles, which was misleading.\n */\n productId?: string;\n quantity: number;\n totalPrice: number;\n },\n): Promise<void> {\n await sendEvent(config, {\n shopDomain: config.shopDomain,\n eventType: \"bundle_add_to_cart\",\n ...event,\n occurredAt: new Date().toISOString(),\n });\n}\n\n/**\n * IntersectionObserver-based impression tracking.\n * Fires once when element is 50% visible, then unobserves.\n */\nexport function observeImpression(\n element: Element,\n callback: () => void,\n): () => void {\n if (typeof IntersectionObserver === \"undefined\") {\n // Fallback: fire immediately if IntersectionObserver not available\n callback();\n return () => {};\n }\n\n const observer = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (entry.isIntersecting) {\n callback();\n observer.unobserve(entry.target);\n }\n }\n },\n { threshold: 0.5 },\n );\n\n observer.observe(element);\n return () => observer.disconnect();\n}\n\n// --- Internal ---\n\nasync function sendEvent(\n config: AnalyticsConfig,\n payload: Record<string, unknown>,\n): Promise<void> {\n const url = `${config.appUrl}/api/analytics`;\n const body = JSON.stringify(payload);\n\n try {\n const response = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body,\n });\n\n if (!response.ok) {\n // Single retry after 2 seconds\n await new Promise((r) => setTimeout(r, 2000));\n await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body,\n });\n }\n } catch {\n // Fire-and-forget — never block UX\n try {\n // Use sendBeacon as last resort\n if (typeof navigator !== \"undefined\" && navigator.sendBeacon) {\n navigator.sendBeacon(url, body);\n }\n } catch {\n // Silently drop\n }\n }\n}\n","/**\n * CSS sanitization for merchant-authored custom CSS.\n *\n * Kept in lockstep with `app/lib/sanitize-css.ts`. When updating one,\n * update the other — the app-side sanitizer runs on merchant INPUT when\n * CSS is saved to the shop metafield, and this SDK-side sanitizer runs\n * on OUTPUT before the CSS is injected into a <style> tag on the\n * storefront. Both must enforce the same rules; a drift means either\n * the merchant's saved CSS fails to render on headless, or malicious\n * CSS sneaks through one side but not the other.\n *\n * 5-step pipeline:\n * 1. Strip comments (prevents keyword obfuscation like @im/**\\/port)\n * 2. Decode unicode escapes (prevents \\0040import → @import bypass)\n * 3. Strip HTML angle brackets (prevents </style><script> breakout)\n * 4. Reject blocked CSS features (@import, expression(), behavior, ...)\n * 5. Allowlist url() values (https://, relative, fragment only)\n */\n\nexport const MAX_CSS_LENGTH = 10_000;\n\nconst BLOCKED_PATTERNS: ReadonlyArray<[RegExp, string]> = [\n [/@import/i, \"@import rules\"],\n [/@charset/i, \"@charset declarations\"],\n [/expression\\s*\\(/i, \"CSS expressions\"],\n [/-moz-binding/i, \"-moz-binding\"],\n [/-webkit-binding/i, \"-webkit-binding\"],\n [/behavior\\s*:/i, \"behavior property\"],\n];\n\nconst SAFE_URL_VALUE = /^(https:|\\/[^/]|\\.\\/|\\.\\.\\/|#)/;\n\nexport type SanitizeResult =\n | { ok: true; css: string }\n | { ok: false; error: string };\n\nexport function sanitizeCustomCss(raw: string): SanitizeResult {\n if (raw.length > MAX_CSS_LENGTH) {\n return {\n ok: false,\n error: `CSS exceeds ${MAX_CSS_LENGTH.toLocaleString(\"en-US\")} character limit`,\n };\n }\n\n let css = raw.replace(/\\/\\*[\\s\\S]*?\\*\\//g, \"\");\n\n try {\n // 1. Decode hex unicode escapes: \\XXXXXX → codepoint. CSS lets authors\n // obfuscate keywords this way (e.g. `\\0040import` → `@import`).\n css = css.replace(/\\\\([0-9a-fA-F]{1,6})[ \\t\\n\\f]?/g, (_, hex) => {\n const codePoint = parseInt(hex, 16);\n if (codePoint < 0 || codePoint > 0x10ffff) return \"\\uFFFD\";\n return String.fromCodePoint(codePoint);\n });\n // 2. Fold backslash + newline (line continuation per CSS spec).\n css = css.replace(/\\\\\\r?\\n/g, \"\");\n // 3. Decode character escapes: \\c → c (backslash + non-hex non-newline).\n // Without this, `@\\i\\mport` would slip past our keyword blocklist.\n css = css.replace(/\\\\([^\\n\\r\\f0-9a-fA-F])/g, \"$1\");\n } catch {\n return {\n ok: false,\n error: \"CSS contains invalid unicode escape sequences\",\n };\n }\n\n css = css.replace(/</g, \"\").replace(/>/g, \"\");\n\n for (const [pattern, label] of BLOCKED_PATTERNS) {\n if (pattern.test(css)) {\n return { ok: false, error: `CSS contains blocked pattern: ${label}` };\n }\n }\n\n // url() extraction: match ANY opening paren → closing paren content,\n // regardless of whether quotes match. Previous regex used `\\1` backref\n // which allowed malformed inputs like `url('javascript:alert(1))` to\n // slip past (mismatched quote → no match → no validation). Now every\n // url(...) is extracted and validated.\n const urlPattern = /url\\s*\\(\\s*([\\s\\S]*?)\\s*\\)/gi;\n let urlMatch: RegExpExecArray | null;\n while ((urlMatch = urlPattern.exec(css)) !== null) {\n let urlValue = urlMatch[1].trim();\n // Strip a single matching or unmatched quote from either end.\n if (urlValue.startsWith(\"'\") || urlValue.startsWith('\"')) {\n urlValue = urlValue.slice(1);\n }\n if (urlValue.endsWith(\"'\") || urlValue.endsWith('\"')) {\n urlValue = urlValue.slice(0, -1);\n }\n urlValue = urlValue.trim();\n if (urlValue && !SAFE_URL_VALUE.test(urlValue)) {\n return {\n ok: false,\n error: \"CSS url() values must use https:// or relative paths\",\n };\n }\n }\n\n return { ok: true, css };\n}\n","/**\n * Merchant custom-CSS injection into the storefront DOM.\n *\n * The headless SDK fetches `$app:custom_css` from the shop metafield\n * alongside bundle data, runs the sanitizer, and injects the output as\n * a <style> tag scoped to a deterministic id. Dedup is by id — if a\n * merchant has two <lime-bundle> elements on the same page, only one\n * <style> tag appears.\n *\n * SSR-safe: no-op when `document` is undefined.\n */\nimport { sanitizeCustomCss } from \"./sanitize\";\n\nconst STYLE_ID_PREFIX = \"lb-custom-css-\";\n\n/**\n * Inject merchant CSS into the document head. Idempotent — calling\n * repeatedly with the same shop updates the existing <style> tag in\n * place rather than appending new ones.\n *\n * Returns true if CSS was injected (or updated), false if skipped\n * (SSR, empty CSS, or sanitization rejected the input).\n */\nexport function injectCustomCss(\n shopDomain: string,\n rawCss: string | null | undefined,\n): boolean {\n if (typeof document === \"undefined\") return false;\n if (!rawCss) return false;\n\n const sanitized = sanitizeCustomCss(rawCss);\n if (!sanitized.ok) return false;\n if (!sanitized.css.trim()) return false;\n\n const id = STYLE_ID_PREFIX + simpleHash(shopDomain);\n\n let style = document.getElementById(id) as HTMLStyleElement | null;\n if (!style) {\n style = document.createElement(\"style\");\n style.id = id;\n style.setAttribute(\"data-lime-bundles\", \"custom-css\");\n document.head.appendChild(style);\n }\n\n // Only touch textContent when it actually changed — avoids triggering\n // layout or style-recalc cycles on repeat invocations.\n if (style.textContent !== sanitized.css) {\n style.textContent = sanitized.css;\n }\n return true;\n}\n\n/** FNV-1a — short, fast, DOM-id-safe hash. Not cryptographic. */\nfunction simpleHash(input: string): string {\n let hash = 0x811c9dc5;\n for (let i = 0; i < input.length; i++) {\n hash ^= input.charCodeAt(i);\n hash = Math.imul(hash, 0x01000193);\n }\n return (hash >>> 0).toString(16);\n}\n","/**\n * Price formatting utilities.\n */\nimport type {\n Money,\n UnitPriceMeasurement,\n UnitPriceMeasurementUnit,\n} from \"../storefront-api/types\";\n\nexport function formatMoney(amount: string | number, currencyCode: string): string {\n const num = typeof amount === \"string\" ? parseFloat(amount) : amount;\n\n try {\n return new Intl.NumberFormat(undefined, {\n style: \"currency\",\n currency: currencyCode,\n }).format(num);\n } catch {\n return `${currencyCode} ${num.toFixed(2)}`;\n }\n}\n\nexport function calculateDiscount(\n price: number,\n discountType: \"percentage\" | \"fixed_amount\",\n discountValue: number,\n): number {\n if (discountType === \"percentage\") {\n return Math.max(0, price * (1 - discountValue / 100));\n }\n return Math.max(0, price - discountValue);\n}\n\nconst UNIT_LABEL: Record<UnitPriceMeasurementUnit, string> = {\n CL: \"cl\",\n CM: \"cm\",\n FLOZ: \"fl oz\",\n FT: \"ft\",\n FT2: \"ft²\",\n G: \"g\",\n GAL: \"gal\",\n IN: \"in\",\n ITEM: \"each\",\n KG: \"kg\",\n L: \"l\",\n LB: \"lb\",\n M: \"m\",\n M2: \"m²\",\n M3: \"m³\",\n MG: \"mg\",\n ML: \"ml\",\n MM: \"mm\",\n OZ: \"oz\",\n PT: \"pt\",\n QT: \"qt\",\n UNKNOWN: \"\",\n YD: \"yd\",\n};\n\n/**\n * Format a Storefront `unitPrice` + `unitPriceMeasurement` pair as a\n * compact display string (\"$0.50/100ml\", \"$12.99/kg\", \"$2.00/each\").\n *\n * Returns `null` when either input is absent — the merchant hasn't\n * configured unit pricing in the Shopify admin and no UI should render.\n *\n * The reference value is omitted when it equals 1 (\"$X.XX/kg\" rather\n * than \"$X.XX/1kg\") to match the format Shopify's `money` filter +\n * `unit_price_with_measurement` produce in Liquid themes.\n */\nexport function formatUnitPrice(\n unitPrice: Money | null | undefined,\n measurement: UnitPriceMeasurement | null | undefined,\n fallbackCurrency?: string,\n): string | null {\n if (!unitPrice || !measurement || measurement.referenceUnit === \"UNKNOWN\") {\n return null;\n }\n\n // Guard against malformed Money.amount strings — Storefront responses\n // are always valid decimals, but a non-finite number would silently\n // render \"$NaN\" via Intl.NumberFormat.format(NaN).\n const amount = parseFloat(unitPrice.amount);\n if (!Number.isFinite(amount)) return null;\n\n // `?? \"\"` + `if (!label)` is defence against Shopify adding enum values\n // we don't yet know about — the type-level lookup would still succeed\n // but the runtime value would be undefined.\n const label = UNIT_LABEL[measurement.referenceUnit] ?? \"\";\n if (!label) return null;\n\n const currency = unitPrice.currencyCode || fallbackCurrency || \"USD\";\n const formatted = formatMoney(amount, currency);\n const value = measurement.referenceValue;\n const measurementText = value === 1 ? label : `${value}${label}`;\n return `${formatted}/${measurementText}`;\n}\n","// Integer-cent arithmetic that matches Shopify's Discount Function\n// per-unit floor rounding — naive `total * (1 - discount)` drifts a\n// cent or two from what the customer actually pays.\nimport type { FixedBundleData, DiscountConfig } from \"../bundle/types\";\n\nexport interface PricingRow {\n /** Pre-discount unit price in cents. */\n unitCents: number;\n /** Quantity of this product in the bundle. */\n qty: number;\n /** Pre-discount line subtotal in cents (`unitCents * qty`). */\n lineCents: number;\n /** Variant's compare-at price in cents, if different from unit. */\n compareCents: number | null;\n}\n\nexport interface FixedBundlePricing {\n rows: PricingRow[];\n /** Pre-discount bundle total in cents. */\n totalCents: number;\n /** Post-discount bundle total in cents. */\n saleCents: number;\n /** Savings in cents — always `max(0, totalCents - saleCents)`. */\n savingsCents: number;\n currency: string;\n}\n\n/**\n * Parse a money amount (Storefront API returns major-unit strings like\n * \"749.95\") into integer cents. Safe on numbers, null, undefined.\n * Rounds to the nearest cent to absorb float-representation drift.\n */\nexport function parseCents(\n amount: string | number | null | undefined,\n): number {\n if (amount === null || amount === undefined) return 0;\n const num = typeof amount === \"string\" ? parseFloat(amount) : amount;\n if (!Number.isFinite(num)) return 0;\n return Math.round(num * 100);\n}\n\n/**\n * Format integer cents as localized currency. Falls back to\n * `CUR 12.34` when the runtime's Intl.NumberFormat rejects the code.\n */\nexport function formatCents(cents: number, currencyCode: string): string {\n try {\n return new Intl.NumberFormat(undefined, {\n style: \"currency\",\n currency: currencyCode,\n }).format(cents / 100);\n } catch {\n return `${currencyCode} ${(cents / 100).toFixed(2)}`;\n }\n}\n\n/**\n * Apply a percentage discount to a single unit with floor rounding —\n * matches the rounding Shopify's Discount Function performs at checkout.\n * For a bundle total, apply this per-unit and sum, rather than computing\n * a single discount on the totalled line — those paths can differ by a\n * cent or two when the per-unit price has odd cents.\n */\nexport function percentageDiscountUnit(\n unitCents: number,\n percent: number,\n): number {\n return Math.max(0, unitCents - Math.floor((unitCents * percent) / 100));\n}\n\n/**\n * Compute the full pricing snapshot for a fixed bundle. Handles\n * percentage and fixed-amount discount types and per-product quantities.\n *\n * `productQuantities` defaults to `bundle.productQuantities` (the\n * merchant-configured map); product IDs missing from that map fall back\n * to qty 1 via the `?? 1` in the row loop.\n *\n * The savings percentage that used to render as a colored header badge was\n * retired in the manifest-and-receipt redesign — savings are now stated once\n * on the receipt summary line (derive it from `savingsCents` / `totalCents`).\n */\nexport function computeFixedPricing(\n bundle: FixedBundleData,\n productQuantities: Record<string, number> = bundle.productQuantities,\n): FixedBundlePricing {\n const { discountType, discountValue } = bundle.discountConfig;\n const rows: PricingRow[] = [];\n let totalCents = 0;\n let saleCents = 0;\n\n for (const product of bundle.products) {\n const variant =\n product.variants.nodes.find((v) => v.availableForSale) ??\n product.variants.nodes[0];\n if (!variant) continue;\n\n const unitCents = parseCents(variant.price.amount);\n const qty = productQuantities[product.id] ?? 1;\n const lineCents = unitCents * qty;\n const compareCents = variant.compareAtPrice\n ? parseCents(variant.compareAtPrice.amount)\n : null;\n\n rows.push({ unitCents, qty, lineCents, compareCents });\n totalCents += lineCents;\n\n if (discountType === \"percentage\") {\n const perUnit = percentageDiscountUnit(unitCents, discountValue);\n saleCents += perUnit * qty;\n } else {\n saleCents += lineCents;\n }\n }\n\n if (discountType === \"fixed_amount\") {\n saleCents = Math.max(0, totalCents - Math.round(discountValue * 100));\n }\n\n const savingsCents = Math.max(0, totalCents - saleCents);\n const currency =\n bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? \"USD\";\n\n return { rows, totalCents, saleCents, savingsCents, currency };\n}\n\n/**\n * Pure helper: compute the sale-price total for an arbitrary set of\n * cart lines under a discount config. Useful for mix-match bundles\n * where the lines are user-selected rather than bundle-configured.\n * Applies percentage discount on the aggregated total (mix-match uses\n * `applies_to_each_item: false`), matching the Liquid block's behaviour.\n */\nexport function computeBundleSaleCents(\n totalCents: number,\n discount: DiscountConfig,\n): number {\n if (discount.discountType === \"percentage\") {\n const off = Math.floor((totalCents * discount.discountValue) / 100);\n return Math.max(0, totalCents - off);\n }\n return Math.max(0, totalCents - Math.round(discount.discountValue * 100));\n}\n","/**\n * Append Shopify CDN image-transform params to a product-image URL so the\n * browser downloads a correctly-sized asset. Mirrors what Liquid's\n * `image_url: width:..., height:..., crop:...` filter chain produces.\n * Non-Shopify CDNs silently ignore the extra params — graceful fallback.\n */\nexport interface ImageTransform {\n width?: number;\n height?: number;\n crop?: \"center\" | \"top\" | \"bottom\" | \"left\" | \"right\";\n}\n\n/**\n * Standard thumbnail size for bundle product rows and picker tiles.\n * Matches the Liquid theme's `image_url: width: 152, height: 152,\n * crop: 'center'`. CSS caps display size around 76px; 152 gives\n * retina-quality rendering on 2x displays.\n */\nexport const THUMB_PX = 152;\n\nexport function transformImageUrl(\n url: string | null | undefined,\n t: ImageTransform = {},\n): string {\n if (!url) return \"\";\n try {\n const u = new URL(url);\n if (t.width !== undefined) u.searchParams.set(\"width\", String(t.width));\n if (t.height !== undefined) u.searchParams.set(\"height\", String(t.height));\n if (t.crop) u.searchParams.set(\"crop\", t.crop);\n return u.toString();\n } catch {\n return url;\n }\n}\n\n/**\n * Responsive `srcset` widths. The smallest variant is ~2× the element's CSS\n * display size — never 1×. A 1× variant is a trap: when the browser resolves\n * device-pixel-ratio to 1 (Chrome device mode, some embedded contexts) it\n * picks that 1× image, which is upscaled and soft on a Retina screen. Starting\n * at 2× guarantees ≥2× density everywhere. Shopify's CDN caps each request at\n * the master's resolution, so the larger variants never upscale.\n *\n * - `THUMB_WIDTHS` — product thumbnails (fixed rows ~48px, mix & match filled\n * slots ~60px). Min 120 = 2× the ~60px slot.\n * - `PICKER_WIDTHS` — the mix & match picker modal (~228px card). Min 480 ≈ 2×\n * the card; up to 1024 (master) for high-DPR / larger displays.\n */\nexport const THUMB_WIDTHS = [120, 180, 240];\nexport const PICKER_WIDTHS = [480, 600, 768, 1024];\n\n/** Display widths for the `sizes` attribute. The modal scales on mobile. */\nexport const THUMB_SIZES = \"60px\";\nexport const PICKER_SIZES = \"(max-width: 767px) 45vw, 228px\";\n\n/** Build a Shopify CDN `srcset` string from a base image URL and widths. */\nexport function imageSrcset(url: string, widths: number[]): string {\n return widths\n .map((w) => `${transformImageUrl(url, { width: w })} ${w}w`)\n .join(\", \");\n}\n","/**\n * Pure countdown formatter — returns the `Nd HHh MMm SSs` / `HHh MMm SSs`\n * string given a millisecond remaining. SSR-safe, no DOM, no timers.\n *\n * A developer building their own widget wires their own `setInterval`\n * + React state and calls this to format the label. The web component's\n * `renderCountdown` uses this internally.\n */\nexport function formatCountdown(msRemaining: number): string {\n if (!Number.isFinite(msRemaining) || msRemaining <= 0) return \"\";\n const totalSeconds = Math.floor(msRemaining / 1000);\n const days = Math.floor(totalSeconds / 86400);\n const hours = Math.floor((totalSeconds % 86400) / 3600);\n const minutes = Math.floor((totalSeconds % 3600) / 60);\n const seconds = totalSeconds % 60;\n const pad = (n: number) => String(n).padStart(2, \"0\");\n if (days > 0) {\n return `${days}d ${pad(hours)}h ${pad(minutes)}m ${pad(seconds)}s`;\n }\n return `${pad(hours)}h ${pad(minutes)}m ${pad(seconds)}s`;\n}\n","// Public surface — only the algorithm and its types. Layout-tuning\n// constants (item height, list padding, max visible items) live inline\n// in each consumer so the SDK contract isn't locked into specific\n// numeric values that might change.\nexport {\n computePosition,\n type DropdownPlacement,\n type ComputePositionArgs,\n type ComputePositionResult,\n} from \"./compute-position\";\n\nexport {\n handleKey,\n firstEnabled,\n type DropdownKeyEvent,\n type DropdownKeyState,\n type DropdownAction,\n} from \"./keyboard\";\n\nexport {\n pushTypeAheadChar,\n emptyTypeAheadState,\n type TypeAheadState,\n type TypeAheadOption,\n type PushCharResult,\n} from \"./type-ahead\";\n","/**\n * Pure positioning helper for an accessible dropdown popover.\n *\n * Given a trigger rect and the viewport height, decide whether the panel\n * should open above or below the trigger and how tall it can be. Used by\n * the Liquid theme dropdown, the `<lime-bundle>` web component, and the\n * React `<VariantDropdown>` so all three surfaces agree.\n *\n * The function is intentionally framework-agnostic — caller passes plain\n * numbers (typically from `getBoundingClientRect()` and `window.innerHeight`)\n * and applies the returned `position: fixed` coordinates itself.\n */\n\nexport const DEFAULT_TRIGGER_MARGIN = 4;\n\nexport type DropdownPlacement = \"up\" | \"down\";\n\nexport interface ComputePositionArgs {\n trigger: { top: number; bottom: number; left: number; width: number };\n viewportHeight: number;\n desiredHeight: number;\n margin?: number;\n /**\n * Optional viewport-coordinate bounds of the nearest clipping ancestor\n * (e.g. a `overflow:auto` product list). When provided, placement is\n * decided against these bounds instead of the full viewport so a panel\n * inside a short scroll container flips upward before it would clip.\n *\n * Bounds are intersected with the viewport (`[0, viewportHeight]`) so a\n * caller that passes the raw `getBoundingClientRect()` of an ancestor\n * still gets a sane result when the ancestor itself extends off-screen.\n */\n clip?: { top: number; bottom: number };\n}\n\nexport interface ComputePositionResult {\n placement: DropdownPlacement;\n maxHeight: number;\n offsetTop: number;\n offsetLeft: number;\n width: number;\n}\n\nexport function computePosition({\n trigger,\n viewportHeight,\n desiredHeight,\n margin = DEFAULT_TRIGGER_MARGIN,\n clip,\n}: ComputePositionArgs): ComputePositionResult {\n const upperBound = clip\n ? Math.min(viewportHeight, clip.bottom)\n : viewportHeight;\n const lowerBound = clip ? Math.max(0, clip.top) : 0;\n const spaceBelow = Math.max(0, upperBound - trigger.bottom - margin);\n const spaceAbove = Math.max(0, trigger.top - lowerBound - margin);\n\n // Prefer downward unless the panel would clip AND upward has more room.\n // Matches native <select> behaviour on most platforms.\n const placement: DropdownPlacement =\n desiredHeight <= spaceBelow\n ? \"down\"\n : spaceAbove > spaceBelow\n ? \"up\"\n : \"down\";\n\n const available = placement === \"down\" ? spaceBelow : spaceAbove;\n // Cap maxHeight at the actual available space so the panel never spills\n // outside the viewport. When `available` is less than the desired height\n // the listbox shrinks and its `overflow-y: auto` scrolls — better than\n // overflowing the viewport with a fixed-minimum tall panel.\n const maxHeight = Math.min(desiredHeight, available);\n\n const offsetTop =\n placement === \"down\"\n ? trigger.bottom + margin\n : trigger.top - margin - maxHeight;\n\n return {\n placement,\n maxHeight,\n offsetTop,\n offsetLeft: trigger.left,\n width: trigger.width,\n };\n}\n","/**\n * Keyboard event → action discriminator for an accessible dropdown.\n *\n * The DOM glue layer (Liquid asset, web component, React component) calls\n * `handleKey(event, state)` with the event details and current state, then\n * applies the returned action. This keeps WAI-ARIA combobox semantics in a\n * single, unit-testable place.\n *\n * Pattern follows the WAI-ARIA Authoring Practices \"Combobox with Listbox\n * Popup, manual selection\" pattern.\n */\n\nexport interface DropdownKeyEvent {\n key: string;\n ctrlKey?: boolean;\n metaKey?: boolean;\n altKey?: boolean;\n shiftKey?: boolean;\n}\n\nexport interface DropdownKeyState {\n isOpen: boolean;\n activeIndex: number;\n selectedIndex: number;\n options: ReadonlyArray<{ disabled: boolean }>;\n}\n\nexport type DropdownAction =\n | {\n type: \"open\";\n activeIndex: number;\n preventDefault: true;\n }\n | {\n type: \"close\";\n commit: false;\n restoreFocus: boolean;\n preventDefault: boolean;\n }\n | {\n type: \"move-active\";\n activeIndex: number;\n preventDefault: true;\n }\n | {\n type: \"commit\";\n index: number;\n preventDefault: true;\n }\n | {\n type: \"type-ahead\";\n char: string;\n preventDefault: false;\n }\n | {\n type: \"passthrough\";\n preventDefault: boolean;\n };\n\nconst PASSTHROUGH: DropdownAction = {\n type: \"passthrough\",\n preventDefault: false,\n};\n\n/**\n * Index of the first selectable option, or -1 when every option is\n * disabled. Exported because consumers need the same \"where does focus\n * land\" answer when opening a listbox with no valid selection.\n */\nexport function firstEnabled(\n options: ReadonlyArray<{ disabled: boolean }>,\n): number {\n for (let i = 0; i < options.length; i++) {\n if (!options[i].disabled) return i;\n }\n return -1;\n}\n\nfunction lastEnabled(options: ReadonlyArray<{ disabled: boolean }>): number {\n for (let i = options.length - 1; i >= 0; i--) {\n if (!options[i].disabled) return i;\n }\n return -1;\n}\n\nfunction nextEnabled(\n options: ReadonlyArray<{ disabled: boolean }>,\n from: number,\n): number {\n if (options.length === 0) return -1;\n for (let step = 1; step <= options.length; step++) {\n const idx = (from + step) % options.length;\n if (!options[idx].disabled) return idx;\n }\n return from; // all disabled — stay put\n}\n\nfunction prevEnabled(\n options: ReadonlyArray<{ disabled: boolean }>,\n from: number,\n): number {\n if (options.length === 0) return -1;\n for (let step = 1; step <= options.length; step++) {\n const idx = (from - step + options.length) % options.length;\n if (!options[idx].disabled) return idx;\n }\n return from;\n}\n\nfunction isPrintable(key: string): boolean {\n return key.length === 1 && key !== \" \" && /\\S/.test(key);\n}\n\nexport function handleKey(\n event: DropdownKeyEvent,\n state: DropdownKeyState,\n): DropdownAction {\n // Ignore modifier-key combos (browser shortcuts, etc).\n if (event.ctrlKey || event.metaKey || event.altKey) return PASSTHROUGH;\n\n const { key } = event;\n const { isOpen, activeIndex, selectedIndex, options } = state;\n\n // Closed → key opens (or noop)\n if (!isOpen) {\n switch (key) {\n case \"Enter\":\n case \" \":\n case \"ArrowDown\":\n return {\n type: \"open\",\n activeIndex:\n selectedIndex >= 0 && !options[selectedIndex]?.disabled\n ? selectedIndex\n : firstEnabled(options),\n preventDefault: true,\n };\n case \"ArrowUp\":\n return {\n type: \"open\",\n activeIndex:\n selectedIndex >= 0 && !options[selectedIndex]?.disabled\n ? selectedIndex\n : lastEnabled(options),\n preventDefault: true,\n };\n default:\n if (isPrintable(key)) {\n return {\n type: \"open\",\n activeIndex:\n selectedIndex >= 0 && !options[selectedIndex]?.disabled\n ? selectedIndex\n : firstEnabled(options),\n preventDefault: true,\n };\n }\n return PASSTHROUGH;\n }\n }\n\n // Open → navigation, commit, close\n switch (key) {\n case \"ArrowDown\":\n return {\n type: \"move-active\",\n activeIndex: nextEnabled(options, activeIndex),\n preventDefault: true,\n };\n case \"ArrowUp\":\n return {\n type: \"move-active\",\n activeIndex: prevEnabled(options, activeIndex),\n preventDefault: true,\n };\n case \"Home\":\n return {\n type: \"move-active\",\n activeIndex: firstEnabled(options),\n preventDefault: true,\n };\n case \"End\":\n return {\n type: \"move-active\",\n activeIndex: lastEnabled(options),\n preventDefault: true,\n };\n case \"Enter\":\n case \" \":\n if (activeIndex >= 0 && !options[activeIndex]?.disabled) {\n return {\n type: \"commit\",\n index: activeIndex,\n preventDefault: true,\n };\n }\n // Enter while open with no committable target — swallow it. The\n // dropdown lives inside a Shopify product-page <form>, so a\n // bubbling Enter would submit the page form. WAI-ARIA combobox\n // pattern says the popup should absorb Enter regardless.\n return { type: \"passthrough\", preventDefault: true };\n case \"Escape\":\n return {\n type: \"close\",\n commit: false,\n restoreFocus: true,\n preventDefault: true,\n };\n case \"Tab\":\n // Don't preventDefault — let the browser advance focus naturally.\n // Don't restoreFocus either: stealing focus back to the trigger\n // would interfere with the browser's tab-advance to the next\n // focusable element.\n return {\n type: \"close\",\n commit: false,\n restoreFocus: false,\n preventDefault: false,\n };\n default:\n if (isPrintable(key)) {\n return { type: \"type-ahead\", char: key, preventDefault: false };\n }\n return PASSTHROUGH;\n }\n}\n","/**\n * Type-ahead matching for dropdown listboxes.\n *\n * The caller drives `now` (typically `Date.now()`) so the function stays\n * pure and deterministic in tests. After `RESET_MS` of inactivity the\n * buffer clears; otherwise additional chars accumulate so the user can\n * type \"me\" to jump from \"Mango\" to \"Medium\" without overshoot.\n */\n\nexport const TYPE_AHEAD_RESET_MS = 500;\n\nexport interface TypeAheadState {\n buffer: string;\n lastTime: number;\n}\n\nexport interface TypeAheadOption {\n disabled: boolean;\n label: string;\n}\n\nexport function emptyTypeAheadState(): TypeAheadState {\n return { buffer: \"\", lastTime: 0 };\n}\n\nexport interface PushCharResult {\n newState: TypeAheadState;\n matchedIndex: number | null;\n}\n\nexport function pushTypeAheadChar(\n state: TypeAheadState,\n char: string,\n now: number,\n options: ReadonlyArray<TypeAheadOption>,\n resetMs: number = TYPE_AHEAD_RESET_MS,\n): PushCharResult {\n if (char.length !== 1) {\n return { newState: state, matchedIndex: null };\n }\n\n const expired = now - state.lastTime > resetMs;\n const buffer = (expired ? \"\" : state.buffer) + char.toLowerCase();\n const newState: TypeAheadState = { buffer, lastTime: now };\n\n for (let i = 0; i < options.length; i++) {\n const opt = options[i];\n if (opt.disabled) continue;\n if (opt.label.toLowerCase().startsWith(buffer)) {\n return { newState, matchedIndex: i };\n }\n }\n\n return { newState, matchedIndex: null };\n}\n","/**\n * Variant resolution + availability cascade for per-option pickers.\n *\n * Mirrors the algorithms in `extensions/bundle-theme/assets/bundle-fixed.js`\n * (`findVariantByOptions`, `isOptionValueAvailable`) so the React component\n * and web component renderer share the same source of truth as the Liquid\n * widget. Liquid keeps its inlined copy because theme assets cannot import\n * npm — a CI parity guard prevents drift.\n *\n * Inputs are normalized to positional option-value arrays. Storefront API\n * consumers can derive this with `v.selectedOptions.map(o => o.value)`.\n */\n\nimport { isFulfillable } from \"../inventory/predicate\";\n\nexport interface PickerVariant {\n id: string;\n /** Positional option values: index i matches the product's option-position i+1. */\n optionValues: string[];\n /** True when the variant has stock and is purchasable. */\n available: boolean;\n}\n\n/**\n * Returns the variant whose positional options exactly match the selected\n * values, or `null` if none. Caller decides what to do on miss (typically\n * fall back to the first available variant).\n */\nexport function findVariantByOptions<V extends PickerVariant>(\n variants: ReadonlyArray<V>,\n optionValues: ReadonlyArray<string>,\n): V | null {\n if (variants.length === 0) return null;\n for (const v of variants) {\n if (v.optionValues.length !== optionValues.length) continue;\n let match = true;\n for (let j = 0; j < v.optionValues.length; j++) {\n if (v.optionValues[j] !== optionValues[j]) {\n match = false;\n break;\n }\n }\n if (match) return v;\n }\n return null;\n}\n\n/**\n * True iff some in-stock variant pairs `value` at `optionIndex` with the\n * currently selected values at every other option index. Drives the\n * disabled state of options in cascading pickers (e.g. selecting Color=Red\n * may disable Size=XL if no Red XL variant exists or it's sold out).\n */\nexport function isOptionValueAvailable(\n variants: ReadonlyArray<PickerVariant>,\n optionIndex: number,\n value: string,\n selected: ReadonlyArray<string>,\n): boolean {\n for (const v of variants) {\n if (!v.available) continue;\n if (v.optionValues[optionIndex] !== value) continue;\n let ok = true;\n for (let j = 0; j < v.optionValues.length; j++) {\n if (j === optionIndex) continue;\n if (v.optionValues[j] !== selected[j]) {\n ok = false;\n break;\n }\n }\n if (ok) return true;\n }\n return false;\n}\n\n/**\n * Convenience helper for Storefront API consumers. Converts a Storefront\n * `ProductVariant` (with `selectedOptions: [{name, value}]`) into the\n * normalized `PickerVariant` shape.\n *\n * Pass `requiredQty` to make availability quantity-aware: a variant with 2\n * units in stock cannot cover a 6-unit bundle slot, and offering it in the\n * dropdown walks the shopper into a cart Shopify caps at add time. Without\n * it, availability falls back to the plain `availableForSale` boolean.\n */\nexport function toPickerVariant<\n T extends {\n id: string;\n selectedOptions: ReadonlyArray<{ value: string }>;\n availableForSale: boolean;\n currentlyNotInStock?: boolean;\n quantityAvailable?: number | null;\n },\n>(variant: T, requiredQty?: number): PickerVariant {\n return {\n id: variant.id,\n optionValues: variant.selectedOptions.map((o) => o.value),\n available:\n requiredQty !== undefined\n ? isFulfillable(variant, requiredQty)\n : variant.availableForSale,\n };\n}\n","/**\n * Storefront-side fulfillability predicate.\n *\n * Mirrors `app/lib/inventory.server.ts:isFulfillable` but reads from a\n * Storefront-shaped variant (no Admin types, no Node imports). Same\n * semantics: a tracked + DENY variant with insufficient stock fails;\n * everything else succeeds.\n *\n * Used by the widget renderers and React components to gate add-to-cart\n * eligibility, badge rendering, and stepper caps. Lives in `core` so the\n * Liquid widget (via `@lime-bundles/widget`) and React consumers (via\n * `@lime-bundles/react`) draw from one source of truth.\n *\n * Storefront API limitations:\n * - `quantityAvailable` is `null` when the merchant has not granted\n * `unauthenticated_read_product_inventory` to the Storefront token.\n * In that case, treat the variant as fulfillable (boolean fallback)\n * rather than blocking the cart on a permission gap.\n * - `quantityAvailable` is `0` (not null) for variants whose inventory\n * is not tracked. Untracked is distinguishable from a genuine zero:\n * a tracked DENY variant at zero is not `availableForSale`, and a\n * tracked CONTINUE variant at zero is flagged `currentlyNotInStock`.\n * Purchasable + not backordered + zero-or-negative quantity therefore\n * means \"untracked — stock unknown, defer to `availableForSale`\",\n * which is exactly how Liquid's `variant.available` treats it.\n * - `currentlyNotInStock` is true for backordered variants (zero\n * stock + `inventoryPolicy = CONTINUE`). Fulfillable for cart\n * purposes — Shopify's checkout still accepts them.\n */\n\n/**\n * Minimal variant shape the predicate needs. Wider Storefront variants\n * (with title, price, etc.) are accepted via structural typing.\n */\nexport interface StorefrontVariantStock {\n availableForSale: boolean;\n /** Optional — Storefront API field, may be undefined on hand-built mocks. */\n currentlyNotInStock?: boolean;\n /** Null when the Storefront token lacks the inventory scope, or when\n * the variant is untracked. Treat as \"unknown stock — defer to\n * availableForSale\". */\n quantityAvailable?: number | null;\n}\n\n/**\n * Purchasable, not backordered, yet reporting zero-or-negative stock:\n * that combination only exists for variants whose inventory is not\n * tracked (tracked DENY at zero loses `availableForSale`; tracked\n * CONTINUE at zero gains `currentlyNotInStock`). Stock is unknown for\n * these, so every stock-based cap must stand down and defer to\n * `availableForSale` — the same answer Liquid's `variant.available`\n * gives for them.\n */\nfunction isUntrackedStock(variant: StorefrontVariantStock): boolean {\n return (\n variant.availableForSale &&\n !variant.currentlyNotInStock &&\n variant.quantityAvailable != null &&\n variant.quantityAvailable <= 0\n );\n}\n\n/**\n * True when a customer can add `requiredQty` units of this variant to\n * their cart and pass Shopify's checkout-time stock check.\n *\n * - `availableForSale = false` → never fulfillable (Shopify will reject).\n * - `currentlyNotInStock = true` → backordered (zero stock + CONTINUE\n * inventory policy). Shopify will accept the cart line for any qty,\n * so treat as fulfillable. Mirrors the Admin-side predicate's\n * CONTINUE branch in [app/lib/inventory.server.ts].\n * - `quantityAvailable` is `null` / `undefined` → defer to the boolean\n * (the merchant's Storefront token may be missing\n * `unauthenticated_read_product_inventory`).\n * - `quantityAvailable <= 0` while still purchasable → untracked\n * inventory (the API reports `0`, not `null`, for untracked variants);\n * fulfillable at any quantity.\n * - Otherwise → require `quantityAvailable >= requiredQty`.\n *\n * Known, accepted asymmetry with the Admin-side predicate: a CONTINUE-policy\n * variant with SOME stock but less than `requiredQty` reads as fulfillable\n * on the admin (Shopify will accept the order and backorder the remainder)\n * but unavailable here — the Storefront API only reveals the policy via\n * `currentlyNotInStock`, which is false while any stock remains, and\n * treating every partial-stock variant as fulfillable would wave through\n * DENY-policy variants whose add Shopify then caps. Blocking the rare\n * backorderable partial is the safe direction; the merchant-visible effect\n * is underselling, not a broken cart. See docs/out-of-stock-lifecycle.md.\n */\nexport function isFulfillable(\n variant: StorefrontVariantStock,\n requiredQty: number,\n): boolean {\n if (!variant.availableForSale) return false;\n if (variant.currentlyNotInStock) return true;\n if (variant.quantityAvailable == null) return true;\n if (isUntrackedStock(variant)) return true;\n return variant.quantityAvailable >= requiredQty;\n}\n\n/**\n * The stock number that should cap UI steppers and cross-slot guards for\n * this variant, or `null` when stock never caps: backorder-allowed\n * (`currentlyNotInStock`), untracked inventory, or unknown because the\n * shop's token lacks `unauthenticated_read_product_inventory`. This is the\n * exact contract of the picker layer's `inventoryQuantity` field — the\n * theme host computes the same value in Liquid.\n */\nexport function stockCapForVariant(\n variant: StorefrontVariantStock,\n): number | null {\n if (variant.currentlyNotInStock) return null;\n if (variant.quantityAvailable == null) return null;\n if (isUntrackedStock(variant)) return null;\n return Math.max(0, variant.quantityAvailable);\n}\n\n/**\n * Whether the \"Only X left\" low-stock badge should render for this\n * variant. False when:\n * - The merchant disabled the badge (`enabled = false`).\n * - We don't know the quantity (`quantityAvailable` is null/undefined).\n * - The variant is below `requiredQty` (the renderer treats this as\n * out-of-stock instead — no double signaling).\n * - The variant has more stock than the threshold (no badge needed).\n */\nexport function shouldShowLowStockBadge(\n variant: StorefrontVariantStock,\n requiredQty: number,\n threshold: number,\n enabled: boolean,\n): boolean {\n if (!enabled) return false;\n if (variant.quantityAvailable == null) return false;\n if (variant.quantityAvailable < requiredQty) return false;\n return variant.quantityAvailable <= threshold;\n}\n\n/**\n * Maximum number of bundles the customer can request given a variant's\n * stock and the per-bundle required quantity. Used to cap quantity\n * steppers — without a cap, a customer can repeatedly press \"+\" until\n * the cart-add request fails at Shopify's checkout-time stock check.\n *\n * Returns `Infinity` when stock is unknown or unbounded (the boolean\n * fallback path) — callers should guard with the existing UI ceiling\n * (e.g. `Math.min(maxBundlesPerOrder, maxBundlesByStock)`).\n */\nexport function maxBundlesByStock(\n variant: StorefrontVariantStock,\n requiredQty: number,\n): number {\n if (variant.quantityAvailable == null) return Number.POSITIVE_INFINITY;\n if (isUntrackedStock(variant)) return Number.POSITIVE_INFINITY;\n if (requiredQty <= 0) return Number.POSITIVE_INFINITY;\n return Math.floor(variant.quantityAvailable / requiredQty);\n}\n\n/**\n * Largest qty the shopper may add to a mix-and-match bundle pick for a given\n * variant, after subtracting any units they've already allocated to this\n * bundle. Returns 0 when the variant isn't fulfillable.\n *\n * Bounded by:\n * - the merchant's per-product `productMax` (rule.max)\n * - the variant's `quantityAvailable` (when known)\n * Backordered variants (`currentlyNotInStock` with policy CONTINUE) bypass\n * the stock cap and rely on `productMax`.\n */\nexport function maxAddableQuantity(\n variant: StorefrontVariantStock,\n productMax: number,\n alreadyInBundle: number,\n): number {\n if (!variant.availableForSale) return 0;\n // Backorder: stock cap doesn't apply.\n if (variant.currentlyNotInStock) {\n return Math.max(0, productMax - alreadyInBundle);\n }\n // Untracked (purchasable but reporting <= 0): stock is unknown, so only\n // the merchant's productMax binds.\n const stockCap =\n variant.quantityAvailable == null || isUntrackedStock(variant)\n ? Number.POSITIVE_INFINITY\n : variant.quantityAvailable;\n return Math.max(0, Math.min(productMax, stockCap) - alreadyInBundle);\n}\n","/**\n * A/B test assignment: given a visitor and a test, which side do they see?\n *\n * Deterministic and stateless. The choice is recomputed from `hash(visitorId +\n * testId)` on every page rather than being decided once and stored, which is\n * why one opaque visitor id covers every test the shop will ever run, changing\n * a split needs no stored-state migration, and there is no per-test cookie to\n * set, read or expire.\n *\n * ## Privacy\n *\n * One random first-party id in `localStorage`, no PII, nothing cross-site, and\n * no consent module. The removed A/B feature shipped `setConsent`/`hasConsent`\n * for a design that needed considerably more than this.\n *\n * ## What happens without storage\n *\n * Blocked or full storage degrades to a per-page id, and that is deliberately\n * survivable rather than fatal. Impression and add-to-cart both happen on the\n * product page, so the funnel stays internally consistent within a page view,\n * and purchase attribution rides the `_lime_bundle_gid` line property into the\n * order. What is lost is only the *experience* staying stable across visits,\n * not the correctness of the numbers.\n *\n * ES2017 target — this package is bundled into both hosts, and esbuild\n * downlevels syntax but does not polyfill runtime APIs.\n */\n\n/** localStorage key holding the opaque per-visitor id. */\nexport const VISITOR_ID_KEY = \"_lb_v\";\n\n/** Module-scope fallback used when localStorage is unavailable. */\nlet inMemoryVisitorId: string | null = null;\n\n/**\n * 32-bit FNV-1a. Small, dependency-free, and well distributed for short\n * strings, which is all that is being asked of it. Not a security primitive\n * and does not need to be one: knowing your own bucket lets a shopper see the\n * other offer, which they could do by clearing storage anyway.\n */\nexport function hashString(input: string): number {\n let hash = 0x811c9dc5;\n for (let i = 0; i < input.length; i++) {\n hash ^= input.charCodeAt(i);\n // Multiply by the FNV prime (16777619) using shifts, staying inside 32-bit\n // integer math the whole way.\n hash +=\n (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);\n hash >>>= 0;\n }\n return hash >>> 0;\n}\n\n/**\n * Read the visitor id, creating and persisting one on first sight.\n *\n * Never throws: Safari private mode, disabled storage and quota-exceeded all\n * fall back to a value that lives for the page.\n */\nexport function getVisitorId(): string {\n try {\n const stored = window.localStorage.getItem(VISITOR_ID_KEY);\n if (stored) return stored;\n const fresh = randomId();\n window.localStorage.setItem(VISITOR_ID_KEY, fresh);\n return fresh;\n } catch (_err) {\n if (!inMemoryVisitorId) inMemoryVisitorId = randomId();\n return inMemoryVisitorId;\n }\n}\n\nfunction randomId(): string {\n // Not crypto-grade on purpose: this is a bucketing key, not a token, and\n // crypto.randomUUID is unavailable on older storefront browsers.\n return (\n Math.random().toString(36).slice(2, 10) +\n Math.random().toString(36).slice(2, 10)\n );\n}\n\n/** The bits of a bundle this module needs. Structural so both hosts fit. */\nexport interface AbAssignable {\n /** Stable identity, used to order the sides deterministically. */\n id: string;\n abTestId: string | null;\n abWeight: number;\n}\n\n/**\n * Roll once per visitor per test, then walk the sides accumulating their\n * weights until the roll lands inside one. A 50/50 pair splits the 0–99 space\n * into 0–49 and 50–99; a 90/10 pair into 0–89 and 90–99.\n *\n * The obvious-looking alternative — asking each side in turn \"is this visitor\n * in your bucket?\" — is broken, and was the original bug here. That predicate\n * depends only on the visitor and the test, so it answers the same for every\n * side: a low roll always matched the first side, and a high roll matched\n * nobody and fell back to the first side. **One side could never be shown.**\n * Weights are shares of a single roll, not independent tests, and the code has\n * to say so.\n *\n * Sides are sorted by `id` first so the choice does not depend on the order\n * they arrived in. DOM order in the theme comes from Shopify's metafield\n * reference list and fetch order in the SDK comes from the API; neither is\n * guaranteed stable, and a visitor must not swap sides because a list came\n * back differently.\n */\nexport function pickAbSide<T extends AbAssignable>(\n members: readonly T[],\n visitorId: string,\n testId: string,\n): T {\n const ordered = members\n .slice()\n .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));\n\n const roll = hashString(visitorId + \":\" + testId) % 100;\n\n let acc = 0;\n for (const member of ordered) {\n acc += Math.max(0, member.abWeight);\n if (roll < acc) return member;\n }\n\n // Weights that don't reach 100 (half-written pairing, stale metaobject):\n // give the remainder to the last side rather than showing the shopper\n // nothing.\n return ordered[ordered.length - 1];\n}\n\n/**\n * Reduce a list of bundles to the ones this visitor should actually see:\n * everything not in a test, plus exactly one side of each test.\n *\n * Order is preserved, so a host that renders bundles top to bottom keeps the\n * merchant's ordering.\n *\n * A test whose weights don't add up still yields a side: showing the shopper\n * one offer always beats showing them none. See `pickAbSide`.\n */\nexport function resolveAbTests<T extends AbAssignable>(\n bundles: readonly T[],\n visitorId: string,\n): T[] {\n const groups = new Map<string, T[]>();\n for (const bundle of bundles) {\n if (!bundle.abTestId) continue;\n const existing = groups.get(bundle.abTestId);\n if (existing) existing.push(bundle);\n else groups.set(bundle.abTestId, [bundle]);\n }\n\n // Nothing paired on this page — every bundle stands alone.\n if (groups.size === 0) return bundles.slice();\n\n const chosen = new Map<string, T>();\n groups.forEach((members, testId) => {\n // A test whose other side isn't on this page (different product, or the\n // sibling went inactive) isn't a choice at all. Show what's here.\n if (members.length === 1) {\n chosen.set(testId, members[0]);\n return;\n }\n chosen.set(testId, pickAbSide(members, visitorId, testId));\n });\n\n return bundles.filter(\n (bundle) => !bundle.abTestId || chosen.get(bundle.abTestId) === bundle,\n );\n}\n","/**\n * What a host supplies to the shared renderer.\n *\n * Two hosts use these modules. The **theme app extension** runs inside a\n * merchant's storefront: it formats money through `Shopify.formatMoney`, which\n * honours the shop's configured format, and its copy is already translated by\n * Liquid's `t` filter. The **headless `<lime-bundle>` element** has neither, so\n * it falls back to `Intl.NumberFormat` and the English defaults below.\n *\n * Everything host-specific is passed in rather than imported, which is what\n * lets one implementation serve both. Nothing in `render/` may reach for\n * `window.Shopify` or a global.\n */\n\n/** A line the widget wants in the cart. `priceCents` feeds analytics only. */\nexport interface CartItem {\n variantId: number | string;\n quantity: number;\n priceCents?: number;\n /**\n * Extra line attributes for THIS line only, on top of the bundle\n * attribution every line carries. Used by BOGO to mark which line the\n * shopper chose as the reward — see `BUNDLE_ROLE_ATTRIBUTE`.\n */\n attributes?: Array<{ key: string; value: string }>;\n}\n\n/**\n * The custom variant dropdown, when the host has wired one up.\n *\n * Optional on purpose: the picker modal works with plain `<select>` elements\n * if no dropdown is bound, so a host can skip it.\n */\nexport interface DropdownApi {\n bind: (select: HTMLSelectElement) => unknown;\n bindAll: (root?: ParentNode | null) => void;\n unbindAll: (root?: ParentNode | null) => void;\n observeRoot: (root: HTMLElement | null) => void;\n}\n\n/** Formats a cents amount for display. */\nexport type FormatMoney = (cents: number) => string;\n\n/**\n * Fallback money formatting for hosts without Shopify's.\n *\n * `Intl.NumberFormat` ignores the merchant's configured money format, so this\n * is a last resort — the theme host always supplies `Shopify.formatMoney`.\n */\nexport function intlFormatMoney(currencyCode: string): FormatMoney {\n return (cents) => {\n try {\n return new Intl.NumberFormat(undefined, {\n style: \"currency\",\n currency: currencyCode,\n }).format(cents / 100);\n } catch {\n // An unrecognised currency code throws rather than degrading.\n return currencyCode + \" \" + (cents / 100).toFixed(2);\n }\n };\n}\n\n/**\n * Line-item attribute keys carrying bundle attribution into the order.\n *\n * The `orders/create` webhook reads both to attribute a purchase, so every\n * line every host adds must carry both. Named here because the theme host\n * writes them as `/cart/add.js` properties and the headless host writes them\n * as `CartLineInput.attributes` — different shapes, one contract.\n */\nexport const BUNDLE_GID_ATTRIBUTE = \"_lime_bundle_gid\";\nexport const BUNDLE_TYPE_ATTRIBUTE = \"_lime_bundle_type\";\n\n/**\n * Marks the BOGO line the shopper chose as their reward.\n *\n * The widget lets the shopper pick the \"get\" variant and shows that one\n * struck through, but the discount function allocated the reward\n * cheapest-first across every line of the get product — so on a\n * same-product offer the buy line counted as a get line, and a shopper who\n * picked the pricier variant as their free item watched checkout discount\n * the other one instead (issue #421).\n *\n * Only written when the reward resolves to a line of its own. A shopper who\n * picks the same variant on both sides gets one merged line, where there is\n * nothing to choose between. A cart assembled by hand carries no role at\n * all and the function falls back to cheapest-first, which is what protects\n * the merchant when nobody made a choice.\n */\nexport const BUNDLE_ROLE_ATTRIBUTE = \"_lime_bundle_role\";\nexport const BUNDLE_ROLE_GET = \"get\";\n\nexport function bundleLineAttributes(\n bundleGid: string,\n bundleType: string,\n): Array<{ key: string; value: string }> {\n return [\n { key: BUNDLE_GID_ATTRIBUTE, value: bundleGid },\n { key: BUNDLE_TYPE_ATTRIBUTE, value: bundleType },\n ];\n}\n","import {\n formatUnitPrice,\n isVariantFulfillable,\n resolveBundleQty,\n stockCapForVariant,\n type Product,\n type ProductVariant,\n} from \"@lime-bundles/core\";\nimport type { BundleProduct, BundleVariant } from \"./product/types\";\n\n/**\n * Storefront API shapes → the render layer's shapes.\n *\n * The render layer speaks the theme's dialect: numeric IDs and integer cents,\n * because that is what Liquid emits into the page and converting there would\n * inflate a payload that ships on every product view. The Storefront API\n * speaks GIDs and decimal-string Money, so the headless host converts here\n * instead — once, at the edge.\n */\n\n/** `gid://shopify/Product/123` → `123`. Returns the input if it isn't a GID. */\nexport function numericId(gid: string): number | string {\n const tail = gid.slice(gid.lastIndexOf(\"/\") + 1);\n const n = Number(tail);\n return Number.isFinite(n) && tail !== \"\" ? n : gid;\n}\n\n/** Decimal-string Money → integer cents, rounded the way Shopify rounds. */\nexport function toCents(amount: string | null | undefined): number {\n if (!amount) return 0;\n const n = parseFloat(amount);\n return Number.isFinite(n) ? Math.round(n * 100) : 0;\n}\n\n/**\n * width / height of a Storefront image, or null when either dimension is\n * missing or degenerate — the same value Liquid's `image.aspect_ratio`\n * exposes, which the theme stamps on each row's thumbnail box.\n */\nfunction imageAspectRatio(\n image: { width?: number | null; height?: number | null } | null | undefined,\n): number | null {\n const w = image?.width;\n const h = image?.height;\n if (typeof w !== \"number\" || typeof h !== \"number\" || w <= 0 || h <= 0) {\n return null;\n }\n return w / h;\n}\n\nexport interface AdaptOptions {\n /** Per-product and per-variant quantity overrides from the bundle. */\n quantities: {\n productQuantities: Record<string, number>;\n variantQuantities: Record<string, number>;\n };\n /** Merchant-restricted variant GIDs for this product, or null for all. */\n allowedVariantIds: string[] | null;\n}\n\nfunction adaptVariant(\n v: ProductVariant,\n requiredQty: number,\n fallbackCurrency: string,\n): BundleVariant {\n return {\n id: numericId(v.id),\n title: v.title,\n // Positional, matching how Liquid emits `variant.options`.\n options: (v.selectedOptions ?? []).map((o) => o.value),\n // The same predicate the admin evaluator and the Liquid gate use, so a\n // variant reads as unavailable on every surface at the same moment.\n available: isVariantFulfillable(v, requiredQty),\n price: toCents(v.price?.amount),\n compareAtPrice: toCents(v.compareAtPrice?.amount),\n unitPrice: formatUnitPrice(v.unitPrice, v.unitPriceMeasurement, fallbackCurrency),\n image: v.image?.url ?? null,\n // Carried so the picker's stepper cap and cross-slot stock guard can\n // bind on the headless surface; dropping it here left them capless.\n inventoryQuantity: stockCapForVariant(v),\n };\n}\n\n/**\n * Adapt one product, narrowed to the variants the merchant allowed.\n *\n * `selectedVariantId` lands on the first allowed variant that is actually\n * fulfillable, falling back to the first allowed one — the same order the\n * Liquid uses when it picks a row's initial variant.\n */\nexport function adaptProduct(\n product: Product,\n bundleId: string,\n opts: AdaptOptions,\n): BundleProduct {\n const allowed = opts.allowedVariantIds;\n const nodes = product.variants.nodes.filter(\n (v) => !allowed || allowed.length === 0 || allowed.includes(v.id),\n );\n\n const currency = product.priceRange.minVariantPrice.currencyCode;\n const productQty = resolveBundleQty(\n opts.quantities,\n product.id,\n nodes[0]?.id ?? \"\",\n );\n\n const variants = nodes.map((v) => {\n const qty = resolveBundleQty(opts.quantities, product.id, v.id);\n const adapted = adaptVariant(v, qty, currency);\n // Only carry a per-variant quantity when it actually differs, so the\n // render layer's `variant.quantity || product.quantity` fallback behaves\n // exactly as it does for Liquid-emitted data.\n if (qty !== productQty) adapted.quantity = qty;\n return adapted;\n });\n\n const initial = variants.find((v) => v.available) ?? variants[0];\n\n return {\n productId: numericId(product.id),\n title: product.title,\n url: product.handle ? \"/products/\" + product.handle : null,\n featuredImage: product.featuredImage?.url ?? null,\n featuredImageRatio: imageAspectRatio(product.featuredImage),\n quantity: productQty,\n selectedVariantId: initial ? initial.id : \"\",\n // Option names come off a variant, since the Storefront product query\n // returns them per-variant rather than as a product-level list.\n optionNames: (nodes[0]?.selectedOptions ?? []).map((o) => o.name),\n variants,\n };\n}\n\n/** Adapt every product in a bundle, honouring per-product variant restrictions. */\nexport function adaptProducts(bundle: {\n id: string;\n products: Product[];\n productQuantities: Record<string, number>;\n variantQuantities: Record<string, number>;\n selectedVariantIds: string[][] | null;\n}): BundleProduct[] {\n return bundle.products.map((product, idx) =>\n adaptProduct(product, bundle.id, {\n quantities: {\n productQuantities: bundle.productQuantities,\n variantQuantities: bundle.variantQuantities,\n },\n allowedVariantIds: bundle.selectedVariantIds?.[idx] ?? null,\n }),\n );\n}\n","import type { BundleProduct, BundleVariant } from \"./types\";\n\n/**\n * Resolve a variant by ID, falling back to the product's first.\n *\n * Both sides are coerced to strings because Liquid emits numeric IDs into JSON\n * while DOM attributes come back as strings, and a strict compare between the\n * two silently misses every time.\n */\nexport function findVariant(\n product: BundleProduct,\n variantId: number | string,\n): BundleVariant {\n for (let i = 0; i < product.variants.length; i++) {\n if (String(product.variants[i].id) === String(variantId)) {\n return product.variants[i];\n }\n }\n return product.variants[0];\n}\n\n/**\n * The variant a row's initial selection should land on: the emitted\n * `selectedVariantId` when that variant is sellable at the bundle's quantity,\n * otherwise the first sellable variant, otherwise null — nothing in the slot\n * can cover, and the caller's CTA lock owns that state.\n *\n * Exists because `selectedVariantId` and the per-variant quantity-aware\n * `available` flags are computed separately at emit time. Seeding a greyed\n * option leaves a live Add-to-cart whose click can only fail while a buyable\n * variant sits one option away (#278). Missing `available` is treated as\n * sellable, matching `isOptionValueAvailable`'s default-true contract.\n */\nexport function resolveSellableVariant(\n product: BundleProduct,\n): BundleVariant | null {\n const seeded = findVariant(product, product.selectedVariantId);\n if (seeded && seeded.available !== false) return seeded;\n for (let i = 0; i < product.variants.length; i++) {\n if (product.variants[i].available !== false) return product.variants[i];\n }\n return null;\n}\n\n/**\n * Resolve the variant whose option values match `optionValues` positionally.\n *\n * Returns null rather than guessing when nothing matches, leaving the fallback\n * to the caller.\n */\nexport function findVariantByOptions(\n product: BundleProduct,\n optionValues: string[] | null | undefined,\n): BundleVariant | null {\n if (!product.variants || !optionValues) return null;\n for (let i = 0; i < product.variants.length; i++) {\n const v = product.variants[i];\n if (!v.options || v.options.length !== optionValues.length) continue;\n let match = true;\n for (let j = 0; j < v.options.length; j++) {\n if (v.options[j] !== optionValues[j]) {\n match = false;\n break;\n }\n }\n if (match) return v;\n }\n return null;\n}\n\n/**\n * Whether any in-stock variant pairs `value` at `optionIndex` with the current\n * selection at every other index.\n *\n * Drives per-`<option>` disabled state the way Dawn does, so picking Large\n * greys out the colours Large doesn't come in rather than silently jumping the\n * shopper to a different combination. Sold-out variants are treated as\n * unavailable, which is the same UX.\n */\nexport function isOptionValueAvailable(\n variants: BundleVariant[],\n optionIndex: number,\n value: string,\n selected: string[],\n): boolean {\n for (let i = 0; i < variants.length; i++) {\n const v = variants[i];\n if (v.available === false) continue;\n if (!v.options || v.options[optionIndex] !== value) continue;\n let ok = true;\n for (let j = 0; j < v.options.length; j++) {\n if (j === optionIndex) continue;\n if (v.options[j] !== selected[j]) {\n ok = false;\n break;\n }\n }\n if (ok) return true;\n }\n return false;\n}\n","import { findVariant } from \"../product/variants\";\nimport type { BundleProduct } from \"../product/types\";\nimport type { CartItem } from \"../host\";\n\n/**\n * Build the add-to-cart payload from the current selection.\n *\n * A fixed bundle is all-or-nothing, so every product contributes a line.\n * `priceCents` is the line total (unit × qty) and feeds analytics only.\n */\nexport function buildCartItems(products: BundleProduct[]): CartItem[] {\n const items: CartItem[] = [];\n for (let i = 0; i < products.length; i++) {\n const variant = findVariant(products[i], products[i].selectedVariantId);\n const qty = variant.quantity || products[i].quantity || 1;\n items.push({\n variantId: Number(products[i].selectedVariantId),\n quantity: qty,\n priceCents: (variant.price || 0) * qty,\n });\n }\n return items;\n}\n","import type { FormatMoney } from \"../host\";\nimport { findVariant } from \"../product/variants\";\nimport type { BundleProduct } from \"../product/types\";\n\nexport interface BundlePricing {\n totalPrice: number;\n salePrice: number;\n savings: number;\n}\n\n/**\n * Derive the bundle's totals from the currently selected variants.\n *\n * Pure, so the arithmetic can be tested without a DOM. `recalcPricing` below\n * paints the result.\n *\n * `percentage` floors per unit, matching how Shopify's discount engine rounds\n * at checkout — summing then discounting would drift by a cent on some\n * quantities and quote the shopper a total they don't get charged.\n *\n * Currency: `discountValue` for `fixed_amount` / `flat_price` must be in\n * the same currency as `variant.price` — the currency the host renders in.\n * The merchant configures the amount in the shop's STORE currency, and the\n * checkout (Rust discount function) converts it by the input's\n * `presentmentCurrencyRate`, so each host converts before this code runs:\n * the theme entries rewrite `data-discount-value` at hydration from\n * `Shopify.currency.rate`, and the headless SDK converts the parsed bundle\n * with the consumer-supplied `currencyRate` (`convertBundleToPresentment`).\n * This layer stays currency-blind; percentage bundles need no conversion.\n */\nexport function computeBundlePricing(\n products: BundleProduct[],\n discountType: string,\n discountValue: number,\n): BundlePricing {\n let totalPrice = 0;\n let salePrice = 0;\n\n for (let i = 0; i < products.length; i++) {\n const variant = findVariant(products[i], products[i].selectedVariantId);\n const qty = variant.quantity || products[i].quantity || 1;\n const linePrice = variant.price * qty;\n totalPrice += linePrice;\n\n if (discountType === \"percentage\") {\n const unitDiscount = Math.floor((variant.price * discountValue) / 100);\n salePrice += (variant.price - unitDiscount) * qty;\n } else {\n // fixed_amount accumulates undiscounted and is adjusted after the loop.\n salePrice += linePrice;\n }\n }\n\n // applies_to_each_item: false — one deduction from the bundle total.\n if (discountType === \"fixed_amount\") {\n salePrice = Math.max(0, totalPrice - Math.round(discountValue * 100));\n }\n if (discountType === \"flat_price\") {\n const flatCents = Math.round(discountValue * 100);\n salePrice = flatCents < totalPrice ? flatCents : totalPrice;\n }\n\n return { totalPrice, salePrice, savings: totalPrice - salePrice };\n}\n\n/**\n * Recompute and paint the bundle total after a variant change.\n *\n * Distinct from the shared `updatePricing` in dom.ts: that one takes finished\n * numbers, this one derives them from the current selection first.\n */\nexport function recalcPricing(\n container: HTMLElement,\n products: BundleProduct[],\n formatMoney: FormatMoney,\n): void {\n // `container` is an ancestor when Liquid rendered the widget and the root\n // itself when the headless host built it, so check both.\n const fixed = container.classList?.contains(\"lb-fixed\")\n ? container\n : container.querySelector<HTMLElement>(\".lb-fixed\");\n if (!fixed) return;\n\n const { totalPrice, salePrice, savings } = computeBundlePricing(\n products,\n fixed.getAttribute(\"data-discount-type\") || \"\",\n parseFloat(fixed.getAttribute(\"data-discount-value\") ?? \"\") || 0,\n );\n\n const saleEl = container.querySelector<HTMLElement>(\"[data-sale-price]\");\n const compareEl = container.querySelector<HTMLElement>(\"[data-compare-price]\");\n const savingsBar = container.querySelector<HTMLElement>(\"[data-savings-bar]\");\n const savingsAmountEl =\n container.querySelector<HTMLElement>(\"[data-savings-amount]\");\n const savingsPercentEl = container.querySelector<HTMLElement>(\n \"[data-savings-percent]\",\n );\n\n if (saleEl) saleEl.textContent = formatMoney(salePrice);\n\n if (compareEl) {\n if (savings > 0) {\n compareEl.textContent = formatMoney(totalPrice);\n compareEl.style.display = \"\";\n } else {\n compareEl.style.display = \"none\";\n }\n }\n\n if (savingsBar) {\n if (savings > 0) {\n if (savingsAmountEl) savingsAmountEl.textContent = formatMoney(savings);\n if (savingsPercentEl) {\n const pct =\n totalPrice > 0 ? Math.round((savings * 100) / totalPrice) : 0;\n savingsPercentEl.textContent = \"(\" + pct + \"%)\";\n }\n savingsBar.style.display = \"\";\n } else {\n savingsBar.style.display = \"none\";\n }\n }\n}\n","import {\n findVariant,\n findVariantByOptions,\n isOptionValueAvailable,\n} from \"./variants\";\nimport type { BundleProduct, BundleVariant } from \"./types\";\n\n/**\n * Wire a row's per-option `<select>` elements to variant resolution.\n *\n * One select per product option (Size, Colour, …), each tagged with\n * `data-option-position`. On change the selected value of every select in the\n * row is collected, resolved to a variant, and handed to `onSelected` for\n * painting — the caller owns what a row looks like, this owns how a variant is\n * chosen.\n *\n * Shared by fixed and BOGO, which emit the same per-variant JSON from Liquid\n * and previously carried near-identical copies of this logic.\n *\n * `getProduct` is a callback rather than a value because fixed resolves its\n * product through an index map that must be read at event time, not at bind\n * time.\n */\nexport function bindVariantSelects(\n row: HTMLElement,\n getProduct: () => BundleProduct | null | undefined,\n onSelected: (variant: BundleVariant, product: BundleProduct) => void,\n): void {\n const optionSelects =\n row.querySelectorAll<HTMLSelectElement>(\"[data-variant-option]\");\n if (!optionSelects.length) return;\n\n function positionOf(sel: HTMLSelectElement): number {\n return parseInt(sel.getAttribute(\"data-option-position\") ?? \"\", 10);\n }\n\n function collectOptionValues(): string[] {\n const values: string[] = [];\n for (let i = 0; i < optionSelects.length; i++) {\n const pos = positionOf(optionSelects[i]);\n if (!pos || pos < 1) continue;\n values[pos - 1] = optionSelects[i].value;\n }\n return values;\n }\n\n function syncSelectsToVariant(variant: BundleVariant | null): void {\n if (!variant || !variant.options) return;\n for (let i = 0; i < optionSelects.length; i++) {\n const pos = positionOf(optionSelects[i]);\n if (!pos || pos < 1) continue;\n const expected = variant.options[pos - 1];\n if (optionSelects[i].value !== expected) {\n optionSelects[i].value = expected;\n }\n }\n }\n\n /** Grey out values that don't pair with the current selection (Dawn-style). */\n function recomputeDisabledState(\n product: BundleProduct,\n selected: string[],\n ): void {\n for (let i = 0; i < optionSelects.length; i++) {\n const sel = optionSelects[i];\n const pos = positionOf(sel);\n if (!pos || pos < 1) continue;\n const opts = sel.options;\n for (let o = 0; o < opts.length; o++) {\n opts[o].disabled = !isOptionValueAvailable(\n product.variants,\n pos - 1,\n opts[o].value,\n selected,\n );\n }\n }\n }\n\n const seeded = getProduct();\n if (seeded) {\n const initialVariant = findVariant(seeded, seeded.selectedVariantId);\n if (initialVariant && initialVariant.options) {\n recomputeDisabledState(seeded, initialVariant.options);\n }\n }\n\n for (let s = 0; s < optionSelects.length; s++) {\n optionSelects[s].addEventListener(\"change\", (e) => {\n // Keep the theme's product-page JS out of this. Dawn's\n // MediaGallery.preloadImage throws when it can't find the bundle's\n // variant among the product's own.\n e.stopPropagation();\n\n const product = getProduct();\n if (!product) return;\n\n const variant = findVariantByOptions(product, collectOptionValues());\n if (!variant) {\n // A disabled value got through — some browsers let you\n // keyboard-select a disabled <option>. Revert rather than jump to an\n // unrelated combination.\n const prev = findVariant(product, product.selectedVariantId);\n syncSelectsToVariant(prev);\n if (prev && prev.options) recomputeDisabledState(product, prev.options);\n return;\n }\n\n product.selectedVariantId = variant.id;\n recomputeDisabledState(product, variant.options);\n onSelected(variant, product);\n });\n }\n}\n","import type { BundleProduct, BundleVariant } from \"./types\";\n\n/**\n * Fill the geometry skeleton Liquid rendered.\n *\n * Liquid emits the row's structure and the product title — the only piece\n * whose height it cannot predict, because text wrapping depends on the\n * merchant's font. Every other box is emitted empty at its final height (see\n * the `:empty` reservations in bundle-base.css) and filled here, so the row\n * never changes size between first paint and hydration.\n *\n * Filling rather than replacing is deliberate: replacing the row would remove\n * and re-add layout boxes, which is a shift even when the dimensions match.\n *\n * Only the controls are built here — the badge and the option selects. The\n * caller paints prices, because fixed and BOGO price a row differently (BOGO's\n * reward side shows a struck original beside the discounted unit).\n */\nexport function hydrateRowControls(\n row: HTMLElement,\n product: BundleProduct,\n selected: BundleVariant,\n): void {\n if (!product.variants.length) return;\n\n // A single eligible variant has nothing to choose, so it reads as a label.\n // More than one gets the option selects. Liquid decided which by emitting\n // the matching empty container, so follow what it reserved.\n const badge = row.querySelector<HTMLElement>(\".lb-bundle-variant-badge\");\n if (badge && !badge.textContent) {\n badge.textContent = selected.title;\n }\n\n const groups = row.querySelector<HTMLElement>(\n \".lb-bundle-variant-option-groups\",\n );\n if (groups && !groups.children.length) {\n buildOptionGroups(groups, product, selected);\n }\n}\n\n/** One `<select>` per product option, values de-duplicated in first-seen order. */\nfunction buildOptionGroups(\n container: HTMLElement,\n product: BundleProduct,\n selected: BundleVariant,\n): void {\n const optionNames = product.optionNames || [];\n\n for (let i = 0; i < optionNames.length; i++) {\n const group = document.createElement(\"div\");\n group.className = \"lb-bundle-variant-option-group\";\n\n const label = document.createElement(\"span\");\n label.className = \"lb-bundle-variant-option-label\";\n label.textContent = optionNames[i];\n group.appendChild(label);\n\n const select = document.createElement(\"select\");\n select.className = \"lb-bundle-variant-select\";\n select.setAttribute(\"data-variant-option\", \"\");\n select.setAttribute(\"data-option-position\", String(i + 1));\n select.setAttribute(\"data-product-id\", String(product.productId));\n select.name = \"lb-variant-\" + product.productId + \"-\" + (i + 1);\n select.setAttribute(\"aria-label\", optionNames[i]);\n\n const seen: Record<string, boolean> = Object.create(null);\n for (const v of product.variants) {\n const value = v.options && v.options[i];\n if (!value || seen[value]) continue;\n seen[value] = true;\n\n const opt = document.createElement(\"option\");\n opt.value = value;\n opt.textContent = value;\n if (selected.options && selected.options[i] === value) {\n opt.selected = true;\n }\n select.appendChild(opt);\n }\n\n group.appendChild(select);\n container.appendChild(group);\n }\n}\n","import type { FormatMoney } from \"../host\";\nimport type { BundleProduct, BundleVariant } from \"./types\";\n\n/**\n * Remembers each row's original image so a variant without one can fall back.\n *\n * Seeded from the product data when present, else from whatever `<img>` the\n * row already had — the skeleton rows Liquid emits start with an empty\n * thumbnail, so there is nothing to read off the DOM.\n */\nconst productImgSrc = new WeakMap<HTMLElement, string>();\n\n/**\n * Point the row's thumbnail at the selected variant's image.\n *\n * Falls back to the row's initial `img.src` — the Liquid-rendered product hero\n * — so a variant with no image of its own doesn't leave the thumbnail stuck on\n * the previously selected variant's photo.\n */\nexport function updateThumbnail(\n row: HTMLElement,\n variant: BundleVariant,\n productImage?: string | null,\n): void {\n const thumb = row.querySelector<HTMLElement>(\"[data-thumbnail]\");\n if (!thumb) return;\n\n const img = thumb.querySelector(\"img\");\n if (!productImgSrc.has(row)) {\n const seed = productImage || (img ? img.src : null);\n if (seed) productImgSrc.set(row, seed);\n }\n\n const nextSrc = variant.image || productImgSrc.get(row) || null;\n if (!nextSrc) return;\n\n if (img) {\n img.src = nextSrc;\n // The Liquid srcset describes the original image, so it has to go or the\n // browser may keep serving a candidate from the previous variant.\n img.srcset = \"\";\n } else {\n // The row had only the placeholder SVG. Build an <img> rather than\n // assigning innerHTML, which would make the image URL an injection point.\n const newImg = document.createElement(\"img\");\n newImg.src = nextSrc;\n newImg.loading = \"lazy\";\n newImg.alt = \"\";\n thumb.textContent = \"\";\n thumb.appendChild(newImg);\n }\n}\n\n/**\n * Update the inline \"×N\" beside a row's price.\n *\n * Always rendered, even at ×1, so the quantity reads as a consistent column\n * down the bundle manifest.\n */\nexport function updateInlineQuantity(row: HTMLElement, qty: number): void {\n const el = row.querySelector<HTMLElement>(\"[data-qty-inline]\");\n if (!el) return;\n el.textContent = \"×\" + qty;\n}\n\n/**\n * Paint a row to reflect a newly selected variant.\n *\n * Purely visual — `bindVariantSelects` owns `product.selectedVariantId`.\n */\nexport function applyVariant(\n row: HTMLElement,\n variant: BundleVariant | null,\n product: BundleProduct,\n formatMoney: FormatMoney,\n): void {\n if (!variant) return;\n\n updateThumbnail(row, variant, product.featuredImage);\n updateInlineQuantity(row, variant.quantity || product.quantity || 1);\n\n const priceEl = row.querySelector<HTMLElement>(\"[data-product-price]\");\n if (priceEl && variant.price != null) {\n priceEl.textContent = formatMoney(variant.price);\n }\n\n const compareEl = row.querySelector<HTMLElement>(\n \"[data-product-compare-price]\",\n );\n if (compareEl) {\n const compare = variant.compareAtPrice;\n if (compare && compare > variant.price) {\n compareEl.textContent = formatMoney(compare);\n compareEl.hidden = false;\n } else {\n compareEl.textContent = \"\";\n compareEl.hidden = true;\n }\n }\n\n const unitEl = row.querySelector<HTMLElement>(\"[data-product-unit-price]\");\n if (unitEl) {\n if (variant.unitPrice) {\n unitEl.textContent = variant.unitPrice;\n unitEl.hidden = false;\n } else {\n unitEl.textContent = \"\";\n unitEl.hidden = true;\n }\n }\n}\n","import type { BundleProduct } from \"./product/types\";\nimport type { PickerTranslations } from \"./picker/types\";\n\n/**\n * The DOM twin of the row skeleton `lb-fixed.liquid` emits.\n *\n * The theme host gets this markup from Liquid, server-rendered, and hydrates\n * it. The headless host has no Liquid, so it builds the same thing here and\n * hydrates it identically — one hydration path, two ways of getting the boxes\n * onto the page.\n *\n * **These must stay in step.** `app/__tests__/skeleton-liquid-parity.test.ts`\n * compares this output against the structure the Liquid emits; the CSS `:empty`\n * reservations are keyed to these exact class names, so a box added here\n * without one added there (and vice versa) is a layout shift.\n */\n\n/** Marks an element as an empty box the hydration step will fill. */\nfunction box(tag: string, className: string, attrs?: Record<string, string>) {\n const el = document.createElement(tag);\n el.className = className;\n for (const k in attrs) el.setAttribute(k, attrs[k]);\n return el;\n}\n\nexport interface RowSkeletonOptions {\n /** Greys the row and replaces its controls with an out-of-stock label. */\n isOos: boolean;\n /** Reward chip text for BOGO's get side. */\n badgeText?: string | null;\n /** BOGO only: which side of the offer this row is. */\n bogoRole?: \"buy\" | \"get\";\n /** BOGO renders its fixed quantity inline; fixed uses a separate chip. */\n inlineQuantity?: number | null;\n /** Product page link. Omitted when the host has no URL for the product. */\n href?: string | null;\n /**\n * Merchant `productList.thumbnailRatio`. Only \"original\" has any effect\n * here: that setting sizes each box to its image's own proportions, which\n * nothing knows before the image loads, so the box gets the ratio stamped\n * inline as `--lb-row-thumb-ratio` — the same stamp the Liquid rows emit.\n * The fixed ratios (square, tall, wide) reserve via the widget-level CSS\n * var, and stamping them here would override the merchant's choice with\n * the image's own (see widget-thumbnail-ratio-override.test.ts).\n */\n thumbnailRatio?: string;\n}\n\n/**\n * Build one product row, empty apart from its title.\n *\n * The title is the only content rendered up front, here as in Liquid: its\n * height depends on where the merchant's font wraps it, so no reservation can\n * predict it.\n */\nexport function buildRowSkeleton(\n product: BundleProduct,\n t: PickerTranslations & { outOfStock?: string },\n opts: RowSkeletonOptions,\n): HTMLElement {\n const eligible = product.variants.length;\n\n const row = box(\"div\", \"lb-bundle-product-row\");\n if (opts.isOos) {\n row.classList.add(\"lb-bundle-product-row--oos\");\n row.setAttribute(\"aria-disabled\", \"true\");\n }\n row.setAttribute(\"data-product-id\", String(product.productId));\n if (opts.bogoRole) row.setAttribute(\"data-bogo-role\", opts.bogoRole);\n\n const thumb = box(\"div\", \"lb-bundle-thumbnail\", { \"data-thumbnail\": \"\" });\n // Liquid guards this on `thumbnail_ratio == 'original' and bp.featured_image`;\n // a product without an image keeps the placeholder box, unstamped.\n const ratio = product.featuredImageRatio;\n if (opts.thumbnailRatio === \"original\" && ratio != null && ratio > 0) {\n thumb.style.setProperty(\"--lb-row-thumb-ratio\", String(ratio));\n }\n row.appendChild(thumb);\n\n const info = box(\"div\", \"lb-bundle-product-info\");\n\n const title = document.createElement(opts.href ? \"a\" : \"span\");\n title.className = \"lb-bundle-product-name\";\n if (opts.href) (title as HTMLAnchorElement).href = opts.href;\n title.textContent = product.title ?? \"\";\n info.appendChild(title);\n\n if (opts.isOos) {\n const oos = box(\"span\", \"lb-bundle-oos-label\");\n oos.textContent = t.outOfStock || \"Out of stock\";\n info.appendChild(oos);\n row.appendChild(info);\n return row;\n }\n\n // A single eligible variant has nothing to choose, so it reads as a label.\n if (eligible === 1 && product.variants[0]?.title !== \"Default Title\") {\n info.appendChild(box(\"span\", \"lb-bundle-variant-badge\"));\n }\n\n const prices = box(\"span\", \"lb-bundle-product-prices\");\n prices.appendChild(\n box(\"span\", \"lb-bundle-product-compare-price\", {\n \"data-product-compare-price\": \"\",\n hidden: \"\",\n }),\n );\n prices.appendChild(\n box(\"span\", \"lb-bundle-product-price\", { \"data-product-price\": \"\" }),\n );\n if (opts.inlineQuantity != null) {\n const qty = box(\"span\", \"lb-bundle-qty-inline\", { \"data-qty-inline\": \"\" });\n qty.textContent = \"×\" + opts.inlineQuantity;\n prices.appendChild(qty);\n }\n info.appendChild(prices);\n\n info.appendChild(\n box(\"span\", \"lb-bundle-product-unit-price\", {\n \"data-product-unit-price\": \"\",\n }),\n );\n\n if (eligible > 1) {\n info.appendChild(\n box(\"div\", \"lb-bundle-variant-option-groups\", {\n // Drives the `:empty` height reservation, exactly as the Liquid's\n // inline style does.\n style: \"--lb-row-option-count: \" + (product.optionNames.length || 1),\n }),\n );\n }\n\n row.appendChild(info);\n\n if (opts.inlineQuantity == null) {\n row.appendChild(box(\"span\", \"lb-bundle-qty-chip\", { \"data-qty-inline\": \"\" }));\n }\n if (opts.badgeText) {\n const badge = box(\"span\", \"lb-bogo__badge\");\n badge.textContent = opts.badgeText;\n row.appendChild(badge);\n }\n\n return row;\n}\n\nexport interface ShellOptions {\n title: string;\n /** Volume labels its summary \"Total\"; the others use \"Bundle price\". */\n summaryLabel?: string;\n /** Volume only: a slot for the \"(N items)\" count beside the label. */\n withItemCount?: boolean;\n /** Volume paints its total into [data-total-price] rather than the sale slot. */\n totalPriceSlot?: boolean;\n subtitle?: string | null;\n showSavingsBar: boolean;\n showComparePrice: boolean;\n ctaText: string;\n /** ISO end date; renders the countdown strip when the merchant set one. */\n endsAt?: string | null;\n countdownLabel?: string;\n}\n\nexport interface WidgetShell {\n root: HTMLElement;\n /** Where rows go. */\n products: HTMLElement;\n cta: HTMLButtonElement;\n}\n\n/**\n * Build the widget chrome the theme gets from Liquid: header, product list,\n * divider, price summary and CTA.\n *\n * Mirrors the anatomy contract in WIDGET-DESIGN.md §1 — the section order is\n * load-bearing, because the CSS targets siblings by position in places.\n */\nexport function buildWidgetShell(\n typeClass: string,\n opts: ShellOptions,\n): WidgetShell {\n const root = box(\"div\", typeClass);\n\n const header = box(\"div\", \"lb-bundle-header\");\n const heading = box(\"h3\", \"lb-bundle-title\");\n heading.textContent = opts.title;\n header.appendChild(heading);\n if (opts.subtitle) {\n const sub = box(\"p\", \"lb-bundle-subtitle\");\n sub.textContent = opts.subtitle;\n header.appendChild(sub);\n }\n root.appendChild(header);\n\n if (opts.endsAt) {\n const countdown = box(\"div\", \"lb-bundle-countdown\", {\n \"data-countdown\": \"\",\n });\n const label = box(\"span\", \"lb-bundle-countdown-label\");\n // Matches the theme's `countdown_label` translation default.\n label.textContent = opts.countdownLabel ?? \"Limited time offer\";\n countdown.appendChild(label);\n countdown.appendChild(\n box(\"span\", \"lb-bundle-countdown-timer\", {\n \"data-countdown-timer\": \"\",\n }),\n );\n root.appendChild(countdown);\n }\n\n const products = box(\"div\", typeClass + \"__products lb-edge-fade\");\n root.appendChild(products);\n\n root.appendChild(box(\"div\", \"lb-bundle-divider\"));\n\n // `data-pricing-section` is the hook the mix-and-match and multi-step\n // pricing updaters hide until the bundle qualifies — the same attribute\n // the Liquid summaries carry. The other types never touch it.\n const summary = box(\"div\", \"lb-bundle-summary\", {\n \"data-pricing-section\": \"\",\n });\n const text = box(\"div\", \"lb-bundle-summary__text\");\n const label = box(\"span\", \"lb-bundle-summary__label\");\n label.textContent = opts.summaryLabel ?? \"Bundle total\";\n text.appendChild(label);\n if (opts.withItemCount) {\n // Volume writes \"(3 items)\" beside the label as the tier changes.\n label.appendChild(box(\"span\", \"\", { \"data-item-count\": \"\" }));\n }\n if (opts.showSavingsBar) {\n const savings = box(\"p\", \"lb-bundle-savings-line\", {\n \"data-savings-bar\": \"\",\n style: \"display:none\",\n });\n savings.appendChild(document.createTextNode(\"Save \"));\n savings.appendChild(box(\"span\", \"\", { \"data-savings-amount\": \"\" }));\n savings.appendChild(document.createTextNode(\" \"));\n savings.appendChild(box(\"span\", \"\", { \"data-savings-percent\": \"\" }));\n text.appendChild(savings);\n }\n summary.appendChild(text);\n\n const prices = box(\"span\", \"lb-bundle-summary__prices\");\n if (opts.showComparePrice) {\n prices.appendChild(\n box(\"span\", \"lb-bundle-compare-price\", { \"data-compare-price\": \"\" }),\n );\n }\n prices.appendChild(\n box(\"span\", \"lb-bundle-sale-price\", {\n [opts.totalPriceSlot ? \"data-total-price\" : \"data-sale-price\"]: \"\",\n }),\n );\n summary.appendChild(prices);\n root.appendChild(summary);\n\n // `data-cta-text` mirrors the attribute the Liquid CTA carries: the state\n // repainters (mix-match slots, multi-step body) re-derive the idle label\n // from it, so without the attribute a repaint would wipe the merchant's\n // custom text back to the translation default.\n const cta = box(\"button\", \"lb-bundle-cta\", {\n \"data-cta-text\": opts.ctaText,\n type: \"button\",\n \"data-add-bundle\": \"\",\n }) as HTMLButtonElement;\n const ctaLabel = box(\"span\", \"lb-cta-label\", { \"data-cta-label\": \"\" });\n ctaLabel.textContent = opts.ctaText;\n cta.appendChild(ctaLabel);\n root.appendChild(cta);\n\n root.appendChild(\n box(\"p\", \"lb-bundle-error\", { \"data-error\": \"\", \"aria-live\": \"polite\" }),\n );\n root.appendChild(\n box(\"span\", \"lb-visually-hidden\", {\n \"data-status\": \"\",\n \"aria-live\": \"polite\",\n }),\n );\n\n return { root, products, cta };\n}\n\nexport interface TierSpec {\n /** Units this tier requires. */\n quantity: number;\n /** Percentage off, or 0 for an amount-based tier. */\n percent: number;\n /** Cents off per unit, or 0 for a percentage tier. */\n amountCents: number;\n label: string;\n savingsLabel?: string | null;\n badgeLabel?: string | null;\n selected: boolean;\n}\n\nexport interface VolumeTierOptions {\n /** Mirror of `pricing.showComparePrice` — the struck base price per tier. */\n showCompare?: boolean;\n /** Mirror of `pricing.showPerUnitPrice` — the discounted \"each\" price. */\n showPriceEach?: boolean;\n /** Text of the unit label after the per-unit price (\"each\" on the theme). */\n unitLabel?: string | null;\n}\n\n/**\n * The DOM twin of the tier list `lb-volume.liquid` emits.\n *\n * The `data-tier-*` attributes are the contract: `selectTier` and\n * `updateAllTierPrices` in render/volume/pricing.ts read the quantity and the\n * discount straight off the element, so the same code drives the list whether\n * Liquid or this built it. Like the Liquid, the compare span only exists on\n * tiers that actually discount, and the price nodes are emitted empty — the\n * host fills them via `updateAllTierPrices` once it knows the base price.\n */\nexport function buildVolumeTiers(\n tiers: TierSpec[],\n groupLabel: string,\n opts: VolumeTierOptions = {},\n): HTMLElement {\n const showCompare = opts.showCompare !== false;\n const showPriceEach = opts.showPriceEach !== false;\n const group = box(\"div\", \"lb-volume__tiers lb-edge-fade\", {\n role: \"radiogroup\",\n \"aria-label\": groupLabel,\n \"data-tier-group\": \"\",\n });\n\n tiers.forEach((tier, i) => {\n const el = box(\"div\", \"lb-volume__tier\", {\n role: \"radio\",\n \"aria-checked\": tier.selected ? \"true\" : \"false\",\n // Roving tabindex: the group is one tab stop and arrows move within it.\n tabindex: tier.selected ? \"0\" : \"-1\",\n \"data-tier-index\": String(i),\n \"data-tier-qty\": String(tier.quantity),\n \"data-tier-pct\": String(tier.percent),\n \"data-tier-amt\": String(tier.amountCents),\n });\n\n const radio = box(\"span\", \"lb-volume__radio\");\n radio.appendChild(box(\"span\", \"lb-volume__radio-dot\"));\n el.appendChild(radio);\n\n const info = box(\"span\", \"lb-volume__tier-info\");\n const label = box(\"span\", \"lb-volume__tier-label\");\n label.textContent = tier.label;\n info.appendChild(label);\n\n const price = box(\"span\", \"lb-volume__tier-price\");\n if (showCompare && (tier.percent > 0 || tier.amountCents > 0)) {\n price.appendChild(box(\"span\", \"lb-volume__tier-compare\"));\n }\n if (showPriceEach) {\n price.appendChild(box(\"span\", \"\", { \"data-tier-price-each\": \"\" }));\n if (opts.unitLabel) {\n const unit = box(\"span\", \"lb-volume__tier-unit\");\n unit.textContent = opts.unitLabel;\n price.appendChild(unit);\n }\n }\n info.appendChild(price);\n el.appendChild(info);\n\n if (tier.badgeLabel || tier.savingsLabel) {\n const right = box(\"span\", \"lb-volume__tier-right\");\n if (tier.badgeLabel) {\n const badge = box(\"span\", \"lb-volume__tier-badge\");\n badge.textContent = tier.badgeLabel;\n right.appendChild(badge);\n }\n if (tier.savingsLabel) {\n const savings = box(\"span\", \"lb-volume__tier-savings\");\n savings.textContent = tier.savingsLabel;\n right.appendChild(savings);\n }\n el.appendChild(right);\n }\n\n group.appendChild(el);\n });\n\n return group;\n}\n\n/**\n * The DOM twin of the progress bar in `lb-mix-match.liquid`.\n *\n * One segment per required unit, plus the two live-region labels\n * `updateProgress` writes into. Multi-step passes its step count instead, so\n * the same bar reads \"2 of 3 steps completed\".\n */\nexport function buildProgressBar(segmentCount: number): HTMLElement {\n const wrap = box(\"div\", \"lb-mix-match__progress\", { \"data-progress\": \"\" });\n\n const segments = box(\"div\", \"lb-mix-match__progress-segments\", {\n role: \"progressbar\",\n \"aria-valuenow\": \"0\",\n \"aria-valuemin\": \"0\",\n \"aria-valuemax\": String(segmentCount),\n });\n for (let i = 0; i < segmentCount; i++) {\n segments.appendChild(\n box(\"span\", \"lb-mix-match__progress-segment\", {\n \"data-progress-segment\": \"\",\n }),\n );\n }\n wrap.appendChild(segments);\n\n const labels = box(\"div\", \"lb-mix-match__progress-labels\");\n labels.appendChild(\n box(\"span\", \"lb-mix-match__progress-count\", {\n \"data-progress-count\": \"\",\n \"aria-live\": \"polite\",\n }),\n );\n labels.appendChild(\n box(\"span\", \"lb-mix-match__progress-remaining\", {\n \"data-progress-remaining\": \"\",\n \"aria-live\": \"polite\",\n }),\n );\n wrap.appendChild(labels);\n\n return wrap;\n}\n","declare global {\n interface HTMLElement {\n /** Handle for the ticking timer, so a re-init can clear the previous one. */\n _lbCountdownInterval?: ReturnType<typeof setInterval> | null;\n }\n}\n\nfunction pad(n: number): string {\n return n < 10 ? \"0\" + n : String(n);\n}\n\n/**\n * Drive the \"ends in\" timer from the widget's `data-ends-at`, and hide the\n * whole widget once the window closes.\n *\n * Host-agnostic: it reads the end date off the DOM, so the theme (where Liquid\n * writes the attribute) and the headless element (where the renderer does)\n * both use it unchanged.\n *\n * Clears any prior interval first: the theme editor re-runs init on every\n * section reload, and without this each reload would leave another timer\n * ticking against a detached node.\n */\nexport function initCountdown(container: HTMLElement): void {\n if (container._lbCountdownInterval) {\n clearInterval(container._lbCountdownInterval);\n container._lbCountdownInterval = null;\n }\n\n const countdown = container.querySelector<HTMLElement>(\"[data-countdown]\");\n if (!countdown) return;\n\n const widget = container.closest<HTMLElement>(\".lb-bundle-widget\");\n const endsAt = widget && widget.getAttribute(\"data-ends-at\");\n if (!endsAt) {\n countdown.style.display = \"none\";\n return;\n }\n\n const endTime = new Date(endsAt).getTime();\n if (isNaN(endTime)) {\n countdown.style.display = \"none\";\n return;\n }\n\n const timerEl = countdown.querySelector<HTMLElement>(\"[data-countdown-timer]\");\n if (!timerEl) return;\n\n let interval: ReturnType<typeof setInterval> | undefined;\n\n function update(): void {\n const remaining = endTime - Date.now();\n if (remaining <= 0) {\n if (widget) widget.style.display = \"none\";\n if (interval) clearInterval(interval);\n return;\n }\n const days = Math.floor(remaining / 86400000);\n const hours = Math.floor((remaining % 86400000) / 3600000);\n const mins = Math.floor((remaining % 3600000) / 60000);\n const secs = Math.floor((remaining % 60000) / 1000);\n timerEl!.textContent =\n days > 0\n ? days + \"d \" + pad(hours) + \"h \" + pad(mins) + \"m \" + pad(secs) + \"s\"\n : pad(hours) + \"h \" + pad(mins) + \"m \" + pad(secs) + \"s\";\n }\n\n update();\n if (endTime - Date.now() > 0) {\n interval = setInterval(update, 1000);\n container._lbCountdownInterval = interval;\n }\n}\n","import type { PickerTranslations } from \"./picker/types\";\n\n/**\n * English fallbacks for hosts without a translation system.\n *\n * The theme app extension never uses these — Shopify's `t` filter has already\n * resolved every string into the data block by the time the render layer runs,\n * in the shopper's language. The headless element has no equivalent, so this\n * is what it ships with, and a consumer can override any of it.\n *\n * `__COUNT__`-style placeholders match the Liquid locale files, so the two sets\n * are interchangeable.\n */\nexport interface BundleStrings extends PickerTranslations {\n outOfStock?: string;\n /** \"__COUNT__ items out of stock\". Either one template for every count, or\n * plural forms keyed by Intl.PluralRules category so \"1 items\" can't\n * happen. */\n itemsOutOfStock?: string | Record<string, string>;\n addToCart?: string;\n bundlePrice?: string;\n save?: string;\n complete?: string;\n moreToGo?: string;\n bundleComplete?: string;\n chooseMoreUnlock?: string;\n chooseMoreComplete?: string;\n editSelection?: string;\n stepOf?: string;\n stepsCompleted?: string;\n doorChoose?: string;\n doorContinue?: string;\n doorQuickSteps?: string | Record<string, string>;\n doorStepsToGo?: Record<string, string>;\n each?: string;\n buyQty?: string;\n /** Plural forms for the summary \"(N items)\" label, keyed by\n * Intl.PluralRules category — the shape `formatItemCount` expects and the\n * Liquid `item_count` translation carries. */\n itemCount?: Record<string, string>;\n}\n\nexport const DEFAULT_STRINGS: BundleStrings = {\n locale: \"en\",\n addItem: \"Add\",\n addedItem: \"Added\",\n removeItem: \"Remove\",\n swapItem: \"Swap\",\n required: \"Required\",\n soldOut: \"Sold out\",\n outOfStock: \"Out of stock\",\n itemsOutOfStock: {\n one: \"__COUNT__ item out of stock\",\n other: \"__COUNT__ items out of stock\",\n },\n quantity: \"Quantity\",\n increaseQuantity: \"Increase quantity\",\n decreaseQuantity: \"Decrease quantity\",\n allTypes: \"All\",\n editSelection: \"Edit selection\",\n addToCart: \"Add to cart\",\n bundlePrice: \"Bundle price\",\n save: \"Save\",\n complete: \"Complete\",\n ofSelected: \"__COUNT__ of __TOTAL__ added\",\n moreToGo: \"__COUNT__ more to go\",\n nProductsShown: \"__COUNT__ products shown\",\n bundleComplete: \"Your bundle is complete\",\n chooseMoreUnlock: \"Choose __COUNT__ more to unlock __PCT__% off\",\n chooseMoreComplete: \"Choose __COUNT__ more to complete your bundle\",\n stepOf: \"Step __STEP__ of __TOTAL__\",\n stepsCompleted: \"__COUNT__ of __TOTAL__ steps completed\",\n doorChoose: \"Choose your products\",\n doorContinue: \"Continue building\",\n doorQuickSteps: {\n one: \"__COUNT__ quick step\",\n other: \"__COUNT__ quick steps\",\n },\n doorStepsToGo: {\n one: \"__COUNT__ step to go\",\n other: \"__COUNT__ steps to go\",\n },\n each: \" each\",\n buyQty: \"Buy __COUNT__\",\n itemCount: {\n one: \"__COUNT__ item\",\n other: \"__COUNT__ items\",\n },\n needsSpots: {\n one: \"Needs __COUNT__ spot\",\n other: \"Needs __COUNT__ spots\",\n },\n};\n\n/**\n * Resolve the disabled-CTA \"N items out of stock\" label for a count,\n * accepting either the single-template or the plural-map shape of\n * `itemsOutOfStock`.\n */\nexport function formatItemsOutOfStock(\n t: BundleStrings,\n count: number,\n): string {\n const raw = t.itemsOutOfStock;\n let tpl: string | undefined;\n if (raw && typeof raw === \"object\") {\n const form = new Intl.PluralRules(t.locale || \"en\").select(count);\n tpl = raw[form] || raw.other;\n } else {\n tpl = raw;\n }\n return (tpl || \"__COUNT__ items out of stock\")\n .split(\"__COUNT__\")\n .join(String(count));\n}\n","/**\n * Nearest ancestor that clips overflow on the Y axis.\n *\n * The listbox flips upward before it would be hidden behind a scrollable\n * container such as `.lb-fixed__products`. Stops at `<body>` — past that the\n * viewport bound is the correct constraint.\n */\nexport function findScrollableAncestor(el: HTMLElement): HTMLElement | null {\n const doc = el.ownerDocument;\n const win = doc && doc.defaultView;\n if (!win) return null;\n\n let cur = el.parentElement;\n while (cur && cur !== doc.body) {\n const overflowY = win.getComputedStyle(cur).overflowY;\n if (overflowY === \"auto\" || overflowY === \"scroll\" || overflowY === \"hidden\") {\n return cur;\n }\n cur = cur.parentElement;\n }\n return null;\n}\n","export interface DropdownInstance {\n shell: HTMLElement;\n listbox: HTMLElement;\n select: HTMLSelectElement;\n close: () => void;\n destroy: () => void;\n}\n\n/**\n * Every currently open dropdown, newest last.\n *\n * Module-level so document listeners are attached once for the page rather\n * than once per dropdown, and torn down again when the last one closes.\n */\nconst openInstances: DropdownInstance[] = [];\nlet documentListenersAttached = false;\n\n/**\n * Whether an event originated inside a dropdown.\n *\n * Checks both shell and listbox because the listbox is portaled to the modal\n * overlay in the mix-and-match picker and so is not a descendant of the shell.\n * `composedPath` also handles shadow-DOM retargeting, should a theme ever wrap\n * the widget in a custom element.\n */\nfunction eventInsideDropdown(event: Event, inst: DropdownInstance): boolean {\n if (event.composedPath) {\n const path = event.composedPath();\n for (let p = 0; p < path.length; p++) {\n if (path[p] === inst.shell || path[p] === inst.listbox) return true;\n }\n return false;\n }\n const target = event.target as Node;\n return inst.shell.contains(target) || inst.listbox.contains(target);\n}\n\nfunction closeAllOutside(event: Event): void {\n for (let i = openInstances.length - 1; i >= 0; i--) {\n if (!eventInsideDropdown(event, openInstances[i])) openInstances[i].close();\n }\n}\n\nfunction onWindowResize(): void {\n for (let i = openInstances.length - 1; i >= 0; i--) openInstances[i].close();\n}\n\nfunction attachDocListeners(): void {\n if (documentListenersAttached) return;\n document.addEventListener(\"pointerdown\", closeAllOutside, true);\n window.addEventListener(\"resize\", onWindowResize);\n // Capture phase, because scroll doesn't bubble. Scrolling the listbox\n // itself is excluded by eventInsideDropdown — the panel has overflow-y:auto\n // for long variant lists and must not close itself.\n window.addEventListener(\"scroll\", closeAllOutside, true);\n documentListenersAttached = true;\n}\n\nfunction detachDocListeners(): void {\n if (!documentListenersAttached) return;\n document.removeEventListener(\"pointerdown\", closeAllOutside, true);\n window.removeEventListener(\"resize\", onWindowResize);\n window.removeEventListener(\"scroll\", closeAllOutside, true);\n documentListenersAttached = false;\n}\n\n/** Register a newly opened dropdown, attaching page listeners if it's the first. */\nexport function registerOpen(instance: DropdownInstance): void {\n openInstances.push(instance);\n if (openInstances.length === 1) attachDocListeners();\n}\n\n/** Deregister a closed dropdown, detaching page listeners if it was the last. */\nexport function registerClosed(instance: DropdownInstance): void {\n const idx = openInstances.indexOf(instance);\n if (idx >= 0) openInstances.splice(idx, 1);\n if (openInstances.length === 0) detachDocListeners();\n}\n\n/** Close every open dropdown except one. Only a single dropdown may be open. */\nexport function closeOthers(except: DropdownInstance | undefined): void {\n for (let i = openInstances.length - 1; i >= 0; i--) {\n if (openInstances[i] !== except) openInstances[i].close();\n }\n}\n","// Namespaced in core to keep its top-level surface clean.\nimport { dropdown } from \"@lime-bundles/core\";\nimport { findScrollableAncestor } from \"./scroll\";\nimport {\n closeOthers,\n registerClosed,\n registerOpen,\n type DropdownInstance,\n} from \"./open-registry\";\n\n/**\n * Listbox layout constants.\n *\n * Deliberately local rather than shared from `@lime-bundles/core`: the\n * algorithm is the contract, these numbers are this surface's styling and are\n * expected to differ between the theme widget and the headless renderer.\n * Must match `.lb-dropdown-option` in bundle-dropdown.css.\n */\nconst ITEM_HEIGHT = 32;\nconst LIST_PADDING_Y = 8;\nconst MAX_VISIBLE_ITEMS = 8;\n\n/** Instance per bound `<select>`, so rebinding and teardown can find it. */\nconst instances = new WeakMap<HTMLSelectElement, DropdownInstance>();\n\nexport function instanceFor(\n select: HTMLSelectElement,\n): DropdownInstance | undefined {\n return instances.get(select);\n}\n\ninterface OptionState {\n disabled: boolean;\n label: string;\n}\n\n/**\n * Replace a native `<select>` with an accessible custom listbox.\n *\n * The original element stays in the DOM as the form value and the source of\n * truth — every commit writes back to it and dispatches `change`, so anything\n * listening to the select (the variant pickers, the theme) is unaffected by\n * the swap.\n */\nexport function bindDropdown(\n selectEl: HTMLSelectElement,\n): DropdownInstance | null {\n if (selectEl.classList.contains(\"lb-dropdown-state\")) return null;\n\n const doc = document;\n const labelText = selectEl.getAttribute(\"aria-label\") || \"\";\n const idBase = \"lb-dd-\" + Math.random().toString(36).slice(2, 9);\n\n selectEl.classList.add(\"lb-dropdown-state\");\n selectEl.setAttribute(\"aria-hidden\", \"true\");\n selectEl.setAttribute(\"tabindex\", \"-1\");\n\n const shell = doc.createElement(\"div\");\n shell.className = \"lb-dropdown\";\n shell.setAttribute(\"data-lb-dropdown\", \"\");\n\n const trigger = doc.createElement(\"button\");\n trigger.type = \"button\";\n trigger.className = \"lb-dropdown-trigger\";\n trigger.setAttribute(\"role\", \"combobox\");\n trigger.setAttribute(\"aria-haspopup\", \"listbox\");\n trigger.setAttribute(\"aria-expanded\", \"false\");\n\n const listboxId = idBase + \"-listbox\";\n trigger.setAttribute(\"aria-controls\", listboxId);\n if (labelText) trigger.setAttribute(\"aria-label\", labelText);\n\n const triggerLabel = doc.createElement(\"span\");\n triggerLabel.className = \"lb-dropdown-trigger-value\";\n\n const chevron = doc.createElement(\"span\");\n chevron.className = \"lb-dropdown-chevron\";\n chevron.setAttribute(\"aria-hidden\", \"true\");\n\n trigger.appendChild(triggerLabel);\n trigger.appendChild(chevron);\n\n const listbox = doc.createElement(\"ul\");\n listbox.id = listboxId;\n listbox.className = \"lb-dropdown-listbox\";\n listbox.setAttribute(\"role\", \"listbox\");\n if (labelText) listbox.setAttribute(\"aria-label\", labelText);\n listbox.hidden = true;\n\n shell.appendChild(trigger);\n selectEl.parentNode?.insertBefore(shell, selectEl.nextSibling);\n\n // The mix-and-match modal animates in with translateY, which makes\n // position:fixed resolve against the modal rather than the viewport. Portal\n // the listbox up to the overlay — it carries the per-bundle --lb-*\n // variables and has no transform of its own. Outside a modal there is no\n // transformed ancestor to escape, so leaving it in the shell keeps teardown\n // simple for no loss.\n const modalOverlay = selectEl.closest<HTMLElement>(\"[data-modal-overlay]\");\n if (modalOverlay) {\n modalOverlay.appendChild(listbox);\n listbox.setAttribute(\"data-lb-dropdown-portal\", \"\");\n } else {\n shell.appendChild(listbox);\n }\n\n let isOpen = false;\n let activeIndex = -1;\n let typeAhead: dropdown.TypeAheadState = dropdown.emptyTypeAheadState();\n let optionEls: HTMLLIElement[] = [];\n\n function readOptionsState(): OptionState[] {\n const sel: OptionState[] = [];\n for (let i = 0; i < selectEl.options.length; i++) {\n const o = selectEl.options[i];\n sel.push({ disabled: o.disabled, label: o.textContent || o.value });\n }\n return sel;\n }\n\n function syncFromSelect(): void {\n // Mirror the select's disabled state onto the trigger. Without this a row\n // that disables its select — mix-and-match products already in the bundle\n // — leaves the custom trigger focusable and clickable, so the visible\n // chrome disagrees with the form control.\n trigger.disabled = selectEl.disabled;\n\n const sel = readOptionsState();\n const idx = selectEl.selectedIndex;\n triggerLabel.textContent = idx >= 0 && sel[idx] ? sel[idx].label : \"\";\n\n // Rebuilt via removeChild rather than innerHTML so option labels can\n // never be an injection point.\n while (listbox.firstChild) listbox.removeChild(listbox.firstChild);\n optionEls = [];\n\n for (let i = 0; i < sel.length; i++) {\n const li = doc.createElement(\"li\");\n li.id = idBase + \"-opt-\" + i;\n li.className = \"lb-dropdown-option\";\n li.setAttribute(\"role\", \"option\");\n li.setAttribute(\"aria-selected\", i === idx ? \"true\" : \"false\");\n if (sel[i].disabled) li.setAttribute(\"aria-disabled\", \"true\");\n li.setAttribute(\"data-value\", selectEl.options[i].value);\n li.setAttribute(\"data-index\", String(i));\n li.textContent = sel[i].label;\n listbox.appendChild(li);\n optionEls.push(li);\n }\n }\n\n function updateActive(newIndex: number): void {\n if (activeIndex >= 0 && optionEls[activeIndex]) {\n optionEls[activeIndex].classList.remove(\"is-active\");\n }\n activeIndex = newIndex;\n\n if (newIndex < 0 || !optionEls[newIndex]) {\n trigger.setAttribute(\"aria-activedescendant\", \"\");\n return;\n }\n\n const li = optionEls[newIndex];\n li.classList.add(\"is-active\");\n trigger.setAttribute(\"aria-activedescendant\", li.id);\n\n // Scrolled by hand rather than with scrollIntoView: when the list is\n // shorter than its max-height there is nothing to scroll, and\n // scrollIntoView falls through to the next scrolling ancestor and yanks\n // the whole page.\n const liTop = li.offsetTop;\n const liBottom = liTop + li.offsetHeight;\n const visTop = listbox.scrollTop;\n const visBottom = visTop + listbox.clientHeight;\n if (liTop < visTop) {\n listbox.scrollTop = liTop;\n } else if (liBottom > visBottom) {\n listbox.scrollTop = liBottom - listbox.clientHeight;\n }\n }\n\n /** Returns false when the trigger has no layout yet and positioning must retry. */\n function position(): boolean {\n const rect = trigger.getBoundingClientRect();\n if (rect.width === 0) return false;\n\n const visibleCount = Math.min(optionEls.length || 1, MAX_VISIBLE_ITEMS);\n const desiredHeight = visibleCount * ITEM_HEIGHT + LIST_PADDING_Y;\n\n // Always clip to the trigger's nearest scrollable ancestor. In-shell that\n // is the bundle's product list, so the panel flips up before hiding\n // behind the widget footer. Portaled, it is the modal's scrollable list,\n // so the panel flips up near the modal's bottom edge even when there is\n // viewport room below — without this a fixed-position listbox on the\n // overlay would open downward and overflow the modal.\n const scrollable = findScrollableAncestor(trigger);\n const clipRect = scrollable\n ? {\n top: scrollable.getBoundingClientRect().top,\n bottom: scrollable.getBoundingClientRect().bottom,\n }\n : undefined;\n\n const pos = dropdown.computePosition({\n trigger: {\n top: rect.top,\n bottom: rect.bottom,\n left: rect.left,\n width: rect.width,\n },\n viewportHeight: window.innerHeight,\n desiredHeight,\n clip: clipRect,\n });\n\n listbox.setAttribute(\"data-placement\", pos.placement);\n listbox.style.maxHeight = pos.maxHeight + \"px\";\n\n // In-shell, CSS handles top/left/width via [data-placement] against the\n // position:relative shell. Only the portaled case needs viewport coords.\n if (listbox.hasAttribute(\"data-lb-dropdown-portal\")) {\n listbox.style.top = pos.offsetTop + \"px\";\n listbox.style.left = pos.offsetLeft + \"px\";\n listbox.style.width = pos.width + \"px\";\n }\n return true;\n }\n\n function open(): void {\n if (isOpen) return;\n closeOthers(instance);\n\n isOpen = true;\n listbox.hidden = false;\n trigger.setAttribute(\"aria-expanded\", \"true\");\n\n if (!position()) {\n requestAnimationFrame(() => {\n position();\n });\n }\n\n const sel = readOptionsState();\n const selIdx = selectEl.selectedIndex;\n if (selIdx >= 0 && sel[selIdx] && !sel[selIdx].disabled) {\n updateActive(selIdx);\n } else {\n updateActive(dropdown.firstEnabled(sel));\n }\n\n registerOpen(instance);\n }\n\n function close(restoreFocus: boolean): void {\n if (!isOpen) return;\n isOpen = false;\n listbox.hidden = true;\n trigger.setAttribute(\"aria-expanded\", \"false\");\n trigger.setAttribute(\"aria-activedescendant\", \"\");\n if (activeIndex >= 0 && optionEls[activeIndex]) {\n optionEls[activeIndex].classList.remove(\"is-active\");\n }\n activeIndex = -1;\n\n registerClosed(instance);\n if (restoreFocus) trigger.focus();\n }\n\n function commit(index: number): void {\n const opt = selectEl.options[index];\n if (!opt || opt.disabled) return;\n if (selectEl.value !== opt.value) {\n selectEl.value = opt.value;\n selectEl.dispatchEvent(new Event(\"change\", { bubbles: true }));\n }\n syncFromSelect();\n close(true);\n }\n\n function onKeydown(event: KeyboardEvent): void {\n const optsState = readOptionsState();\n const action = dropdown.handleKey(\n {\n key: event.key,\n ctrlKey: event.ctrlKey,\n metaKey: event.metaKey,\n altKey: event.altKey,\n shiftKey: event.shiftKey,\n },\n {\n isOpen,\n activeIndex,\n selectedIndex: selectEl.selectedIndex,\n options: optsState,\n },\n );\n\n if (action.preventDefault) event.preventDefault();\n\n switch (action.type) {\n case \"open\":\n open();\n if (action.activeIndex >= 0) updateActive(action.activeIndex);\n break;\n case \"close\":\n close(action.restoreFocus);\n break;\n case \"move-active\":\n updateActive(action.activeIndex);\n break;\n case \"commit\":\n commit(action.index);\n break;\n case \"type-ahead\": {\n const r = dropdown.pushTypeAheadChar(\n typeAhead,\n action.char,\n Date.now(),\n optsState,\n );\n typeAhead = r.newState;\n if (r.matchedIndex !== null) {\n if (!isOpen) open();\n updateActive(r.matchedIndex);\n }\n break;\n }\n }\n }\n\n function onTriggerClick(event: MouseEvent): void {\n event.preventDefault();\n if (isOpen) close(false);\n else open();\n }\n\n /** Walk up from the event target to the option element, if any. */\n function optionIndexFrom(event: Event): number | null {\n let target = event.target as HTMLElement | null;\n while (target && target !== listbox) {\n if (target.classList && target.classList.contains(\"lb-dropdown-option\")) {\n const idx = parseInt(target.getAttribute(\"data-index\") ?? \"\", 10);\n return isNaN(idx) ? null : idx;\n }\n target = target.parentNode as HTMLElement | null;\n }\n return null;\n }\n\n function onListboxClick(event: MouseEvent): void {\n const idx = optionIndexFrom(event);\n if (idx !== null) commit(idx);\n }\n\n function onListboxMousemove(event: MouseEvent): void {\n let target = event.target as HTMLElement | null;\n while (target && target !== listbox) {\n if (target.classList && target.classList.contains(\"lb-dropdown-option\")) {\n if (target.getAttribute(\"aria-disabled\") === \"true\") return;\n const idx = parseInt(target.getAttribute(\"data-index\") ?? \"\", 10);\n if (!isNaN(idx) && idx !== activeIndex) updateActive(idx);\n return;\n }\n target = target.parentNode as HTMLElement | null;\n }\n }\n\n function onShellFocusout(): void {\n // Deferred past the focus transition so document.activeElement is settled.\n setTimeout(() => {\n if (!isOpen) return;\n if (!shell.contains(document.activeElement)) close(false);\n }, 0);\n }\n\n /** Something else set select.value — resync the visible chrome. */\n function onSelectChange(): void {\n syncFromSelect();\n }\n\n // Cascading availability changes rewrite the option list, so rebuild.\n const observer = new MutationObserver(() => {\n syncFromSelect();\n });\n observer.observe(selectEl, {\n childList: true,\n subtree: true,\n attributes: true,\n attributeFilter: [\"disabled\", \"value\", \"selected\"],\n });\n\n /**\n * Stop mousedown on a non-focusable <li> from blurring the trigger.\n *\n * Without this, focusout fires on the shell and queues a setTimeout(0) that\n * closes the dropdown. On desktop that close runs before the synthesised\n * click, so onListboxClick never sees the option and the commit is lost.\n * Mobile is unaffected because touchstart doesn't move focus.\n */\n function onListboxMousedown(event: MouseEvent): void {\n event.preventDefault();\n }\n\n trigger.addEventListener(\"click\", onTriggerClick);\n trigger.addEventListener(\"keydown\", onKeydown);\n shell.addEventListener(\"focusout\", onShellFocusout);\n listbox.addEventListener(\"mousedown\", onListboxMousedown);\n listbox.addEventListener(\"click\", onListboxClick);\n listbox.addEventListener(\"mousemove\", onListboxMousemove);\n selectEl.addEventListener(\"change\", onSelectChange);\n\n function destroy(): void {\n if (isOpen) close(false);\n observer.disconnect();\n trigger.removeEventListener(\"click\", onTriggerClick);\n trigger.removeEventListener(\"keydown\", onKeydown);\n shell.removeEventListener(\"focusout\", onShellFocusout);\n listbox.removeEventListener(\"mousedown\", onListboxMousedown);\n listbox.removeEventListener(\"click\", onListboxClick);\n listbox.removeEventListener(\"mousemove\", onListboxMousemove);\n selectEl.removeEventListener(\"change\", onSelectChange);\n shell.parentNode?.removeChild(shell);\n listbox.parentNode?.removeChild(listbox);\n selectEl.classList.remove(\"lb-dropdown-state\");\n selectEl.removeAttribute(\"aria-hidden\");\n selectEl.removeAttribute(\"tabindex\");\n instances.delete(selectEl);\n }\n\n // Declared last but captured by the handlers above; they only run after\n // binding completes, so the closures always see an initialised value.\n const instance: DropdownInstance = {\n shell,\n listbox,\n select: selectEl,\n close: () => close(false),\n destroy,\n };\n instances.set(selectEl, instance);\n\n syncFromSelect();\n return instance;\n}\n","import { bindDropdown, instanceFor } from \"./bind\";\n\n/**\n * The variant `<select>` classes the auto-binder attaches to.\n *\n * Single source of truth — add another bundle type's class here if it wants\n * the custom dropdown.\n */\nexport const BIND_SELECTOR =\n \"select.lb-bundle-variant-select:not(.lb-dropdown-state), \" +\n \"select.lb-mix-match__variant-select:not(.lb-dropdown-state)\";\n\n/** Roots already under observation, so re-init doesn't stack observers. */\nconst observedRoots = new WeakSet<HTMLElement>();\n\nexport function bindAll(root?: ParentNode | null): void {\n const scope = root ?? document;\n const selects = scope.querySelectorAll<HTMLSelectElement>(BIND_SELECTOR);\n for (let i = 0; i < selects.length; i++) bindDropdown(selects[i]);\n}\n\nexport function unbindAll(root?: ParentNode | null): void {\n const scope = root ?? document;\n const bound = scope.querySelectorAll<HTMLSelectElement>(\n \"select.lb-dropdown-state\",\n );\n for (let i = 0; i < bound.length; i++) {\n instanceFor(bound[i])?.destroy();\n }\n}\n\n/**\n * Bind dropdowns to selects added after load — mix-and-match slots, picker\n * modal rows, and theme editor section reloads all inject markup late.\n */\nexport function observeRoot(rootEl: HTMLElement | null): void {\n if (!rootEl || observedRoots.has(rootEl)) return;\n observedRoots.add(rootEl);\n\n new MutationObserver((mutations) => {\n for (let m = 0; m < mutations.length; m++) {\n const added = mutations[m].addedNodes;\n for (let n = 0; n < added.length; n++) {\n if (added[n].nodeType !== 1) continue;\n const node = added[n] as HTMLElement;\n if (node.matches && node.matches(BIND_SELECTOR)) {\n bindDropdown(node as HTMLSelectElement);\n } else if (node.querySelectorAll) {\n const found = node.querySelectorAll<HTMLSelectElement>(BIND_SELECTOR);\n for (let f = 0; f < found.length; f++) bindDropdown(found[f]);\n }\n }\n }\n }).observe(rootEl, { childList: true, subtree: true });\n}\n","import { bindAll, unbindAll } from \"@lime-bundles/render/dropdown/auto-bind\";\n\n/**\n * Bind the custom variant dropdown inside a rendered bundle, and return the\n * teardown.\n *\n * The theme host binds through `window.LimeBundles.Dropdown` because its\n * scripts are separate `<script>` tags with no module graph between them. This\n * host imports the same module directly — one implementation, two ways of\n * reaching it.\n */\nexport function bindDropdowns(root: HTMLElement): () => void {\n bindAll(root);\n return () => unbindAll(root);\n}\n","import type { CartLineInput, FixedBundleData } from \"@lime-bundles/core\";\nimport { adaptProducts } from \"@lime-bundles/render/adapt\";\nimport { buildCartItems } from \"@lime-bundles/render/fixed/cart\";\nimport { recalcPricing } from \"@lime-bundles/render/fixed/pricing\";\nimport { bundleLineAttributes, intlFormatMoney } from \"@lime-bundles/render/host\";\nimport { bindVariantSelects } from \"@lime-bundles/render/product/option-selects\";\nimport { hydrateRowControls } from \"@lime-bundles/render/product/hydrate\";\nimport { applyVariant } from \"@lime-bundles/render/product/row\";\nimport {\n findVariant,\n resolveSellableVariant,\n} from \"@lime-bundles/render/product/variants\";\nimport { buildRowSkeleton, buildWidgetShell } from \"@lime-bundles/render/skeleton\";\nimport { initCountdown } from \"@lime-bundles/render/countdown\";\nimport { DEFAULT_STRINGS, formatItemsOutOfStock } from \"@lime-bundles/render/strings\";\nimport { bindDropdowns } from \"./dropdown-host\";\n\n/**\n * Fixed bundle, headless.\n *\n * This host has no Liquid, so it builds the skeleton the theme's Liquid emits\n * and then runs the identical hydration path — `hydrateRowControls`,\n * `applyVariant`, `bindVariantSelects`, `recalcPricing`. Everything below the\n * skeleton is shared code; what is here is only the difference between having\n * a server template and not.\n */\nexport function renderFixedBundle(\n container: HTMLElement,\n bundle: FixedBundleData,\n onAddToCart: (lines: CartLineInput[]) => void,\n onCleanup?: (fn: () => void) => void,\n): void {\n const wc = bundle.widgetConfig;\n const products = adaptProducts(bundle);\n if (!products.length) return;\n\n // A fixed bundle is all-or-nothing: sold-out rows render greyed with the\n // CTA locked, and the status flip hides a bundle that can't be bought.\n const oosProducts = products.filter((p) => !p.variants.some((v) => v.available));\n\n const currency =\n bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? \"USD\";\n const formatMoney = intlFormatMoney(currency);\n const t = DEFAULT_STRINGS;\n\n const shell = buildWidgetShell(\"lb-fixed\", {\n title: bundle.title,\n subtitle: bundle.description,\n showSavingsBar: wc.savingsBar?.visible !== false,\n showComparePrice: wc.pricing?.showCompareAtPrice !== false,\n ctaText: wc.cta?.ctaText || t.addToCart || \"Add to cart\",\n endsAt: wc.countdown?.showCountdown === false ? null : bundle.endsAt,\n });\n shell.root.setAttribute(\"data-discount-type\", bundle.discountConfig.discountType);\n shell.root.setAttribute(\n \"data-discount-value\",\n String(bundle.discountConfig.discountValue),\n );\n\n for (const product of products) {\n const isOos = !product.variants.some((v) => v.available);\n const row = buildRowSkeleton(product, t, {\n isOos,\n href: product.url,\n thumbnailRatio: wc.productList?.thumbnailRatio,\n });\n shell.products.appendChild(row);\n\n if (isOos) continue;\n\n // adaptProducts already seeds a sellable variant; resolving again keeps\n // this host correct if the seed and the availability flags ever diverge,\n // the same guarantee the theme host makes.\n const sellable = resolveSellableVariant(product);\n if (sellable) product.selectedVariantId = sellable.id;\n const selected = sellable ?? findVariant(product, product.selectedVariantId);\n hydrateRowControls(row, product, selected);\n applyVariant(row, selected, product, formatMoney);\n\n bindVariantSelects(\n row,\n () => product,\n (variant, p) => {\n applyVariant(row, variant, p, formatMoney);\n recalcPricing(shell.root, products, formatMoney);\n },\n );\n }\n\n // A product that can't be fulfilled locks the CTA rather than hiding it, so\n // the shopper is told why instead of finding a button that does nothing.\n if (oosProducts.length > 0) {\n shell.cta.disabled = true;\n const label = shell.cta.querySelector(\"[data-cta-label]\");\n if (label) {\n label.textContent = formatItemsOutOfStock(t, oosProducts.length);\n }\n } else {\n shell.cta.addEventListener(\"click\", () => {\n onAddToCart(\n buildCartItems(products).map((item) => ({\n merchandiseId: \"gid://shopify/ProductVariant/\" + item.variantId,\n quantity: item.quantity,\n attributes: bundleLineAttributes(bundle.id, bundle.bundleType),\n })),\n );\n });\n }\n\n container.appendChild(shell.root);\n initCountdown(shell.root);\n recalcPricing(shell.root, products, formatMoney);\n\n onCleanup?.(bindDropdowns(shell.root));\n}\n","import type { PickerTranslations } from \"./types\";\n\n/** Substitute `__NAME__` placeholders in a translated template. */\nexport function fill(\n template: string,\n values: Record<string, string | number>,\n): string {\n let out = template;\n for (const key in values) {\n out = out.split(\"__\" + key + \"__\").join(String(values[key]));\n }\n return out;\n}\n\n/**\n * \"Needs N spots\" hint, pluralised through Intl.PluralRules against the\n * translations' per-form map — the same mechanism the volume widget's item\n * count uses, so languages with more than two plural forms read correctly.\n */\nexport function formatNeedsSpots(\n min: number,\n t: PickerTranslations,\n): string {\n const map = t.needsSpots;\n if (!map) return \"Needs \" + min + \" spots\";\n const form = new Intl.PluralRules(t.locale || \"en\").select(min);\n return fill(map[form] || map.other || \"__COUNT__\", { COUNT: min });\n}\n","import { fill } from \"../picker/i18n\";\nimport type { MixMatchTranslations } from \"./types\";\n\nexport { formatNeedsSpots } from \"../picker/i18n\";\n\nexport function formatOfSelected(\n count: number,\n total: number,\n t: MixMatchTranslations,\n): string {\n return fill(t.ofSelected || \"__COUNT__ of __TOTAL__ added\", {\n COUNT: count,\n TOTAL: total,\n });\n}\n\nexport function formatMoreToGo(\n remaining: number,\n t: MixMatchTranslations,\n): string {\n return remaining > 0\n ? fill(t.moreToGo || \"__COUNT__ more to go\", { COUNT: remaining })\n : t.complete || \"Complete\";\n}\n\n/**\n * Modal subtitle: names the discount when there is a percentage to advertise,\n * otherwise just counts down the remaining picks.\n */\nexport function formatSubtitle(\n remaining: number,\n discountType: string | undefined,\n discountValue: number | undefined,\n t: MixMatchTranslations,\n): string {\n if (remaining <= 0) return t.bundleComplete || \"Your bundle is complete\";\n\n const pct =\n discountType === \"percentage\" && discountValue\n ? Math.round(discountValue)\n : 0;\n\n return pct > 0\n ? fill(t.chooseMoreUnlock || \"Choose __COUNT__ more to unlock __PCT__% off\", {\n COUNT: remaining,\n PCT: pct,\n })\n : fill(\n t.chooseMoreComplete || \"Choose __COUNT__ more to complete your bundle\",\n { COUNT: remaining },\n );\n}\n","/**\n * Rule primitives shared by every picker-based bundle type.\n *\n * Step-agnostic on purpose: mix & match applies these to one flat selection,\n * multi-step applies them per step. Anything that needs to know about steps\n * or a bundle-wide required quantity lives in the type's own rules module.\n */\n\n/** Per-product unit rule from the bundle's `productRules` map. */\nexport interface ProductRule {\n min: number;\n max: number;\n required?: boolean;\n}\n\n/** Applied to products with no explicit entry (collection-sourced, mostly). */\nexport const DEFAULT_RULE: ProductRule = { min: 1, max: 99 };\n\n/** One committed pick. Slots are variant-scoped: one per (product, variant). */\nexport interface SelectedItem {\n productId: number | string;\n variantId: number | string;\n title: string;\n url: string | null;\n variantTitle: string;\n featuredImage: string | null;\n price: number;\n compareAtPrice: number | null;\n unitPrice: string | null;\n quantity: number;\n}\n\n/**\n * The rule lookup, keyed by GID because that is what the metaobject stores\n * while the picker works in numeric IDs.\n *\n * One map holds both levels. Product GIDs and variant GIDs are different\n * namespaces (`gid://shopify/Product/…` vs `gid://shopify/ProductVariant/…`)\n * so they cannot collide, and merging them at the host boundary — see\n * `mergeRuleMaps` — means every function that already threads `rulesMap`\n * keeps working untouched. Nothing iterates this map; it is only ever read by\n * key.\n */\nexport type RulesMap = Record<string, ProductRule | undefined>;\n\n/**\n * Rule for a pick.\n *\n * Pass `variantId` wherever the caller knows which variant it is talking\n * about, and the variant's own rule wins. Without one — a product row before\n * the shopper has chosen a variant — this resolves the product rule, which is\n * the envelope covering every variant.\n *\n * A malformed entry falls back to the default rather than throwing at either\n * level: the map comes from merchant-authored data via Liquid.\n */\nexport function ruleFor(\n productId: number | string,\n rulesMap: RulesMap,\n variantId?: number | string | null,\n): ProductRule {\n if (variantId != null) {\n const vRule = rulesMap[\"gid://shopify/ProductVariant/\" + variantId];\n if (vRule && typeof vRule.min === \"number\" && typeof vRule.max === \"number\") {\n return vRule;\n }\n }\n const rule = rulesMap[\"gid://shopify/Product/\" + productId];\n if (!rule || typeof rule.min !== \"number\" || typeof rule.max !== \"number\") {\n return DEFAULT_RULE;\n }\n return rule;\n}\n\n/**\n * The variant's OWN rule, or undefined when it inherits the product's.\n *\n * Distinct from `ruleFor(pid, map, vid)`, which falls back to the product\n * rule. Callers that must know which level a rule came from need this one:\n * \"is this variant required\" and \"is this variant's product required\" are\n * different questions with different answers, and conflating them would make\n * every variant of a required product unremovable.\n */\nexport function variantRuleFor(\n variantId: number | string | null | undefined,\n rulesMap: RulesMap,\n): ProductRule | undefined {\n if (variantId == null) return undefined;\n const rule = rulesMap[\"gid://shopify/ProductVariant/\" + variantId];\n if (!rule || typeof rule.min !== \"number\" || typeof rule.max !== \"number\") {\n return undefined;\n }\n return rule;\n}\n\n/**\n * Fold optional per-variant rules into the product rule map the hosts already\n * build, so the picker has one lookup rather than two threaded side by side.\n *\n * Returns the product map untouched when there are no variant rules, which is\n * every bundle saved before they existed.\n */\nexport function mergeRuleMaps(\n productRules: RulesMap | undefined,\n variantRules: RulesMap | undefined,\n): RulesMap {\n const products = productRules ?? {};\n if (!variantRules) return products;\n const keys = Object.keys(variantRules);\n if (keys.length === 0) return products;\n const merged: RulesMap = { ...products };\n for (const key of keys) merged[key] = variantRules[key];\n return merged;\n}\n\n/**\n * Default units a single pick contributes.\n *\n * One of five implementations of the bundle quantity fallback in this repo\n * (canonical: packages/core/src/bundle/qty.ts; also the Rust discount function\n * and the metafield producer). A pick starts at the merchant's `min` — the\n * variant's when it has one, else the product's — and the stepper raises it\n * from there. Named rather than inlined so the parity test in\n * packages/core/__tests__/resolve-bundle-qty.test.ts can assert it directly.\n */\nexport function defaultPickQuantity(\n productId: number | string,\n rulesMap: RulesMap,\n variantId?: number | string | null,\n): number {\n return ruleFor(productId, rulesMap, variantId).min;\n}\n\n/** Total units across every slot. Completion counts units, not products. */\nexport function totalUnits(items: SelectedItem[]): number {\n let qty = 0;\n for (let i = 0; i < items.length; i++) qty += items[i].quantity || 1;\n return qty;\n}\n\n/** Index of the slot holding a variant, or -1. */\nexport function indexOfVariantInBundle(\n items: SelectedItem[],\n variantId: number | string,\n): number {\n for (let i = 0; i < items.length; i++) {\n if (items[i] && items[i].variantId === variantId) return i;\n }\n return -1;\n}\n\n/** Committed units of the slot holding this variant, 0 when absent. */\nexport function committedQtyForVariant(\n items: SelectedItem[],\n variantId: number | string,\n): number {\n const idx = indexOfVariantInBundle(items, variantId);\n return idx === -1 ? 0 : items[idx].quantity || 0;\n}\n\n/**\n * Whether an unselected row can be added at all.\n *\n * Exact-size rule: a product's minimum pick must fit the remaining spots, or\n * adding it would overshoot the bundle size on its own.\n */\nexport function isRowAddable(\n rule: ProductRule,\n remainingSpots: number,\n): boolean {\n return rule.min <= remainingSpots;\n}\n\n/**\n * Ceiling for a row's quantity stepper.\n *\n * The lower of what the product's rule still allows (max minus sibling-variant\n * units) and what the bundle has room for (the row's own committed units plus\n * the open spots — its own units already count toward the total, so a\n * committed row edits live within that headroom). Stock, when known, has the\n * final say.\n *\n * `variantMax` is the nested level: the two bind independently, so it is NOT\n * reduced by `productOtherUnits`. Sibling units eat into the product envelope,\n * never into this variant's own allowance — \"max 4 shirts, no more than 2 of\n * them medium\" still permits 2 mediums once two larges are in the bundle.\n */\nexport function stepperCeiling(\n rule: ProductRule,\n remainingSpots: number,\n stock?: number,\n ownQty = 0,\n productOtherUnits = 0,\n variantMax?: number,\n): number {\n let ceiling = Math.min(rule.max - productOtherUnits, ownQty + remainingSpots);\n if (typeof variantMax === \"number\") ceiling = Math.min(ceiling, variantMax);\n return typeof stock === \"number\" ? Math.min(ceiling, stock) : ceiling;\n}\n","/**\n * Responsive picker imagery. Mirrors packages/widget/src/renderers/image.ts.\n *\n * Shopify's CDN caps each request at the master image's native resolution, so\n * the browser picks the smallest srcset candidate that satisfies\n * `sizes` × devicePixelRatio. The smallest candidate is deliberately ~2× the\n * display size rather than 1×, so a DPR-1 browser still gets a 2× image —\n * otherwise the thumbnail looks soft the moment it's viewed on a Retina panel.\n */\nexport const THUMB_WIDTHS = [120, 180, 240];\nexport const PICKER_WIDTHS = [480, 600, 768, 1024];\nexport const THUMB_SIZES = \"60px\";\nexport const PICKER_SIZES = \"(max-width: 767px) 45vw, 228px\";\n\n/** Set or replace the `width` query parameter on a Shopify CDN image URL. */\nexport function imgWidth(url: string, w: number): string {\n if (!url) return url;\n return /[?&]width=\\d+/.test(url)\n ? url.replace(/([?&])width=\\d+/, \"$1width=\" + w)\n : url + (url.indexOf(\"?\") === -1 ? \"?\" : \"&\") + \"width=\" + w;\n}\n\nexport function srcset(url: string, widths: number[]): string {\n if (!url) return \"\";\n return widths.map((w) => imgWidth(url, w) + \" \" + w + \"w\").join(\", \");\n}\n","/** Trailing-edge debounce, used to keep picker search off every keystroke. */\nexport function debounce<A extends unknown[]>(\n fn: (...args: A) => void,\n delay: number,\n): (...args: A) => void {\n let timer: ReturnType<typeof setTimeout> | null = null;\n return function (this: unknown, ...args: A) {\n if (timer) clearTimeout(timer);\n timer = setTimeout(() => fn.apply(this, args), delay);\n };\n}\n\n/**\n * Fold case and strip diacritics so picker search matches \"creme\" against\n * \"Crème\".\n *\n * The range is written as escapes on purpose. The multi-step copy of this\n * function carried the literal combining marks instead, which are invisible in\n * an editor and trivially mangled by anything that renormalises the file.\n */\nexport function normalizeText(str: string | null | undefined): string {\n if (!str) return \"\";\n return str\n .normalize(\"NFD\")\n .replace(/[\\u0300-\\u036f]/g, \"\")\n .toLowerCase();\n}\n","import {\n isRowAddable,\n ruleFor,\n stepperCeiling,\n variantRuleFor,\n type ProductRule,\n} from \"./rules\";\nimport { imgWidth, srcset, PICKER_SIZES, PICKER_WIDTHS } from \"./images\";\nimport { formatNeedsSpots } from \"./i18n\";\nimport { normalizeText } from \"../utils\";\nimport type {\n EligibleProduct,\n EligibleVariant,\n PickerTranslations,\n} from \"./types\";\n\n/**\n * How a row asks about capacity.\n *\n * Mix & match answers against one flat selection and a bundle-wide required\n * quantity; multi-step answers against the current step. The row's own logic —\n * stepper bounds, Add/Remove/Swap state, the needs-spots hint — is identical\n * either way, which is why it is written once and the scoping is injected.\n */\nexport interface RowCapacity {\n /** Open units the row may still claim. */\n remainingSpots(): number;\n /** Units of this variant already committed to the row's own slot. */\n ownQty(variantId: number | string): number;\n /** Units of this variant held by any other slot; they draw the same stock. */\n variantUnitsElsewhere(variantId: number | string): number;\n /** Units of this product held elsewhere; rule.max caps the product, not the slot. */\n productUnitsElsewhere(\n productId: number | string,\n variantId: number | string,\n ): number;\n /** Units an atomic swap can displace, or 0 when the row isn't in that state. */\n swapUnits(productId: number | string, variantId: number | string): number;\n isInBundle(variantId: number | string): boolean;\n /** Whether the committed pick for this variant may be removed. */\n canRemove(variantId: number | string): boolean;\n /** Whether there is no room left at all. */\n isFull(): boolean;\n /** Write a new quantity onto the committed pick, or return false if absent. */\n commitQty(variantId: number | string, qty: number): boolean;\n}\n\n/** Width the picker card's `src` fallback requests; srcset covers the rest. */\nconst PICKER_BASE_WIDTH = 600;\n\nexport interface PickerRowContext {\n t: PickerTranslations;\n capacity: RowCapacity;\n rulesMap: Record<string, ProductRule | undefined>;\n showQtySelector: boolean;\n formatMoney: (cents: number) => string;\n /** Called after a stepper edit writes through to a committed pick. */\n onCommittedQtyChanged: () => void;\n}\n\nexport interface PickerRow {\n el: HTMLElement;\n /**\n * Recompute this row's stepper bounds and Add state from the live\n * selection. Called on every modal open and after every mutation, so\n * cross-slot stock accounting stays current.\n */\n refresh: () => void;\n}\n\n/**\n * Build one product row for the picker modal.\n *\n * Previously an IIFE inside the build loop, needed so each row's mutable state\n * got its own scope — with `var` alone every row's change handler aliased the\n * last iteration's variables, and picking a variant on one row wrote the\n * selection onto another. A function per row makes that structural rather than\n * a trick to remember.\n */\nexport function buildPickerRow(\n p: EligibleProduct,\n rowIndex: number,\n ctx: PickerRowContext,\n): PickerRow {\n const { t, rulesMap, capacity, formatMoney } = ctx;\n const pRule = ruleFor(p.id, rulesMap);\n\n const row = document.createElement(\"div\");\n row.className = \"lb-mix-match__modal-product\";\n if (!p.available) row.className += \" lb-mix-match__modal-product--sold-out\";\n row.setAttribute(\"data-product-item\", \"\");\n row.setAttribute(\"data-title\", normalizeText(p.title));\n row.setAttribute(\"data-type\", p.type || \"\");\n\n // Sold-out rows lock their variant selects. In-stock rows keep theirs\n // enabled even while a variant is committed — slots are variant-scoped, so\n // the dropdown is how a shopper reaches another variant of the same product.\n let rowDisabled = false;\n // The stepper has its own flag: a committed row keeps it enabled to edit the\n // pick's quantity live, while uncommitted rows lock it together with Add.\n let stepperDisabled = false;\n\n /* ── Thumbnail ─────────────────────────────────────────────── */\n\n const thumbDiv = document.createElement(\"div\");\n thumbDiv.className = \"lb-mix-match__modal-product-thumb\";\n\n // Informational only; the row's button is what acts.\n const addedBadge = document.createElement(\"span\");\n addedBadge.className = \"lb-mix-match__modal-added-badge\";\n addedBadge.textContent = t.addedItem || \"Added\";\n addedBadge.hidden = true;\n thumbDiv.appendChild(addedBadge);\n\n const firstAvailable = p.variants.find((v) => v.available) ?? null;\n const initialThumbSrc =\n (firstAvailable && firstAvailable.image) || p.featuredImage;\n if (initialThumbSrc) {\n const img = document.createElement(\"img\");\n img.src = imgWidth(initialThumbSrc, PICKER_BASE_WIDTH);\n img.srcset = srcset(initialThumbSrc, PICKER_WIDTHS);\n img.sizes = PICKER_SIZES;\n img.alt = p.title;\n img.loading = \"lazy\";\n thumbDiv.appendChild(img);\n }\n row.appendChild(thumbDiv);\n\n /* ── Info column ───────────────────────────────────────────── */\n\n const infoDiv = document.createElement(\"div\");\n infoDiv.className = \"lb-mix-match__modal-product-info\";\n\n const titleEl = document.createElement(\"p\");\n titleEl.className = \"lb-mix-match__modal-product-title\";\n if (p.url) {\n // New tab keeps the in-progress bundle alive; same-tab navigation would\n // wipe the shopper's picks.\n const titleLink = document.createElement(\"a\");\n titleLink.href = p.url;\n titleLink.target = \"_blank\";\n titleLink.rel = \"noopener noreferrer\";\n titleLink.textContent = p.title;\n titleEl.appendChild(titleLink);\n } else {\n titleEl.textContent = p.title;\n }\n infoDiv.appendChild(titleEl);\n\n // The row's currently-picked variant. Declared up here because the stepper\n // and the refresh hook both read it, on single- and multi-variant rows.\n let currentVariant: EligibleVariant | null =\n firstAvailable ?? (p.variants.length > 0 ? p.variants[0] : null);\n\n /**\n * The rule in force for a given variant: its own when the merchant wrote\n * one, else the product's, which is the envelope covering every variant.\n * Resolved per call rather than captured once, because the shopper moves\n * between variants through the dropdowns and the row's min/max must follow.\n * `pRule` stays the envelope and is used wherever the question is about the\n * product's total rather than this variant's allowance.\n */\n const ruleForVariant = (v: EligibleVariant | null): ProductRule =>\n ruleFor(p.id, rulesMap, v ? v.id : null);\n\n const modalQty = currentVariant ? ruleForVariant(currentVariant).min : 1;\n\n const priceEl = document.createElement(\"p\");\n priceEl.className = \"lb-mix-match__modal-product-price\";\n priceEl.setAttribute(\"data-row-price\", \"\");\n const priceSaleEl = document.createElement(\"span\");\n priceSaleEl.setAttribute(\"data-row-price-sale\", \"\");\n if (currentVariant) {\n priceSaleEl.textContent = formatMoney(currentVariant.price * modalQty);\n }\n priceEl.appendChild(priceSaleEl);\n const priceCompareEl = document.createElement(\"s\");\n priceCompareEl.className = \"lb-mix-match__modal-product-compare\";\n priceCompareEl.setAttribute(\"data-row-compare\", \"\");\n priceEl.appendChild(priceCompareEl);\n infoDiv.appendChild(priceEl);\n\n /** Struck compare-at for variant × qty; also CSS-gated by --lb-product-compare-display. */\n function paintRowCompare(\n el: HTMLElement,\n variant: EligibleVariant | null,\n qty: number,\n ): void {\n const cmp = variant && variant.compareAtPrice ? variant.compareAtPrice * qty : 0;\n const price = variant ? variant.price * qty : 0;\n if (cmp > price) {\n el.textContent = formatMoney(cmp);\n el.hidden = false;\n } else {\n el.textContent = \"\";\n el.hidden = true;\n }\n }\n paintRowCompare(priceCompareEl, currentVariant, modalQty);\n\n const unitPriceEl = document.createElement(\"p\");\n unitPriceEl.className =\n \"lb-mix-match__modal-product-unit-price lb-bundle-product-unit-price\";\n unitPriceEl.setAttribute(\"data-row-unit-price\", \"\");\n const initialUnit = currentVariant ? currentVariant.unitPrice : null;\n if (initialUnit) unitPriceEl.textContent = initialUnit;\n else unitPriceEl.hidden = true;\n infoDiv.appendChild(unitPriceEl);\n\n /* ── Quantity stepper ──────────────────────────────────────── */\n\n let qtyGroup: HTMLElement | null = null;\n let qtyValueEl: HTMLElement | null = null;\n let current = ruleForVariant(currentVariant).min;\n let effectiveMax = ruleForVariant(currentVariant).max;\n let paint: (() => void) | null = null;\n let setBoundsForVariant:\n | ((variant: EligibleVariant | null, alreadyInBundle?: number) => void)\n | null = null;\n\n if (p.available && ctx.showQtySelector) {\n // Mirrors renderQtyStepper in packages/widget/src/renderers/mix-match.ts;\n // the bundle-css-parity test pins both stylesheets together.\n qtyGroup = document.createElement(\"div\");\n qtyGroup.className =\n \"lb-bundle-variant-option-group lb-mix-match__qty-stepper-group\";\n\n const stepperWrap = document.createElement(\"div\");\n stepperWrap.className = \"lb-mix-match__qty-stepper\";\n stepperWrap.setAttribute(\"role\", \"group\");\n stepperWrap.setAttribute(\"aria-label\", t.quantity || \"Quantity\");\n\n const qtyMinusBtn = document.createElement(\"button\");\n qtyMinusBtn.type = \"button\";\n qtyMinusBtn.className =\n \"lb-mix-match__qty-stepper-button lb-mix-match__qty-stepper-button--minus\";\n qtyMinusBtn.setAttribute(\n \"aria-label\",\n t.decreaseQuantity || \"Decrease quantity\",\n );\n qtyMinusBtn.textContent = \"−\";\n\n qtyValueEl = document.createElement(\"span\");\n qtyValueEl.className = \"lb-mix-match__qty-stepper-value\";\n qtyValueEl.setAttribute(\"data-row-qty\", \"\");\n qtyValueEl.setAttribute(\"aria-live\", \"polite\");\n qtyValueEl.textContent = String(ruleForVariant(currentVariant).min);\n\n const qtyPlusBtn = document.createElement(\"button\");\n qtyPlusBtn.type = \"button\";\n qtyPlusBtn.className =\n \"lb-mix-match__qty-stepper-button lb-mix-match__qty-stepper-button--plus\";\n qtyPlusBtn.setAttribute(\n \"aria-label\",\n t.increaseQuantity || \"Increase quantity\",\n );\n qtyPlusBtn.textContent = \"+\";\n\n stepperWrap.appendChild(qtyMinusBtn);\n stepperWrap.appendChild(qtyValueEl);\n stepperWrap.appendChild(qtyPlusBtn);\n qtyGroup.appendChild(stepperWrap);\n\n const valueEl = qtyValueEl;\n paint = () => {\n valueEl.textContent = String(current);\n valueEl.setAttribute(\"data-qty\", String(current));\n qtyMinusBtn.disabled =\n stepperDisabled || current <= ruleForVariant(currentVariant).min;\n qtyPlusBtn.disabled = stepperDisabled || current >= effectiveMax;\n };\n\n setBoundsForVariant = (variant, alreadyInBundle = 0) => {\n const variantId = variant ? variant.id : null;\n const ownQty =\n variantId === null ? 0 : capacity.ownQty(variantId);\n const otherUnits =\n variantId === null\n ? 0\n : capacity.productUnitsElsewhere(p.id, variantId);\n\n // A swap displaces sibling units rather than adding, so the bundle's\n // open spots don't constrain it — the total never moves.\n const swapUnits =\n variantId === null\n ? 0\n : capacity.swapUnits(p.id, variantId);\n\n const stock = variant ? variant.inventoryQuantity : null;\n // Own committed units draw from the same stock pool the stepper edits,\n // so only units held by OTHER slots reduce what's available here.\n const othersInBundle = Math.max(0, alreadyInBundle - ownQty);\n const stockCap =\n typeof stock === \"number\"\n ? Math.max(0, stock - othersInBundle)\n : undefined;\n\n // The variant's own cap, when it has one. Passed separately because it\n // binds independently of the envelope: sibling units eat into the\n // product's total, never into this variant's allowance.\n const vMax = variantRuleFor(variantId, rulesMap)?.max;\n const rMin = ruleForVariant(variant).min;\n\n effectiveMax =\n swapUnits > 0\n ? typeof stockCap === \"number\"\n ? Math.min(swapUnits, stockCap)\n : swapUnits\n : stepperCeiling(\n pRule,\n capacity.remainingSpots(),\n stockCap,\n ownQty,\n otherUnits,\n vMax,\n );\n\n if (effectiveMax < rMin) {\n // Nothing can satisfy another pick — pin the display at rule.min so\n // the stepper doesn't flash 0; refresh() disables Add below.\n current = rMin;\n } else {\n if (current > effectiveMax) current = effectiveMax;\n if (current < rMin) current = rMin;\n }\n paint!();\n };\n\n /**\n * Live edit: when the row's variant holds a slot, a stepper click writes\n * straight onto that pick and runs the standard update path, so freeing\n * units immediately re-enables rows the exact-size rule had disabled.\n * Removal stays with the Added toggle — the floor here is rule.min, never 0.\n */\n const commitQtyToBundle = (): void => {\n if (!currentVariant) return;\n if (capacity.commitQty(currentVariant.id, current)) {\n ctx.onCommittedQtyChanged();\n }\n };\n\n qtyMinusBtn.addEventListener(\"click\", () => {\n if (current > ruleForVariant(currentVariant).min) current -= 1;\n paint!();\n commitQtyToBundle();\n });\n qtyPlusBtn.addEventListener(\"click\", () => {\n if (current < effectiveMax) current += 1;\n paint!();\n commitQtyToBundle();\n });\n\n if (currentVariant) setBoundsForVariant(currentVariant);\n else paint();\n }\n\n /* ── Variant option selects ────────────────────────────────── */\n\n // Sold-out variants are included so their option values render disabled,\n // matching how unavailable combinations behave; isValueAvailable filters\n // them out of the selectable set.\n const availVariants = p.variants.slice();\n const optionSelects: HTMLSelectElement[] = [];\n\n if (availVariants.length > 1) {\n const optionNames = p.optionNames || [];\n // Prefer the first purchasable option so the default price and the Add\n // click target something buyable.\n currentVariant =\n availVariants.find((v) => v.available !== false) ?? availVariants[0];\n\n const findMatchingVariant = (): EligibleVariant | null => {\n const values = optionSelects.map((s) => s.value);\n for (const v of availVariants) {\n if (!v.options || v.options.length !== values.length) continue;\n if (v.options.every((o, j) => o === values[j])) return v;\n }\n return null;\n };\n\n const syncSelectsToVariant = (v: EligibleVariant | null): void => {\n if (!v || !v.options) return;\n for (let i = 0; i < optionSelects.length && i < v.options.length; i++) {\n if (optionSelects[i].value !== v.options[i]) {\n optionSelects[i].value = v.options[i];\n }\n }\n };\n\n const isValueAvailable = (\n optionIndex: number,\n value: string,\n selected: string[],\n ): boolean => {\n for (const v of availVariants) {\n if (v.available === false) continue;\n if (!v.options || v.options[optionIndex] !== value) continue;\n let ok = true;\n for (let j = 0; j < v.options.length; j++) {\n if (j === optionIndex) continue;\n if (v.options[j] !== selected[j]) {\n ok = false;\n break;\n }\n }\n if (ok) return true;\n }\n return false;\n };\n\n const recomputeDisabled = (selected: string[]): void => {\n for (let i = 0; i < optionSelects.length; i++) {\n const sel = optionSelects[i];\n for (let o = 0; o < sel.options.length; o++) {\n sel.options[o].disabled = !isValueAvailable(\n i,\n sel.options[o].value,\n selected,\n );\n }\n }\n };\n\n const onOptionChange = (): void => {\n const v = findMatchingVariant();\n if (!v) {\n // Disabled combination reached by keyboard — revert rather than jump\n // to an unrelated variant.\n syncSelectsToVariant(currentVariant);\n if (currentVariant?.options) recomputeDisabled(currentVariant.options);\n return;\n }\n\n currentVariant = v;\n row.setAttribute(\"data-selected-variant-id\", String(v.id));\n\n const resolvedQty = ruleForVariant(v).min;\n priceSaleEl.textContent = formatMoney(v.price * resolvedQty);\n paintRowCompare(priceCompareEl, v, resolvedQty);\n\n if (v.unitPrice) {\n unitPriceEl.textContent = v.unitPrice;\n unitPriceEl.hidden = false;\n } else {\n unitPriceEl.textContent = \"\";\n unitPriceEl.hidden = true;\n }\n\n const rowImg = thumbDiv.querySelector(\"img\");\n const imgSrc = v.image || p.featuredImage;\n if (rowImg && imgSrc) {\n rowImg.src = imgWidth(imgSrc, PICKER_BASE_WIDTH);\n rowImg.srcset = srcset(imgSrc, PICKER_WIDTHS);\n }\n\n // Bounds depend on the new variant's stock and on how many of it other\n // slots already hold.\n setBoundsForVariant?.(v, capacity.variantUnitsElsewhere(v.id));\n if (v.options) recomputeDisabled(v.options);\n refresh();\n };\n\n const groupsContainer = document.createElement(\"div\");\n groupsContainer.className = \"lb-bundle-variant-option-groups\";\n\n for (let on = 0; on < optionNames.length; on++) {\n const optName = optionNames[on];\n const groupEl = document.createElement(\"div\");\n groupEl.className = \"lb-bundle-variant-option-group\";\n\n const labelEl = document.createElement(\"span\");\n labelEl.className = \"lb-bundle-variant-option-label\";\n labelEl.textContent = optName;\n groupEl.appendChild(labelEl);\n\n const sel = document.createElement(\"select\");\n sel.className = \"lb-mix-match__variant-select\";\n sel.setAttribute(\"data-variant-option\", \"\");\n sel.setAttribute(\"data-option-position\", String(on + 1));\n sel.name = \"lb-variant-\" + p.id + \"-\" + (on + 1);\n sel.setAttribute(\"aria-label\", optName);\n\n const seenValues: Record<string, boolean> = Object.create(null);\n const currentVal = currentVariant?.options?.[on];\n for (const av of availVariants) {\n const val = av.options && av.options[on];\n if (!val || seenValues[val]) continue;\n seenValues[val] = true;\n const opt = document.createElement(\"option\");\n opt.value = val;\n opt.textContent = val;\n if (val === currentVal) opt.selected = true;\n sel.appendChild(opt);\n }\n\n sel.addEventListener(\"change\", onOptionChange);\n optionSelects.push(sel);\n groupEl.appendChild(sel);\n groupsContainer.appendChild(groupEl);\n }\n\n infoDiv.appendChild(groupsContainer);\n if (currentVariant) {\n row.setAttribute(\"data-selected-variant-id\", String(currentVariant.id));\n if (currentVariant.options) recomputeDisabled(currentVariant.options);\n }\n } else if (\n availVariants.length === 1 &&\n availVariants[0].title !== \"Default Title\"\n ) {\n const varLabel = document.createElement(\"span\");\n varLabel.className = \"lb-mix-match__filled-variant\";\n varLabel.textContent = availVariants[0].title;\n infoDiv.appendChild(varLabel);\n }\n\n /* ── Needs-spots hint and actions ──────────────────────────── */\n\n let needsSpotsEl: HTMLElement | null = null;\n if (p.available) {\n needsSpotsEl = document.createElement(\"span\");\n needsSpotsEl.className = \"lb-mix-match__modal-needs-spots\";\n needsSpotsEl.hidden = true;\n infoDiv.appendChild(needsSpotsEl);\n }\n\n let addBtn: HTMLButtonElement | null = null;\n if (p.available) {\n const actionsDiv = document.createElement(\"div\");\n actionsDiv.className = \"lb-mix-match__modal-product-actions\";\n if (qtyGroup) actionsDiv.appendChild(qtyGroup);\n\n addBtn = document.createElement(\"button\");\n addBtn.type = \"button\";\n addBtn.className = \"lb-mix-match__modal-add\";\n addBtn.textContent = t.addItem || \"Add\";\n // Per-row accessible name: without it every Add announces as just \"Add,\n // button\" and a screen reader user can't tell which product it belongs to.\n addBtn.setAttribute(\"aria-label\", (t.addItem || \"Add\") + \" \" + p.title);\n addBtn.setAttribute(\"data-add-product\", String(rowIndex));\n actionsDiv.appendChild(addBtn);\n infoDiv.appendChild(actionsDiv);\n } else {\n const soldOutLabel = document.createElement(\"span\");\n soldOutLabel.className = \"lb-mix-match__modal-sold-out-label\";\n soldOutLabel.textContent = t.soldOut || \"Sold out\";\n row.setAttribute(\"aria-disabled\", \"true\");\n infoDiv.appendChild(soldOutLabel);\n }\n\n row.appendChild(infoDiv);\n\n /* ── Refresh hook ──────────────────────────────────────────── */\n\n let lastVariantId: number | null = currentVariant ? currentVariant.id : null;\n\n function refresh(): void {\n if (!currentVariant) return;\n const variant = currentVariant;\n\n const stock = variant.inventoryQuantity;\n const already =\n typeof stock === \"number\"\n ? capacity.variantUnitsElsewhere(variant.id)\n : 0;\n const inBundle = capacity.isInBundle(variant.id);\n\n if (setBoundsForVariant) {\n // A committed row's stepper mirrors and edits the committed quantity.\n // Switching to an uncommitted variant starts fresh at rule.min rather\n // than dragging the previous variant's quantity across.\n const committed = capacity.ownQty(variant.id);\n if (committed > 0) current = committed;\n else if (lastVariantId !== variant.id)\n current = ruleForVariant(variant).min;\n lastVariantId = variant.id;\n setBoundsForVariant(variant, already);\n }\n\n // A hard-capped required product's uncommitted variants displace sibling\n // units, so the exact-size and capacity gates don't apply to them.\n const rowSwapUnits = capacity.swapUnits(p.id, variant.id);\n const vRule = ruleForVariant(variant);\n const inSwapState = rowSwapUnits >= vRule.min;\n\n const remaining = capacity.remainingSpots();\n const needsMoreSpots =\n !inBundle && !inSwapState && !isRowAddable(vRule, remaining);\n\n if (needsSpotsEl) {\n // Hidden at capacity: the modal's complete state already tells that\n // story for every row at once.\n if (needsMoreSpots && remaining > 0) {\n needsSpotsEl.textContent = formatNeedsSpots(vRule.min, t);\n needsSpotsEl.hidden = false;\n } else {\n needsSpotsEl.hidden = true;\n }\n }\n\n if (addBtn) {\n if (inBundle) {\n addBtn.disabled = false;\n if (!capacity.canRemove(variant.id)) {\n // The required product's last covering pick — an inert \"Required\"\n // rather than a Remove that would silently no-op.\n addBtn.disabled = true;\n addBtn.textContent = t.required || \"Required\";\n addBtn.setAttribute(\n \"aria-label\",\n p.title + \" is required and cannot be removed\",\n );\n addBtn.classList.add(\"lb-mix-match__modal-add--required\");\n } else {\n addBtn.textContent = t.removeItem || \"Remove\";\n addBtn.setAttribute(\n \"aria-label\",\n (t.removeItem || \"Remove\") + \" \" + p.title,\n );\n addBtn.classList.remove(\"lb-mix-match__modal-add--required\");\n }\n addBtn.classList.add(\"lb-mix-match__modal-add--added\");\n row.classList.add(\"lb-mix-match__modal-product--in-bundle\");\n addedBadge.hidden = false;\n } else {\n const swapLabel = t.swapItem || \"Swap\";\n const label = inSwapState ? swapLabel : t.addItem || \"Add\";\n addBtn.textContent = label;\n addBtn.setAttribute(\"aria-label\", label + \" \" + p.title);\n addBtn.classList.remove(\"lb-mix-match__modal-add--added\");\n addBtn.classList.remove(\"lb-mix-match__modal-add--required\");\n row.classList.remove(\"lb-mix-match__modal-product--in-bundle\");\n addedBadge.hidden = true;\n\n const noStock =\n typeof stock === \"number\"\n ? Math.max(0, stock - already) < vRule.min\n : false;\n\n if (inSwapState) {\n // A swap frees its own room, so only stock can block it.\n addBtn.disabled = noStock;\n } else {\n // Sibling-variant slots may already claim so much of rule.max that\n // this variant can't take rule.min.\n const productMaxed =\n pRule.max - capacity.productUnitsElsewhere(p.id, variant.id) <\n vRule.min;\n addBtn.disabled =\n noStock ||\n productMaxed ||\n needsMoreSpots ||\n capacity.isFull();\n }\n }\n }\n\n // The custom dropdown trigger follows its select's disabled attribute via\n // a MutationObserver (see dropdown/bind.ts syncFromSelect), so propagating\n // here keeps the picker chrome consistent with the form control.\n rowDisabled = !p.available;\n stepperDisabled = !p.available || (!inBundle && !!addBtn?.disabled);\n paint?.();\n for (const sel of optionSelects) sel.disabled = rowDisabled;\n }\n\n return { el: row, refresh };\n}\n","const FOCUSABLE =\n 'a[href],button:not([disabled]),input:not([disabled]),' +\n 'select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex=\"-1\"])';\n\n/**\n * Focusable descendants that are actually on screen.\n *\n * `offsetParent === null` filters out anything hidden by `display: none` or an\n * ancestor that is — a filter pill in a collapsed step, say. Without it the\n * trap can send focus to an invisible control and the modal appears to swallow\n * Tab.\n */\nexport function getFocusable(container: HTMLElement): HTMLElement[] {\n const nodes = container.querySelectorAll<HTMLElement>(FOCUSABLE);\n const result: HTMLElement[] = [];\n for (let i = 0; i < nodes.length; i++) {\n if (nodes[i].offsetParent !== null) result.push(nodes[i]);\n }\n return result;\n}\n","/**\n * Body scroll lock that survives iOS Safari.\n *\n * `overflow: hidden` alone does not hold on iOS, so the body is pinned with\n * `position: fixed` at a negative offset and the scroll position is restored\n * on unlock. Reference-counted because the mix-and-match picker can open a\n * dropdown over an already-locked modal, and the inner unlock must not release\n * the outer lock.\n */\nlet savedY = 0;\nlet lockCount = 0;\n\nexport function lock(): void {\n lockCount++;\n if (lockCount > 1) return;\n\n savedY = window.pageYOffset || document.documentElement.scrollTop;\n const body = document.body;\n body.style.position = \"fixed\";\n body.style.top = \"-\" + savedY + \"px\";\n body.style.left = \"0\";\n body.style.right = \"0\";\n body.style.overflow = \"hidden\";\n}\n\nexport function unlock(): void {\n if (lockCount <= 0) return;\n lockCount--;\n if (lockCount > 0) return;\n\n const body = document.body;\n body.style.position = \"\";\n body.style.top = \"\";\n body.style.left = \"\";\n body.style.right = \"\";\n body.style.overflow = \"\";\n window.scrollTo(0, savedY);\n}\n","/**\n * Order the picker's product rows so what's left to pick sits at the top.\n *\n * Products the shopper has already dealt with — any variant committed to the\n * bundle (or the current step), required seeds included — move after the\n * products not yet added. The partition is stable: within each half, rows\n * keep the merchant's product order.\n *\n * Runs only when the modal opens or a step loads, never on add/remove: a\n * list that re-sorts under the shopper's finger loses their place, so a\n * product picked mid-session stays put until the next open or step change.\n *\n * Rows are moved in the DOM rather than rebuilt, and always re-appended from\n * the canonical order, so repeated opens are deterministic and the rows'\n * `data-add-product` indices (which point into the products array, not the\n * DOM) stay valid.\n */\nexport function sinkAddedRows(\n list: HTMLElement,\n rowEls: HTMLElement[],\n isAdded: (rowIndex: number) => boolean,\n): void {\n const front: HTMLElement[] = [];\n const back: HTMLElement[] = [];\n for (let i = 0; i < rowEls.length; i++) {\n (isAdded(i) ? back : front).push(rowEls[i]);\n }\n for (const el of front) list.appendChild(el);\n for (const el of back) list.appendChild(el);\n}\n","import { formatOfSelected, formatSubtitle } from \"./i18n\";\nimport { fill } from \"../picker/i18n\";\nimport {\n buildPickerRow,\n type PickerRow,\n type RowCapacity,\n} from \"../picker/row\";\nimport {\n defaultPickQuantity,\n totalUnits,\n type ProductRule,\n type SelectedItem,\n} from \"../picker/rules\";\nimport { getFocusable } from \"../picker/focus-trap\";\nimport { lock, unlock } from \"../picker/scroll-lock\";\nimport { sinkAddedRows } from \"../picker/sort\";\nimport { debounce, normalizeText } from \"../utils\";\nimport type { DropdownApi } from \"../host\";\nimport type {\n EligibleProduct,\n MixMatchData,\n MixMatchTranslations,\n} from \"./types\";\n\nconst ALL_TYPES = \"__all__\";\n/** Matches the overlay's CSS transition so display:none lands after it. */\nconst CLOSE_TRANSITION_MS = 300;\nconst SEARCH_DEBOUNCE_MS = 200;\n\nexport interface ModalDeps {\n container: HTMLElement;\n data: MixMatchData;\n t: MixMatchTranslations;\n rulesMap: Record<string, ProductRule | undefined>;\n eligibleProducts: EligibleProduct[];\n requiredQty: number;\n showQtySelector: boolean;\n items: SelectedItem[];\n formatMoney: (cents: number) => string;\n /** Answers the row's capacity questions against the live selection. */\n rowCapacity: RowCapacity;\n dropdown: DropdownApi | undefined;\n /** Where to portal the overlay, or null to leave it in place. */\n portalTarget: HTMLElement | null;\n /** Element focus returns to when the picker closes. */\n fallbackFocus: HTMLElement | null;\n /** Called when a row's add/remove/swap changed the selection. */\n onSelectionChanged: () => void;\n /** Add a pick, subject to the capacity and product-max gates. */\n addSelection: (item: SelectedItem) => void;\n removeVariant: (variantId: number) => void;\n swapVariant: (item: SelectedItem, qty: number) => void;\n swapUnitsFor: (productId: number, variantId: number) => number;\n isInBundle: (variantId: number) => boolean;\n atCapacity: () => boolean;\n}\n\nexport interface PickerModal {\n open: () => void;\n close: () => void;\n isOpen: () => boolean;\n /** Recompute every row's bounds and the footer, after any mutation. */\n refresh: () => void;\n overlay: HTMLElement | null;\n}\n\nexport function createPickerModal(deps: ModalDeps): PickerModal {\n const { container, data, t, eligibleProducts, items } = deps;\n\n const overlay = container.querySelector<HTMLElement>(\"[data-modal-overlay]\");\n // Themes routinely wrap sections in a transform or `contain`, either of\n // which makes position:fixed resolve against that ancestor instead of the\n // viewport — so the theme host portals the overlay to <body>. A shadow root\n // has no such ancestor, and moving the overlay out of it would strip the\n // scoped styles, so that host passes null and it stays put.\n if (overlay && deps.portalTarget) deps.portalTarget.appendChild(overlay);\n\n const q = <T extends HTMLElement>(sel: string): T | null =>\n overlay ? overlay.querySelector<T>(sel) : null;\n\n const modalDialog = q<HTMLElement>('[role=\"dialog\"]');\n const searchInput = q<HTMLInputElement>(\"[data-modal-search]\");\n const searchClear = q<HTMLElement>(\"[data-modal-search-clear]\");\n const modalList = q<HTMLElement>(\"[data-modal-list]\");\n const modalEmpty = q<HTMLElement>(\"[data-modal-empty]\");\n const modalLive = q<HTMLElement>(\"[data-modal-live]\");\n const closeBtn = q<HTMLElement>(\"[data-modal-close]\");\n const doneBtn = q<HTMLElement>(\"[data-modal-done]\");\n const footerCountEl = q<HTMLElement>(\"[data-modal-footer-count]\");\n const subtitleEl = q<HTMLElement>(\"[data-modal-subtitle]\");\n const filtersContainer = q<HTMLElement>(\"[data-modal-filters]\");\n\n const showTypeFilters = data.showTypeFilters !== false;\n let activeType = ALL_TYPES;\n let productListBuilt = false;\n let filtersBuilt = false;\n let modalOpen = false;\n let rows: PickerRow[] = [];\n\n function refreshAllStepperBounds(): void {\n for (const r of rows) r.refresh();\n }\n\n function updateModalMeta(): void {\n // Capped for display: over-completion (required 3, two picks of 2) still\n // reads \"3 of 3\".\n const count = Math.min(totalUnits(items), deps.requiredQty);\n const remaining = Math.max(0, deps.requiredQty - count);\n\n if (footerCountEl) {\n footerCountEl.textContent = formatOfSelected(count, deps.requiredQty, t);\n }\n if (subtitleEl) {\n subtitleEl.textContent = formatSubtitle(\n remaining,\n data.discountType,\n data.discountValue,\n t,\n );\n }\n }\n\n function refresh(): void {\n refreshAllStepperBounds();\n updateModalMeta();\n }\n\n /* ── Product list ──────────────────────────────────────────── */\n\n function buildProductList(): void {\n if (!modalList) return;\n\n // Tear the custom dropdowns down before wiping, or their MutationObservers\n // are left watching detached selects.\n deps.dropdown?.unbindAll(modalList);\n modalList.innerHTML = \"\";\n rows = [];\n\n for (let i = 0; i < eligibleProducts.length; i++) {\n const row = buildPickerRow(eligibleProducts[i], i, {\n t,\n rulesMap: deps.rulesMap,\n showQtySelector: deps.showQtySelector,\n capacity: deps.rowCapacity,\n formatMoney: deps.formatMoney,\n onCommittedQtyChanged: () => {\n deps.onSelectionChanged();\n refresh();\n },\n });\n rows.push(row);\n modalList.appendChild(row.el);\n }\n\n // The modal lives outside .lb-bundle-widget once reparented, so the global\n // auto-binder can't reach these selects.\n deps.dropdown?.bindAll(modalList);\n\n modalList.addEventListener(\"click\", onListClick);\n }\n\n function onListClick(e: MouseEvent): void {\n const addBtn = (e.target as HTMLElement).closest<HTMLButtonElement>(\n \"[data-add-product]\",\n );\n if (!addBtn || addBtn.disabled) return;\n\n const prodIndex = parseInt(\n addBtn.getAttribute(\"data-add-product\") ?? \"\",\n 10,\n );\n const prod = eligibleProducts[prodIndex];\n if (!prod) return;\n\n // The row carries the id resolved by its option selects; single-variant\n // rows fall back to the first available variant.\n const row = addBtn.closest<HTMLElement>(\".lb-mix-match__modal-product\");\n const selectedId = row && row.getAttribute(\"data-selected-variant-id\");\n let selectedVariant =\n (selectedId\n ? prod.variants.find((v) => v.id === parseInt(selectedId, 10))\n : null) ?? null;\n if (!selectedVariant) {\n selectedVariant = prod.variants.find((v) => v.available) ?? null;\n }\n if (!selectedVariant) return;\n\n // Toggle off. Slots are variant-scoped, so other variants of the same\n // product keep theirs.\n if (deps.isInBundle(selectedVariant.id)) {\n deps.removeVariant(selectedVariant.id);\n refresh();\n return;\n }\n\n // The stepper publishes its live value in data-qty, so this handler\n // doesn't need to share a closure with it.\n const rowQtyEl = row?.querySelector<HTMLElement>(\"[data-row-qty]\");\n const pickedQty = rowQtyEl\n ? Math.max(\n 1,\n parseInt(\n rowQtyEl.getAttribute(\"data-qty\") || rowQtyEl.textContent || \"\",\n 10,\n ) || 1,\n )\n : defaultPickQuantity(prod.id, deps.rulesMap, selectedVariant.id);\n\n const newItem: SelectedItem = {\n productId: prod.id,\n variantId: selectedVariant.id,\n title: prod.title,\n url: prod.url || null,\n variantTitle: selectedVariant.title,\n featuredImage: selectedVariant.image || prod.featuredImage || null,\n price: selectedVariant.price,\n compareAtPrice: selectedVariant.compareAtPrice || null,\n unitPrice: selectedVariant.unitPrice || null,\n quantity: pickedQty,\n };\n\n // An atomic swap displaces sibling units, so the total never moves and\n // this runs instead of the capacity gate.\n const swapUnits = deps.swapUnitsFor(prod.id, selectedVariant.id);\n if (swapUnits > 0) {\n deps.swapVariant(newItem, Math.min(pickedQty, swapUnits));\n refresh();\n return;\n }\n\n if (deps.atCapacity()) return;\n\n deps.addSelection(newItem);\n refresh();\n }\n\n /* ── Filter pills ──────────────────────────────────────────── */\n\n function buildFilters(): void {\n if (!filtersContainer || !showTypeFilters) return;\n\n const types: string[] = [];\n const seen: Record<string, boolean> = Object.create(null);\n for (const p of eligibleProducts) {\n const ty = (p.type || \"\").trim();\n if (ty && !seen[ty]) {\n seen[ty] = true;\n types.push(ty);\n }\n }\n\n // One type filters nothing — hide the row and let search own the gap.\n if (types.length < 2) {\n filtersContainer.style.display = \"none\";\n modalDialog?.classList.add(\"lb-mix-match__modal--filters-hidden\");\n return;\n }\n\n filtersContainer.innerHTML = \"\";\n for (const value of [ALL_TYPES, ...types]) {\n const pill = document.createElement(\"button\");\n pill.type = \"button\";\n pill.className = \"lb-mix-match__filter\";\n pill.setAttribute(\"data-filter\", value);\n pill.setAttribute(\"aria-pressed\", value === activeType ? \"true\" : \"false\");\n if (value === activeType) pill.classList.add(\"lb-mix-match__filter--active\");\n pill.textContent = value === ALL_TYPES ? t.allTypes || \"All\" : value;\n pill.addEventListener(\"click\", () => {\n activeType = value;\n syncActiveFilterPill();\n filterProducts(searchInput ? searchInput.value : \"\");\n });\n filtersContainer.appendChild(pill);\n }\n }\n\n function syncActiveFilterPill(): void {\n if (!filtersContainer) return;\n const pills = filtersContainer.querySelectorAll<HTMLElement>(\"[data-filter]\");\n for (let i = 0; i < pills.length; i++) {\n const on = pills[i].getAttribute(\"data-filter\") === activeType;\n pills[i].classList.toggle(\"lb-mix-match__filter--active\", on);\n pills[i].setAttribute(\"aria-pressed\", on ? \"true\" : \"false\");\n }\n }\n\n /* ── Search ────────────────────────────────────────────────── */\n\n function filterProducts(query: string): void {\n if (!modalList) return;\n const normalizedQuery = normalizeText(query);\n const listItems = modalList.querySelectorAll<HTMLElement>(\n \"[data-product-item]\",\n );\n let visibleCount = 0;\n\n for (let i = 0; i < listItems.length; i++) {\n const title = listItems[i].getAttribute(\"data-title\") ?? \"\";\n const type = listItems[i].getAttribute(\"data-type\") || \"\";\n const matches =\n (!normalizedQuery || title.indexOf(normalizedQuery) !== -1) &&\n (activeType === ALL_TYPES || type === activeType);\n listItems[i].classList.toggle(\"lb-hidden\", !matches);\n if (matches) visibleCount++;\n }\n\n if (modalEmpty) {\n modalEmpty.style.display =\n visibleCount === 0 && normalizedQuery.length > 0 ? \"\" : \"none\";\n }\n if (searchClear) {\n searchClear.style.display = query.length > 0 ? \"\" : \"none\";\n }\n if (modalLive) {\n modalLive.textContent = fill(\n t.nProductsShown || \"__COUNT__ products shown\",\n { COUNT: visibleCount },\n );\n }\n }\n\n if (searchInput) {\n const handleSearch = debounce(\n () => filterProducts(searchInput.value),\n SEARCH_DEBOUNCE_MS,\n );\n searchInput.addEventListener(\"input\", handleSearch);\n }\n if (searchClear) {\n searchClear.addEventListener(\"click\", () => {\n if (!searchInput) return;\n searchInput.value = \"\";\n filterProducts(\"\");\n searchInput.focus();\n });\n }\n\n /* ── Open / close ──────────────────────────────────────────── */\n\n function open(): void {\n if (!overlay || modalOpen) return;\n modalOpen = true;\n\n if (!productListBuilt) {\n buildProductList();\n productListBuilt = true;\n }\n if (!filtersBuilt) {\n buildFilters();\n filtersBuilt = true;\n }\n\n refresh();\n\n activeType = ALL_TYPES;\n syncActiveFilterPill();\n if (searchInput) searchInput.value = \"\";\n filterProducts(searchInput ? searchInput.value : \"\");\n\n // Products already in the bundle (required seeds included) sink below\n // the ones still to pick. Only here, on open — never on add/remove, so\n // the list doesn't jump under the shopper's finger mid-session.\n if (modalList) {\n const selectedProductIds: Record<string, true> = Object.create(null);\n for (const it of items) selectedProductIds[String(it.productId)] = true;\n sinkAddedRows(\n modalList,\n rows.map((r) => r.el),\n (i) => selectedProductIds[String(eligibleProducts[i].id)] === true,\n );\n modalList.scrollTop = 0;\n }\n\n overlay.style.display = \"\";\n // Force reflow so the transition runs from the hidden state.\n void overlay.offsetHeight;\n overlay.classList.add(\"lb-mix-match__modal-overlay--open\");\n\n lock();\n\n // The close icon, not the search input: focusing search on a phone pops\n // the keyboard over half the modal before the shopper asked for it.\n setTimeout(() => {\n if (closeBtn) closeBtn.focus();\n else modalDialog?.focus();\n }, 50);\n }\n\n function close(): void {\n if (!overlay || !modalOpen) return;\n modalOpen = false;\n\n overlay.classList.remove(\"lb-mix-match__modal-overlay--open\");\n unlock();\n\n setTimeout(() => {\n if (!modalOpen) overlay.style.display = \"none\";\n }, CLOSE_TRANSITION_MS);\n\n const elToFocus = deps.fallbackFocus;\n if (elToFocus && typeof elToFocus.focus === \"function\") {\n setTimeout(() => elToFocus.focus(), 0);\n }\n }\n\n closeBtn?.addEventListener(\"click\", close);\n doneBtn?.addEventListener(\"click\", close);\n\n // Backdrop dismissal tracks mousedown and mouseup so a drag that starts\n // inside the dialog and ends on the backdrop doesn't close it.\n if (overlay) {\n let mouseDownTarget: EventTarget | null = null;\n overlay.addEventListener(\"mousedown\", (e) => {\n mouseDownTarget = e.target;\n });\n overlay.addEventListener(\"mouseup\", (e) => {\n if (e.target === overlay && mouseDownTarget === overlay) close();\n mouseDownTarget = null;\n });\n }\n\n document.addEventListener(\"keydown\", (e) => {\n if (!modalOpen) return;\n\n if (e.key === \"Escape\") {\n e.preventDefault();\n close();\n return;\n }\n\n if (e.key === \"Tab\" && modalDialog) {\n const focusable = getFocusable(modalDialog);\n if (focusable.length === 0) {\n e.preventDefault();\n return;\n }\n const first = focusable[0];\n const last = focusable[focusable.length - 1];\n if (e.shiftKey && document.activeElement === first) {\n e.preventDefault();\n last.focus();\n } else if (!e.shiftKey && document.activeElement === last) {\n e.preventDefault();\n first.focus();\n }\n }\n });\n\n return { open, close, isOpen: () => modalOpen, refresh, overlay };\n}\n","/**\n * Mix & match capacity maths, over one flat selection.\n *\n * Takes the selection as an argument rather than closing over it, so the\n * arithmetic that decides what a shopper may pick is testable without building\n * a modal. Step-agnostic primitives live in ../picker/rules.\n */\nimport {\n indexOfVariantInBundle,\n ruleFor,\n variantRuleFor,\n totalUnits,\n type ProductRule,\n type SelectedItem,\n} from \"../picker/rules\";\n\n\n/**\n * Open spots left. `requiredQty` is a hard bundle size — the picker never lets\n * the unit total overshoot it, so a product whose rule.min exceeds what's left\n * cannot be added at all.\n */\nexport function remainingUnits(\n items: SelectedItem[],\n requiredQty: number,\n): number {\n return Math.max(0, requiredQty - totalUnits(items));\n}\n\nexport function atCapacity(items: SelectedItem[], requiredQty: number): boolean {\n return totalUnits(items) >= requiredQty;\n}\n\n/**\n * Units of a variant already committed.\n *\n * Caps the picker stepper and the Add button against stock: without it a\n * shopper could fill several slots with the same low-stock variant and blow\n * past inventory at checkout.\n */\nexport function alreadyInBundleForVariant(\n items: SelectedItem[],\n variantId: number | string,\n): number {\n let qty = 0;\n for (let i = 0; i < items.length; i++) {\n if (items[i].variantId === variantId) qty += items[i].quantity || 0;\n }\n return qty;\n}\n\n/**\n * Units of a product held by slots other than the given variant's own.\n *\n * `rule.max` caps a product's total across all its variant slots, so a row's\n * ceiling is rule.max minus what its sibling variants already claim.\n */\nexport function productUnitsInOtherSlots(\n items: SelectedItem[],\n productId: number | string,\n excludeVariantId: number | string,\n): number {\n let qty = 0;\n for (let i = 0; i < items.length; i++) {\n const it = items[i];\n if (it && it.productId === productId && it.variantId !== excludeVariantId) {\n qty += it.quantity || 0;\n }\n }\n return qty;\n}\n\n/**\n * Whether a pick may be removed.\n *\n * Product-level lock for required products: removal is allowed only while the\n * product's other slots keep it at or above rule.min. Shoppers can swap\n * variants (add Large, remove Small) but never drop a required product out of\n * the bundle.\n *\n * A required VARIANT is stricter and needs no tally. Slots are one per\n * (product, variant), so a required variant has no other slot to fall back on\n * and no sibling may stand in for it — naming the variant is precisely what\n * takes the swap off the table.\n */\nexport function canRemovePick(\n items: SelectedItem[],\n item: SelectedItem,\n rulesMap: Record<string, ProductRule | undefined>,\n): boolean {\n if (variantRuleFor(item.variantId, rulesMap)?.required) return false;\n\n const rule = ruleFor(item.productId, rulesMap);\n if (!rule.required) return true;\n\n let unitsElsewhere = 0;\n for (let ci = 0; ci < items.length; ci++) {\n const cs = items[ci];\n if (cs !== item && cs.productId === item.productId) {\n unitsElsewhere += cs.quantity || 1;\n }\n }\n return unitsElsewhere >= rule.min;\n}\n\n/**\n * Units available for an atomic variant swap, or 0 when the row isn't in that\n * state.\n *\n * A required product with no headroom (rule.min === rule.max) can't do the\n * add-then-remove swap the product-level lock normally allows: adding a second\n * variant would breach the cap and removing the committed one would break the\n * floor. Those rows get a \"Swap\" button instead, where the new variant's units\n * displace sibling units rather than adding on top.\n */\nexport function swapUnitsFor(\n items: SelectedItem[],\n productId: number | string,\n variantId: number | string,\n rulesMap: Record<string, ProductRule | undefined>,\n): number {\n const rule = ruleFor(productId, rulesMap);\n if (!rule.required || rule.max !== rule.min) return 0;\n if (indexOfVariantInBundle(items, variantId) !== -1) return 0;\n return productUnitsInOtherSlots(items, productId, variantId);\n}\n","import { imgWidth, srcset, THUMB_SIZES, THUMB_WIDTHS } from \"./images\";\nimport type { PickerTranslations, SelectedItem } from \"./types\";\n\nfunction removeIcon(): SVGElement {\n const NS = \"http://www.w3.org/2000/svg\";\n const svg = document.createElementNS(NS, \"svg\");\n svg.setAttribute(\"width\", \"16\");\n svg.setAttribute(\"height\", \"16\");\n svg.setAttribute(\"viewBox\", \"0 0 20 20\");\n svg.setAttribute(\"fill\", \"none\");\n for (const [x1, y1, x2, y2] of [\n [\"5\", \"5\", \"15\", \"15\"],\n [\"15\", \"5\", \"5\", \"15\"],\n ]) {\n const line = document.createElementNS(NS, \"line\");\n line.setAttribute(\"x1\", x1);\n line.setAttribute(\"y1\", y1);\n line.setAttribute(\"x2\", x2);\n line.setAttribute(\"y2\", y2);\n line.setAttribute(\"stroke\", \"currentColor\");\n line.setAttribute(\"stroke-width\", \"2\");\n line.setAttribute(\"stroke-linecap\", \"round\");\n svg.appendChild(line);\n }\n return svg;\n}\n\n/**\n * One filled slot in the widget body.\n *\n * Shared by mix & match and multi-step, which render identical cards — the\n * only difference is which selection the callbacks act on.\n */\nexport function buildSlotCard(\n item: SelectedItem,\n trans: PickerTranslations,\n formatMoney: (cents: number) => string,\n /** False for a required pick at its floor: shows an inert chip instead of ×. */\n removable: boolean,\n onRemove: () => void,\n onEdit: () => void,\n): HTMLElement {\n const card = document.createElement(\"div\");\n card.className = \"lb-mix-match__slot lb-mix-match__slot--filled\";\n\n const thumb = document.createElement(\"div\");\n thumb.className = \"lb-bundle-thumbnail\";\n if (item.featuredImage) {\n const img = document.createElement(\"img\");\n img.src = imgWidth(item.featuredImage, 240);\n img.srcset = srcset(item.featuredImage, THUMB_WIDTHS);\n img.sizes = THUMB_SIZES;\n img.alt = item.title;\n img.loading = \"lazy\";\n thumb.appendChild(img);\n }\n card.appendChild(thumb);\n\n const info = document.createElement(\"div\");\n info.className = \"lb-mix-match__filled-info\";\n\n // A button, not a link: navigating away would destroy the in-memory picks.\n // Product-page access lives on the modal row titles instead.\n const title = document.createElement(\"button\");\n title.type = \"button\";\n title.className = \"lb-mix-match__filled-title\";\n title.textContent = item.title;\n title.setAttribute(\n \"aria-label\",\n (trans.editSelection || \"Edit selection\") + \": \" + item.title,\n );\n info.appendChild(title);\n\n if (item.variantTitle && item.variantTitle !== \"Default Title\") {\n const variant = document.createElement(\"span\");\n variant.className = \"lb-mix-match__filled-variant\";\n variant.textContent = item.variantTitle;\n info.appendChild(variant);\n }\n\n const priceRow = document.createElement(\"span\");\n priceRow.className = \"lb-mix-match__filled-price\";\n const unitCompare = item.compareAtPrice || 0;\n if (unitCompare > item.price) {\n const strikeEl = document.createElement(\"s\");\n strikeEl.className = \"lb-mix-match__filled-compare\";\n strikeEl.textContent = formatMoney(unitCompare);\n priceRow.appendChild(strikeEl);\n }\n const saleEl = document.createElement(\"span\");\n saleEl.className = \"lb-bundle-product-price\";\n saleEl.textContent = formatMoney(item.price);\n priceRow.appendChild(saleEl);\n // Always rendered, even at ×1, so the count reads as a column the way the\n // fixed widget's quantity chip does.\n const qtyInline = document.createElement(\"span\");\n qtyInline.className = \"lb-bundle-qty-inline\";\n qtyInline.textContent = \"×\" + (item.quantity || 1);\n priceRow.appendChild(qtyInline);\n info.appendChild(priceRow);\n\n if (item.unitPrice) {\n const unitPriceEl = document.createElement(\"span\");\n unitPriceEl.className = \"lb-bundle-product-unit-price\";\n unitPriceEl.textContent = item.unitPrice;\n info.appendChild(unitPriceEl);\n }\n card.appendChild(info);\n\n if (removable) {\n const removeBtn = document.createElement(\"button\");\n removeBtn.type = \"button\";\n removeBtn.className = \"lb-mix-match__slot-remove\";\n removeBtn.setAttribute(\n \"aria-label\",\n (trans.removeItem || \"Remove\") + \" \" + item.title,\n );\n removeBtn.appendChild(removeIcon());\n removeBtn.addEventListener(\"click\", (e) => {\n e.stopPropagation();\n onRemove();\n });\n card.appendChild(removeBtn);\n } else {\n // Required pick at its floor — a quiet chip rather than a × that would\n // silently no-op. Removal returns once another slot covers the minimum.\n const requiredChip = document.createElement(\"span\");\n requiredChip.className = \"lb-mix-match__slot-required\";\n requiredChip.textContent = trans.required || \"Required\";\n card.appendChild(requiredChip);\n }\n\n // The whole card reopens the picker; the title button's keyboard activation\n // bubbles here too, and the × stops propagation above.\n card.addEventListener(\"click\", onEdit);\n return card;\n}\n\n","import { formatMoreToGo, formatOfSelected } from \"./i18n\";\nimport { canRemovePick } from \"./rules\";\nimport { buildSlotCard } from \"../picker/slot-card\";\nimport {\n totalUnits,\n type ProductRule,\n type SelectedItem,\n} from \"../picker/rules\";\nimport type { MixMatchData, MixMatchTranslations } from \"./types\";\n\nexport interface SlotRenderDeps {\n formatMoney: (cents: number) => string;\n rulesMap: Record<string, ProductRule | undefined>;\n onRemove: (index: number) => void;\n onEdit: () => void;\n}\n\nexport function updateSlotUI(\n cont: HTMLElement,\n items: SelectedItem[],\n reqQty: number,\n d: MixMatchData,\n deps: SlotRenderDeps,\n): void {\n const trans = d.translations || {};\n const slotsWrap = cont.querySelector<HTMLElement>(\"[data-selection-slots]\");\n\n if (slotsWrap) {\n slotsWrap.innerHTML = \"\";\n for (let i = 0; i < items.length; i++) {\n const index = i;\n slotsWrap.appendChild(\n buildSlotCard(\n items[index],\n trans,\n deps.formatMoney,\n canRemovePick(items, items[index], deps.rulesMap),\n () => deps.onRemove(index),\n deps.onEdit,\n ),\n );\n }\n }\n\n const trigger = cont.querySelector<HTMLElement>(\"[data-add-product-trigger]\");\n if (trigger) {\n trigger.style.display = totalUnits(items) >= reqQty ? \"none\" : \"\";\n }\n\n // Counts total UNITS, not slots filled: requiredQuantity is a unit total, so\n // one product at qty 3 completes a \"pick 3\" bundle — matching the Rust\n // discount function's gate.\n const ctaBtn = cont.querySelector<HTMLButtonElement>(\"[data-add-bundle]\");\n if (ctaBtn) {\n ctaBtn.disabled = totalUnits(items) < reqQty;\n // Written to the label child, never the button's textContent, which would\n // destroy the sibling spinner element. A forked theme missing the label\n // silently skips the text rather than falling back to that.\n const ctaLabel = ctaBtn.querySelector<HTMLElement>(\"[data-cta-label]\");\n if (ctaLabel) {\n ctaLabel.textContent =\n ctaBtn.getAttribute(\"data-cta-text\") || trans.addToCart || \"Add to cart\";\n }\n }\n}\n\nexport function updatePricing(\n cont: HTMLElement,\n items: SelectedItem[],\n reqQty: number,\n d: MixMatchData,\n calcDiscount: (total: number, type: string, value: number) => number,\n paint: (cont: HTMLElement, sale: number, compare: number) => void,\n): void {\n const pricingSection = cont.querySelector<HTMLElement>(\n \"[data-pricing-section]\",\n );\n\n if (totalUnits(items) < reqQty) {\n // Hide the whole summary band — price and savings both live inside it.\n if (pricingSection) pricingSection.style.display = \"none\";\n return;\n }\n\n if (pricingSection) pricingSection.style.display = \"\";\n\n let totalPrice = 0;\n for (let i = 0; i < items.length; i++) {\n totalPrice += items[i].price * (items[i].quantity || 1);\n }\n\n if (totalPrice > 0 && d.discountType) {\n paint(\n cont,\n calcDiscount(totalPrice, d.discountType, d.discountValue ?? 0),\n totalPrice,\n );\n }\n}\n\n/**\n * Repaint the progress bar.\n *\n * Runs over both roots because the modal is reparented to `<body>` and so\n * carries its own copy of the bar outside the widget container.\n */\nexport function updateProgress(\n cont: HTMLElement,\n overlay: HTMLElement | null,\n items: SelectedItem[],\n reqQty: number,\n trans: MixMatchTranslations,\n): void {\n const count = Math.min(totalUnits(items), reqQty);\n const remaining = Math.max(0, reqQty - count);\n\n const roots: HTMLElement[] = [cont];\n if (overlay) roots.push(overlay);\n\n for (const root of roots) {\n const segments = root.querySelectorAll<HTMLElement>(\n \"[data-progress-segment]\",\n );\n for (let s = 0; s < segments.length; s++) {\n segments[s].classList.toggle(\n \"lb-mix-match__progress-segment--filled\",\n s < count,\n );\n }\n\n const progressCount = root.querySelector<HTMLElement>(\n \"[data-progress-count]\",\n );\n if (progressCount) {\n progressCount.textContent = formatOfSelected(count, reqQty, trans);\n }\n\n const progressRemaining = root.querySelector<HTMLElement>(\n \"[data-progress-remaining]\",\n );\n if (progressRemaining) {\n progressRemaining.textContent = formatMoreToGo(remaining, trans);\n }\n\n const segWrap = root.querySelector<HTMLElement>(\n \".lb-mix-match__progress-segments\",\n );\n if (segWrap) segWrap.setAttribute(\"aria-valuenow\", String(count));\n }\n}\n","import type { FormatMoney } from \"@lime-bundles/render/host\";\n\n/**\n * The paint half of the shared pricing update.\n *\n * The theme host passes `LB.updatePricing`, which lives in its own DOM\n * helpers. This is the same thing for a host without that global.\n */\nexport function updatePricing(formatMoney: FormatMoney) {\n return (container: HTMLElement, salePrice: number, comparePrice: number): void => {\n const saleEl = container.querySelector<HTMLElement>(\"[data-sale-price]\");\n const compareEl = container.querySelector<HTMLElement>(\"[data-compare-price]\");\n const savingsBar = container.querySelector<HTMLElement>(\"[data-savings-bar]\");\n const savingsAmountEl = container.querySelector<HTMLElement>(\"[data-savings-amount]\");\n const savingsPercentEl = container.querySelector<HTMLElement>(\"[data-savings-percent]\");\n const savings = comparePrice - salePrice;\n\n if (saleEl) saleEl.textContent = formatMoney(salePrice);\n if (compareEl) {\n if (savings > 0) {\n compareEl.textContent = formatMoney(comparePrice);\n compareEl.style.display = \"\";\n } else {\n compareEl.style.display = \"none\";\n }\n }\n if (savingsBar) {\n if (savings > 0) {\n if (savingsAmountEl) savingsAmountEl.textContent = formatMoney(savings);\n if (savingsPercentEl) {\n const pct = comparePrice > 0 ? Math.round((savings * 100) / comparePrice) : 0;\n savingsPercentEl.textContent = \"(\" + pct + \"%)\";\n }\n savingsBar.style.display = \"\";\n } else {\n savingsBar.style.display = \"none\";\n }\n }\n };\n}\n","import {\n ruleFor,\n variantRuleFor,\n type ProductRule,\n type SelectedItem,\n} from \"../picker/rules\";\nimport type { EligibleProduct, EligibleVariant } from \"./types\";\n\n/**\n * The picks a mix & match bundle starts with.\n *\n * Two rules, in order:\n *\n * 1. **Required picks seed themselves.** Every product whose rule says\n * `required` gets a pick at its `rule.min`, because the discount only\n * applies while the cart holds that many — a shopper should not have to\n * discover that by trial. A required VARIANT seeds that exact variant\n * instead of the first available one: \"must include the black tote\" is a\n * thing the merchant can now say, and seeding an arbitrary colour would\n * both ignore them and fail the checkout gate. A product is required at one\n * level or the other, never both — the admin schema enforces it.\n * 2. **Otherwise, one courtesy pick — if the host asks for it.** Slot zero is\n * filled with the product the shopper is looking at when it qualifies, else\n * the first that does. That is theme UX: the shopper is standing on a\n * product page and expects to see it already in the bundle. A headless\n * embed has no such context, so it opts out and starts empty.\n *\n * Exact-size: a product whose minimum pick would overshoot the bundle size on\n * its own is never seeded.\n */\nexport function seedSelection(\n eligibleProducts: EligibleProduct[],\n rulesMap: Record<string, ProductRule | undefined>,\n requiredQty: number,\n courtesy?: { enabled: boolean; currentProductId?: number | null },\n): SelectedItem[] {\n const firstAvailable = (p: EligibleProduct): EligibleVariant | null =>\n p.variants.find((v) => v.available) ?? null;\n\n const pick = (\n p: EligibleProduct,\n v: EligibleVariant,\n quantity: number,\n ): SelectedItem => ({\n productId: p.id,\n variantId: v.id,\n title: p.title,\n url: p.url || null,\n variantTitle: v.title,\n featuredImage: p.featuredImage || null,\n price: v.price,\n compareAtPrice: v.compareAtPrice || null,\n unitPrice: v.unitPrice || null,\n quantity,\n });\n\n const required: SelectedItem[] = [];\n for (const p of eligibleProducts) {\n if (!p.available || !p.variants) continue;\n\n // Variant-level first: seed every variant the merchant named, at its own\n // minimum. A product with named variants never also carries a\n // product-level `required`, so this is not an either/or on the same units.\n let seededVariant = false;\n for (const v of p.variants) {\n const vRule = variantRuleFor(v.id, rulesMap);\n if (!vRule?.required || !v.available) continue;\n required.push(pick(p, v, Math.max(1, vRule.min)));\n seededVariant = true;\n }\n if (seededVariant) continue;\n\n const rule = ruleFor(p.id, rulesMap);\n if (!rule.required) continue;\n const v = firstAvailable(p);\n if (!v) continue;\n required.push(pick(p, v, Math.max(1, rule.min)));\n }\n if (required.length || !courtesy?.enabled) return required;\n\n const qualifies = (p: EligibleProduct): boolean =>\n p.available && ruleFor(p.id, rulesMap).min <= requiredQty;\n\n const seed =\n (courtesy.currentProductId != null\n ? eligibleProducts.find(\n (p) => p.id === courtesy.currentProductId && qualifies(p),\n )\n : null) ?? eligibleProducts.find(qualifies);\n if (!seed) return [];\n\n const v = firstAvailable(seed);\n return v ? [pick(seed, v, ruleFor(seed.id, rulesMap).min)] : [];\n}\n","\n/**\n * The DOM twin of `lb-picker-modal.liquid`.\n *\n * Mix & match and multi-step both open this shell; the shared `createPickerModal`\n * and `createWizard` query it by the `data-modal-*` hooks below, so those\n * attributes are the contract between the two languages — not the class names,\n * which only carry styling.\n */\n\nfunction icon(paths: Array<[string, string, string, string]>): SVGElement {\n const NS = \"http://www.w3.org/2000/svg\";\n const svg = document.createElementNS(NS, \"svg\");\n svg.setAttribute(\"width\", \"20\");\n svg.setAttribute(\"height\", \"20\");\n svg.setAttribute(\"viewBox\", \"0 0 20 20\");\n svg.setAttribute(\"fill\", \"none\");\n for (const [x1, y1, x2, y2] of paths) {\n const line = document.createElementNS(NS, \"line\");\n line.setAttribute(\"x1\", x1);\n line.setAttribute(\"y1\", y1);\n line.setAttribute(\"x2\", x2);\n line.setAttribute(\"y2\", y2);\n line.setAttribute(\"stroke\", \"currentColor\");\n line.setAttribute(\"stroke-width\", \"2\");\n line.setAttribute(\"stroke-linecap\", \"round\");\n svg.appendChild(line);\n }\n return svg;\n}\n\nfunction el(tag: string, className: string, attrs: Record<string, string> = {}) {\n const node = document.createElement(tag);\n if (className) node.className = className;\n for (const k in attrs) node.setAttribute(k, attrs[k]);\n return node;\n}\n\nexport interface PickerModalOptions {\n bundleGid: string;\n /** Unique per widget, so `aria-labelledby` resolves when several are on a page. */\n domId: string;\n showSearch: boolean;\n showTypeFilters: boolean;\n /**\n * Mix & match renders one progress segment per required unit up front.\n * The wizard leaves the container empty — its per-step segments are rebuilt\n * on every step change.\n */\n segmentCount: number | null;\n wizard: boolean;\n}\n\n/** Build the overlay, hidden. The caller appends it and drives open/close. */\nexport function buildPickerModal(opts: PickerModalOptions): HTMLElement {\n const titleId = \"lb-modal-title-\" + opts.domId;\n\n const overlay = el(\"div\", \"lb-mix-match__modal-overlay\", {\n \"data-modal-overlay\": \"\",\n \"data-bundle-gid\": opts.bundleGid,\n style: \"display: none;\",\n });\n\n const dialog = el(\n \"div\",\n \"lb-mix-match__modal\" +\n (opts.showTypeFilters ? \"\" : \" lb-mix-match__modal--filters-hidden\"),\n {\n role: \"dialog\",\n \"aria-modal\": \"true\",\n \"aria-labelledby\": titleId,\n tabindex: \"-1\",\n },\n );\n\n /* ── Header ────────────────────────────────────────────────── */\n\n const header = el(\"div\", \"lb-mix-match__modal-header\");\n const top = el(\"div\", \"lb-mix-match__modal-header-top\");\n\n const heading = el(\"div\", \"lb-mix-match__modal-heading\");\n const title = el(\"h4\", \"lb-mix-match__modal-title\", { id: titleId });\n // The wizard moves focus here on every step change, so it has to be\n // programmatically focusable.\n if (opts.wizard) title.setAttribute(\"tabindex\", \"-1\");\n title.textContent = \"Add to your bundle\";\n heading.appendChild(title);\n heading.appendChild(\n el(\"p\", \"lb-mix-match__modal-subtitle\", { \"data-modal-subtitle\": \"\" }),\n );\n top.appendChild(heading);\n\n const close = el(\"button\", \"lb-mix-match__modal-close\", {\n type: \"button\",\n \"data-modal-close\": \"\",\n \"aria-label\": \"Close\",\n });\n close.appendChild(\n icon([\n [\"5\", \"5\", \"15\", \"15\"],\n [\"15\", \"5\", \"5\", \"15\"],\n ]),\n );\n top.appendChild(close);\n header.appendChild(top);\n\n const progress = el(\n \"div\",\n \"lb-mix-match__progress lb-mix-match__modal-progress\",\n { \"data-progress\": \"\" },\n );\n const segments = el(\"div\", \"lb-mix-match__progress-segments\", {\n role: \"progressbar\",\n \"aria-valuenow\": \"0\",\n \"aria-valuemin\": \"0\",\n \"aria-valuemax\": String(opts.segmentCount ?? 0),\n });\n if (opts.wizard) {\n segments.setAttribute(\"data-modal-step-segments\", \"\");\n } else {\n for (let i = 0; i < (opts.segmentCount ?? 0); i++) {\n segments.appendChild(\n el(\"span\", \"lb-mix-match__progress-segment\", {\n \"data-progress-segment\": \"\",\n }),\n );\n }\n }\n progress.appendChild(segments);\n header.appendChild(progress);\n dialog.appendChild(header);\n\n /* ── Search and filters ────────────────────────────────────── */\n\n if (opts.showSearch) {\n const search = el(\"div\", \"lb-mix-match__modal-search\");\n const input = el(\"input\", \"lb-mix-match__modal-search-input\", {\n type: \"text\",\n \"data-modal-search\": \"\",\n role: \"searchbox\",\n \"aria-label\": \"Search products\",\n placeholder: \"Search products\",\n autocomplete: \"off\",\n });\n search.appendChild(input);\n\n const clear = el(\"button\", \"lb-mix-match__modal-search-clear\", {\n type: \"button\",\n \"data-modal-search-clear\": \"\",\n \"aria-label\": \"Clear search\",\n style: \"display: none;\",\n });\n clear.appendChild(\n icon([\n [\"5\", \"5\", \"15\", \"15\"],\n [\"15\", \"5\", \"5\", \"15\"],\n ]),\n );\n search.appendChild(clear);\n dialog.appendChild(search);\n }\n\n if (opts.showTypeFilters) {\n dialog.appendChild(\n el(\"div\", \"lb-mix-match__filters\", {\n \"data-modal-filters\": \"\",\n role: \"group\",\n \"aria-label\": \"Filter by type\",\n }),\n );\n }\n\n /* ── List, empty state, live region ────────────────────────── */\n\n dialog.appendChild(\n el(\"div\", \"lb-mix-match__modal-list\", { \"data-modal-list\": \"\" }),\n );\n\n const empty = el(\"div\", \"lb-mix-match__modal-empty\", {\n \"data-modal-empty\": \"\",\n style: \"display: none;\",\n });\n const emptyText = document.createElement(\"p\");\n emptyText.textContent = \"No products match your search\";\n empty.appendChild(emptyText);\n dialog.appendChild(empty);\n\n dialog.appendChild(\n el(\"span\", \"lb-visually-hidden\", {\n \"data-modal-live\": \"\",\n \"aria-live\": \"polite\",\n }),\n );\n\n /* ── Footer ────────────────────────────────────────────────── */\n\n const footer = el(\n \"div\",\n opts.wizard\n ? \"lb-mix-match__modal-footer lb-multi-step__modal-footer\"\n : \"lb-mix-match__modal-footer\",\n );\n\n if (opts.wizard) {\n const back = el(\"button\", \"lb-multi-step__modal-back\", {\n type: \"button\",\n \"data-modal-back\": \"\",\n style: \"display: none;\",\n });\n back.textContent = \"Back\";\n footer.appendChild(back);\n }\n\n footer.appendChild(\n el(\"span\", \"lb-mix-match__modal-footer-count\", {\n \"data-modal-footer-count\": \"\",\n \"aria-live\": \"polite\",\n }),\n );\n\n if (opts.wizard) {\n const next = el(\"button\", \"lb-mix-match__modal-done\", {\n type: \"button\",\n \"data-modal-next\": \"\",\n style: \"display: none;\",\n });\n next.textContent = \"Next\";\n footer.appendChild(next);\n }\n\n const done = el(\"button\", \"lb-mix-match__modal-done\", {\n type: \"button\",\n \"data-modal-done\": \"\",\n ...(opts.wizard ? { style: \"display: none;\" } : {}),\n });\n done.textContent = \"Done\";\n footer.appendChild(done);\n\n dialog.appendChild(footer);\n overlay.appendChild(dialog);\n\n return overlay;\n}\n","import type { CartLineInput, MixMatchBundleData } from \"@lime-bundles/core\";\nimport { adaptProducts } from \"@lime-bundles/render/adapt\";\nimport { bundleLineAttributes, intlFormatMoney } from \"@lime-bundles/render/host\";\nimport { createPickerModal } from \"@lime-bundles/render/mix-match/modal\";\nimport {\n updatePricing,\n updateProgress,\n updateSlotUI,\n} from \"@lime-bundles/render/mix-match/slots\";\nimport type { EligibleProduct } from \"@lime-bundles/render/mix-match/types\";\nimport {\n alreadyInBundleForVariant,\n atCapacity,\n canRemovePick,\n productUnitsInOtherSlots,\n remainingUnits,\n swapUnitsFor,\n} from \"@lime-bundles/render/mix-match/rules\";\nimport {\n committedQtyForVariant,\n indexOfVariantInBundle,\n mergeRuleMaps,\n ruleFor,\n totalUnits,\n variantRuleFor,\n type SelectedItem,\n} from \"@lime-bundles/render/picker/rules\";\nimport { calculateDiscount } from \"@lime-bundles/core\";\nimport { updatePricing as paintPricing } from \"./pricing-paint\";\nimport type { RowCapacity } from \"@lime-bundles/render/picker/row\";\nimport { seedSelection } from \"@lime-bundles/render/mix-match/seed\";\nimport { buildPickerModal } from \"@lime-bundles/render/picker-modal\";\nimport { buildProgressBar, buildWidgetShell } from \"@lime-bundles/render/skeleton\";\nimport { initCountdown } from \"@lime-bundles/render/countdown\";\nimport { DEFAULT_STRINGS } from \"@lime-bundles/render/strings\";\nimport { bindDropdowns } from \"./dropdown-host\";\n\n/**\n * Mix & match, headless.\n *\n * The picker itself was already fully client-rendered on both hosts, so this\n * only has to supply what Liquid supplies in a theme: the widget shell, the\n * modal shell, and the eligible-product pool shaped the way the picker expects.\n */\nexport function renderMixMatchBundle(\n container: HTMLElement,\n bundle: MixMatchBundleData,\n onAddToCart: (lines: CartLineInput[]) => void,\n onCleanup?: (fn: () => void) => void,\n currentProductHandle?: string | null,\n): void {\n const wc = bundle.widgetConfig;\n const t = DEFAULT_STRINGS;\n const requiredQty = bundle.minQuantity || 1;\n const rulesMap = mergeRuleMaps(bundle.productRules, bundle.variantRules);\n\n // The picker's pool is the bundle's products in render-layer shape, plus the\n // per-product availability flag it gates rows on. `adapted` is positionally\n // aligned with `bundle.products`, which still carries the Storefront-shaped\n // `productType` the filter pills group by.\n const adapted = adaptProducts(bundle);\n const eligibleProducts: EligibleProduct[] = adapted.map((p, i) => ({\n id: Number(p.productId),\n title: p.title ?? \"\",\n url: p.url ?? null,\n type: bundle.products[i]?.productType ?? \"\",\n featuredImage: p.featuredImage ?? null,\n available: p.variants.some((v) => v.available),\n optionNames: p.optionNames,\n variants: p.variants.map((v) => ({\n id: Number(v.id),\n title: v.title,\n options: v.options,\n available: v.available,\n price: v.price,\n compareAtPrice: v.compareAtPrice,\n unitPrice: v.unitPrice,\n image: v.image,\n inventoryQuantity: v.inventoryQuantity ?? null,\n })),\n }));\n if (!eligibleProducts.length) return;\n\n // An empty pool can't build anything: hide, matching the theme's units\n // gate and React's in-stock count. Partial stock renders greyed.\n const poolUnits = eligibleProducts.filter((p) => p.available).length;\n if (poolUnits === 0) return;\n\n const formatMoney = intlFormatMoney(\n bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? \"USD\",\n );\n\n const shell = buildWidgetShell(\"lb-mix-match\", {\n title: bundle.title,\n subtitle: bundle.description,\n showSavingsBar: wc.savingsBar?.visible !== false,\n showComparePrice: wc.pricing?.showCompareAtPrice !== false,\n ctaText: wc.cta?.ctaText || t.addToCart || \"Add to cart\",\n endsAt: wc.countdown?.showCountdown === false ? null : bundle.endsAt,\n });\n shell.root.setAttribute(\"data-required-quantity\", String(requiredQty));\n\n // The picker reads its slots and progress from these hooks, which Liquid\n // emits in a theme.\n shell.products.before(buildProgressBar(requiredQty));\n\n const slots = document.createElement(\"div\");\n slots.className = \"lb-mix-match__slots lb-edge-fade\";\n slots.setAttribute(\"data-selection-slots\", \"\");\n shell.products.replaceWith(slots);\n\n const addTrigger = document.createElement(\"button\");\n addTrigger.type = \"button\";\n addTrigger.className = \"lb-mix-match__add-product\";\n addTrigger.setAttribute(\"data-add-product-trigger\", \"\");\n addTrigger.textContent = t.addItem ? \"Add a product\" : \"Add a product\";\n slots.after(addTrigger);\n\n // Same merchant toggles the Liquid host reads (lb-mix-match.liquid's\n // show_search / show_type_filters / show_quantity_selector params).\n const overlay = buildPickerModal({\n bundleGid: bundle.id,\n domId: bundle.id.replace(/\\D/g, \"\"),\n showSearch: wc.showSearch !== false,\n showTypeFilters: wc.mixMatchShowTypeFilters !== false,\n segmentCount: requiredQty,\n wizard: false,\n });\n shell.root.appendChild(overlay);\n\n // Same seeding the theme host does on a product page: required picks\n // first, else a courtesy pick of the product the shopper is looking at,\n // else the first that qualifies. The theme always runs on a PDP; this host\n // only has that context when a product handle resolved, so an embed on a\n // landing page still starts empty.\n const currentIdx = currentProductHandle\n ? bundle.products.findIndex((p) => p.handle === currentProductHandle)\n : -1;\n const selectedItems: SelectedItem[] = seedSelection(\n eligibleProducts,\n rulesMap,\n requiredQty,\n {\n enabled: !!currentProductHandle,\n currentProductId:\n currentIdx !== -1 ? eligibleProducts[currentIdx]?.id ?? null : null,\n },\n );\n\n const rowCapacity: RowCapacity = {\n remainingSpots: () => remainingUnits(selectedItems, requiredQty),\n ownQty: (v) => committedQtyForVariant(selectedItems, v),\n variantUnitsElsewhere: (v) => alreadyInBundleForVariant(selectedItems, v),\n productUnitsElsewhere: (p, v) => productUnitsInOtherSlots(selectedItems, p, v),\n swapUnits: (p, v) => swapUnitsFor(selectedItems, p, v, rulesMap),\n isInBundle: (v) => indexOfVariantInBundle(selectedItems, v) !== -1,\n canRemove: (v) => {\n const i = indexOfVariantInBundle(selectedItems, v);\n return i !== -1 && canRemovePick(selectedItems, selectedItems[i], rulesMap);\n },\n isFull: () => atCapacity(selectedItems, requiredQty),\n commitQty: (v, qty) => {\n const i = indexOfVariantInBundle(selectedItems, v);\n if (i === -1 || selectedItems[i].quantity === qty) return false;\n selectedItems[i].quantity = qty;\n return true;\n },\n };\n\n function updateAll(): void {\n updateSlotUI(shell.root, selectedItems, requiredQty, bundle as never, {\n formatMoney,\n rulesMap,\n onRemove: (i) => {\n if (!canRemovePick(selectedItems, selectedItems[i], rulesMap)) return;\n selectedItems.splice(i, 1);\n updateAll();\n if (modal.isOpen()) modal.refresh();\n },\n onEdit: () => modal.open(),\n });\n updatePricing(\n shell.root,\n selectedItems,\n requiredQty,\n // The shared updater reads the theme data-block shape: discount fields\n // flat, not nested under discountConfig. Passing the bundle itself left\n // `discountType` undefined, so the summary never painted.\n {\n discountType: bundle.discountConfig.discountType,\n discountValue: bundle.discountConfig.discountValue,\n } as never,\n (total, type, value) =>\n calculateDiscount(total, type as \"percentage\" | \"fixed_amount\", value),\n paintPricing(formatMoney),\n );\n updateProgress(shell.root, modal.overlay, selectedItems, requiredQty, t);\n }\n\n const modal = createPickerModal({\n container: shell.root,\n data: { translations: t, discountType: bundle.discountConfig.discountType, discountValue: bundle.discountConfig.discountValue } as never,\n t,\n rulesMap,\n eligibleProducts,\n requiredQty,\n showQtySelector: wc.mixMatchShowQuantitySelector !== false,\n items: selectedItems,\n formatMoney,\n rowCapacity,\n dropdown: undefined,\n // Stays inside the shadow root, which carries the scoped styles.\n portalTarget: null,\n fallbackFocus: addTrigger,\n onSelectionChanged: updateAll,\n addSelection: (item) => {\n if (totalUnits(selectedItems) + (item.quantity || 1) > requiredQty) return;\n // The product envelope, minus what sibling variants already hold. The\n // variant's own cap binds on top of it and is NOT reduced by siblings.\n const vMax = variantRuleFor(item.variantId, rulesMap)?.max;\n let headroom =\n ruleFor(item.productId, rulesMap).max -\n productUnitsInOtherSlots(selectedItems, item.productId, item.variantId);\n if (typeof vMax === \"number\") headroom = Math.min(headroom, vMax);\n if ((item.quantity || 1) > headroom) return;\n selectedItems.push(item);\n updateAll();\n },\n removeVariant: (v) => {\n const i = indexOfVariantInBundle(selectedItems, v);\n if (i === -1 || !canRemovePick(selectedItems, selectedItems[i], rulesMap)) return;\n selectedItems.splice(i, 1);\n updateAll();\n },\n swapVariant: (item, qty) => {\n let freed = 0;\n for (let i = selectedItems.length - 1; i >= 0 && freed < qty; i--) {\n const it = selectedItems[i];\n if (it.productId !== item.productId || it.variantId === item.variantId) continue;\n const take = Math.min(it.quantity || 1, qty - freed);\n if (take >= (it.quantity || 1)) selectedItems.splice(i, 1);\n else it.quantity = (it.quantity || 1) - take;\n freed += take;\n }\n if (freed > 0) {\n item.quantity = freed;\n selectedItems.push(item);\n updateAll();\n }\n },\n swapUnitsFor: (p, v) => swapUnitsFor(selectedItems, p, v, rulesMap),\n isInBundle: (v) => indexOfVariantInBundle(selectedItems, v) !== -1,\n atCapacity: () => atCapacity(selectedItems, requiredQty),\n });\n\n addTrigger.addEventListener(\"click\", () => {\n if (!modal.isOpen()) modal.open();\n });\n\n shell.cta.addEventListener(\"click\", () => {\n if (totalUnits(selectedItems) < requiredQty) return;\n onAddToCart(\n selectedItems.map((item) => ({\n merchandiseId: \"gid://shopify/ProductVariant/\" + item.variantId,\n quantity: item.quantity || 1,\n attributes: bundleLineAttributes(bundle.id, bundle.bundleType),\n })),\n );\n });\n\n container.appendChild(shell.root);\n initCountdown(shell.root);\n updateAll();\n\n onCleanup?.(bindDropdowns(shell.root));\n}\n","/**\n * Per-tier stock gating for the volume widget, shared by both hosts.\n *\n * A \"buy 6\" tier with 3 units left used to render live everywhere; the\n * shopper clicked through to a cart Shopify capped at add time, paid full\n * price, and saw no discount. A tier the current variant can't cover now\n * greys out and refuses selection.\n *\n * `cap` is the same contract as the picker's `inventoryQuantity`: the units\n * the current variant can supply, or `null` when stock never caps\n * (untracked, backorder-allowed, or unknown) — null fails open to \"every\n * tier available\".\n */\n\nconst TIER_OOS_CLASS = \"lb-volume__tier--oos\";\n\nexport function tierQty(el: HTMLElement): number {\n return parseInt(el.getAttribute(\"data-tier-qty\") ?? \"\", 10) || 0;\n}\n\nexport function tierUnavailable(el: HTMLElement): boolean {\n return el.getAttribute(\"aria-disabled\") === \"true\";\n}\n\n/** Stamp availability state onto every tier row for the current variant. */\nexport function applyTierStockState(\n tierEls: ArrayLike<HTMLElement>,\n cap: number | null,\n): void {\n for (let i = 0; i < tierEls.length; i++) {\n const el = tierEls[i];\n const unavailable = cap !== null && tierQty(el) > cap;\n if (unavailable) {\n el.classList.add(TIER_OOS_CLASS);\n el.setAttribute(\"aria-disabled\", \"true\");\n } else {\n el.classList.remove(TIER_OOS_CLASS);\n el.removeAttribute(\"aria-disabled\");\n }\n }\n}\n\n/**\n * The tier index selection should land on: `preferred` when it is still\n * available, otherwise the first available tier, otherwise -1 (nothing the\n * variant can cover — the caller locks the CTA).\n */\nexport function resolveAvailableTierIndex(\n tierEls: ArrayLike<HTMLElement>,\n preferred: number,\n): number {\n const p = tierEls[preferred];\n if (p && !tierUnavailable(p)) return preferred;\n for (let i = 0; i < tierEls.length; i++) {\n if (!tierUnavailable(tierEls[i])) return i;\n }\n return -1;\n}\n\n/** Next selectable index for arrow-key movement; skips greyed tiers. */\nexport function stepToAvailableTier(\n tierEls: ArrayLike<HTMLElement>,\n from: number,\n direction: 1 | -1,\n): number {\n let i = from + direction;\n while (i >= 0 && i < tierEls.length) {\n if (!tierUnavailable(tierEls[i])) return i;\n i += direction;\n }\n return from;\n}\n","import type { FormatMoney } from \"../host\";\nimport type { VolumeTranslations } from \"./types\";\n\n/**\n * Normalise an Ajax Product API price to integer cents.\n *\n * The API returns cents as a number (12900 for $129). The string branch is a\n * guard for contexts that have been observed to return a decimal string.\n */\nexport function toCents(price: number | string): number {\n if (typeof price === \"string\") return Math.round(parseFloat(price) * 100);\n return price;\n}\n\n// NOTE: the tier index resolution helpers (bestValueTierIndex,\n// resolveDefaultTierIndex, resolvePopularTierIndex) live in\n// @lime-bundles/core (bundle/tier-calculator) so the React SDK shares them.\n// They are NOT re-exported here: a re-export of the core barrel would inline\n// the whole core bundle into the committed theme asset, which never uses\n// them — Liquid resolves the indices server-side. Hosts that need them\n// import them from core directly.\n\n/**\n * Per-unit price for one tier.\n *\n * Percentage floors the discount per unit, matching the checkout's rounding —\n * the same rule the fixed bundle follows.\n *\n * Currency: `data-tier-amt` must be in the same currency as `basePrice` —\n * the currency the host renders in. The merchant configures the amount in\n * the shop's STORE currency, so each host converts before this code runs:\n * the theme entry rewrites `data-tier-amt` at hydration from\n * `Shopify.currency.rate`, and the headless renderer builds the tier from a\n * bundle already converted by the consumer-supplied `currencyRate`. See the\n * currency note on `computeBundlePricing` in ../fixed/pricing.ts.\n */\nexport function calcTierPrice(\n basePrice: number,\n discountType: string,\n tierEl: HTMLElement,\n): number {\n if (discountType === \"fixed_amount\") {\n const amt = parseInt(tierEl.getAttribute(\"data-tier-amt\") ?? \"\", 10) || 0;\n return Math.max(0, basePrice - amt);\n }\n const pct = parseInt(tierEl.getAttribute(\"data-tier-pct\") ?? \"\", 10) || 0;\n return Math.max(0, basePrice - Math.floor((basePrice * pct) / 100));\n}\n\nexport interface TierTotals {\n qty: number;\n priceEach: number;\n totalPrice: number;\n undiscountedTotal: number;\n totalSavings: number;\n}\n\n/** Derive one tier's totals. Pure, so the arithmetic is testable without a DOM. */\nexport function computeTierTotals(\n basePrice: number,\n discountType: string,\n tierEl: HTMLElement,\n): TierTotals {\n const qty = parseInt(tierEl.getAttribute(\"data-tier-qty\") ?? \"\", 10);\n const priceEach = calcTierPrice(basePrice, discountType, tierEl);\n const totalPrice = priceEach * qty;\n const undiscountedTotal = basePrice * qty;\n return {\n qty,\n priceEach,\n totalPrice,\n undiscountedTotal,\n totalSavings: undiscountedTotal - totalPrice,\n };\n}\n\n/**\n * Resolve the \"(N items)\" label for a quantity.\n *\n * Uses Intl.PluralRules against the shop's locale so languages with more than\n * two plural forms read correctly; `__COUNT__` is the placeholder the Liquid\n * translation carries.\n */\nexport function formatItemCount(\n qty: number,\n translations: VolumeTranslations,\n): string | null {\n if (!translations.itemCount) return null;\n const form = new Intl.PluralRules(translations.locale || \"en\").select(qty);\n const tpl =\n translations.itemCount[form] || translations.itemCount.other || \"__COUNT__\";\n return \" (\" + tpl.split(\"__COUNT__\").join(String(qty)) + \")\";\n}\n\n/** Repaint every tier row's per-unit and compare-at price after a base price change. */\nexport function updateAllTierPrices(\n allTiers: ArrayLike<HTMLElement>,\n basePrice: number,\n discountType: string,\n formatMoney: FormatMoney,\n): void {\n for (let i = 0; i < allTiers.length; i++) {\n const tierEl = allTiers[i];\n const priceEachEl = tierEl.querySelector<HTMLElement>(\n \"[data-tier-price-each]\",\n );\n if (priceEachEl) {\n priceEachEl.textContent = formatMoney(\n calcTierPrice(basePrice, discountType, tierEl),\n );\n }\n const compareEl = tierEl.querySelector<HTMLElement>(\n \".lb-volume__tier-compare\",\n );\n if (compareEl) compareEl.textContent = formatMoney(basePrice);\n }\n}\n\n/** Mark a tier selected and repaint the total, item count and savings. */\nexport function selectTier(\n container: HTMLElement,\n allTiers: ArrayLike<HTMLElement>,\n basePrice: number,\n discountType: string,\n index: number,\n translations: VolumeTranslations,\n formatMoney: FormatMoney,\n): void {\n for (let i = 0; i < allTiers.length; i++) {\n allTiers[i].setAttribute(\"aria-checked\", i === index ? \"true\" : \"false\");\n // Roving tabindex: only the selected tier is in the tab order, so the\n // group is one stop and arrow keys move within it.\n allTiers[i].setAttribute(\"tabindex\", i === index ? \"0\" : \"-1\");\n }\n\n const { qty, totalPrice, undiscountedTotal, totalSavings } =\n computeTierTotals(basePrice, discountType, allTiers[index]);\n\n const itemCountEl = container.querySelector<HTMLElement>(\"[data-item-count]\");\n if (itemCountEl) {\n const label = formatItemCount(qty, translations);\n if (label !== null) itemCountEl.textContent = label;\n }\n\n const totalPriceEl = container.querySelector<HTMLElement>(\"[data-total-price]\");\n if (totalPriceEl) totalPriceEl.textContent = formatMoney(totalPrice);\n\n const compareEl = container.querySelector<HTMLElement>(\"[data-compare-price]\");\n if (compareEl) {\n if (totalSavings > 0) {\n compareEl.textContent = formatMoney(undiscountedTotal);\n compareEl.style.display = \"\";\n } else {\n compareEl.style.display = \"none\";\n }\n }\n\n const savingsBar = container.querySelector<HTMLElement>(\"[data-savings-bar]\");\n const savingsAmountEl =\n container.querySelector<HTMLElement>(\"[data-savings-amount]\");\n const savingsPercentEl = container.querySelector<HTMLElement>(\n \"[data-savings-percent]\",\n );\n if (savingsBar) {\n if (totalSavings > 0) {\n if (savingsAmountEl) {\n savingsAmountEl.textContent = formatMoney(totalSavings);\n }\n if (savingsPercentEl) {\n const savingsPct =\n undiscountedTotal > 0\n ? Math.round((totalSavings * 100) / undiscountedTotal)\n : 0;\n savingsPercentEl.textContent = \"(\" + savingsPct + \"%)\";\n }\n savingsBar.style.display = \"\";\n } else {\n savingsBar.style.display = \"none\";\n }\n }\n}\n","import { bundleLineAttributes, intlFormatMoney } from \"@lime-bundles/render/host\";\nimport { buildVolumeTiers, buildWidgetShell, type TierSpec } from \"@lime-bundles/render/skeleton\";\nimport { initCountdown } from \"@lime-bundles/render/countdown\";\nimport { DEFAULT_STRINGS } from \"@lime-bundles/render/strings\";\nimport {\n bestValueTierIndex,\n isVariantFulfillable,\n resolveContextProduct,\n resolveDefaultTierIndex,\n resolvePopularTierIndex,\n stockCapForVariant,\n type CartLineInput,\n type ProductContext,\n type VolumeBundleData,\n} from \"@lime-bundles/core\";\nimport {\n applyTierStockState,\n resolveAvailableTierIndex,\n stepToAvailableTier,\n tierUnavailable,\n} from \"@lime-bundles/render/volume/stock\";\nimport { selectTier, updateAllTierPrices } from \"@lime-bundles/render/volume/pricing\";\nimport { toCents } from \"@lime-bundles/render/adapt\";\nimport { fill } from \"@lime-bundles/render/picker/i18n\";\n\n/**\n * Volume / quantity-break bundle, headless.\n *\n * The tier list is built here rather than hydrated: unlike a product row it has\n * no server-rendered counterpart in this host, and its prices are recomputed\n * from `data-tier-*` by the shared `selectTier` the moment it is built. That is\n * also why volume has no layout-shift exposure — it paints once.\n */\nexport function renderVolumeBundle(\n container: HTMLElement,\n bundle: VolumeBundleData,\n onAddToCart: (lines: CartLineInput[]) => void,\n productContext?: ProductContext,\n): void {\n const wc = bundle.widgetConfig;\n // Price and sell the product page the widget is standing on, the way the\n // theme widget binds to Liquid's `product`; first product only when the\n // host supplied no context.\n const product = resolveContextProduct(bundle.products, productContext);\n if (!product) return;\n\n // Price the tiers from a variant that can cover the smallest tier,\n // falling back through merely-purchasable to the first so a fully\n // sold-out product still renders numbers rather than NaN.\n const variants = product.variants.nodes;\n const minTierQty = Math.min(\n ...bundle.volumeTiers.map((tier) => tier.minQuantity),\n );\n const pricingVariant =\n variants.find((v) => isVariantFulfillable(v, minTierQty)) ??\n variants.find((v) => v.availableForSale) ??\n variants[0];\n if (!pricingVariant) return;\n\n const basePrice = toCents(pricingVariant.price.amount);\n const discountType = bundle.discountConfig.discountType;\n const formatMoney = intlFormatMoney(product.priceRange.minVariantPrice.currencyCode);\n const t = DEFAULT_STRINGS;\n\n // The same three resolutions lb-volume.liquid performs, in the same order:\n // best-savings tier first, then the merchant's default tier and the popular\n // badge position, both of which fall back onto it.\n const bestIdx = bestValueTierIndex(bundle.volumeTiers, discountType);\n const defaultIdx = resolveDefaultTierIndex(\n wc.defaultTier,\n bundle.volumeTiers.length,\n bestIdx,\n );\n const popularIdx = resolvePopularTierIndex(\n wc.popularBadge?.tierIndex,\n wc.popularBadge?.visible !== false,\n bestIdx,\n );\n\n const tiers: TierSpec[] = bundle.volumeTiers.map((tier, i) => {\n const percent = discountType === \"fixed_amount\" ? 0 : Math.round(tier.percentage ?? 0);\n const amountCents = discountType === \"fixed_amount\" ? Math.round((tier.amount ?? 0) * 100) : 0;\n const priceEach =\n discountType === \"fixed_amount\"\n ? Math.max(0, basePrice - amountCents)\n : Math.max(0, basePrice - Math.floor((basePrice * percent) / 100));\n const savedPct =\n basePrice > 0 ? Math.round(((basePrice - priceEach) * 100) / basePrice) : 0;\n\n return {\n quantity: tier.minQuantity,\n percent,\n amountCents,\n label: fill(t.buyQty || \"Buy __COUNT__\", { COUNT: tier.minQuantity }),\n savingsLabel: savedPct > 0 ? \"Save \" + savedPct + \"%\" : null,\n badgeLabel: i === popularIdx ? wc.popularBadge?.text || \"Most popular\" : null,\n selected: i === defaultIdx,\n };\n });\n if (!tiers.length) return;\n\n const shell = buildWidgetShell(\"lb-volume\", {\n title: bundle.title,\n subtitle: bundle.description,\n summaryLabel: \"Total\",\n withItemCount: wc.pricing?.showItemCount !== false,\n totalPriceSlot: true,\n showSavingsBar: wc.savingsBar?.visible !== false,\n showComparePrice: wc.pricing?.showCompareAtPrice !== false,\n ctaText: wc.cta?.ctaText || t.addToCart || \"Add to cart\",\n endsAt: wc.countdown?.showCountdown === false ? null : bundle.endsAt,\n });\n\n const group = buildVolumeTiers(tiers, \"Select quantity\", {\n showCompare: wc.pricing?.showComparePrice !== false,\n showPriceEach: wc.pricing?.showPerUnitPrice !== false,\n unitLabel: (t.each || \"each\").trim(),\n });\n shell.products.replaceWith(group);\n\n const tierEls = group.querySelectorAll<HTMLElement>(\"[data-tier-index]\");\n // Paint each row's struck base price and discounted per-unit price — the\n // Liquid renders these server-side; this host has to fill its own skeleton.\n updateAllTierPrices(tierEls, basePrice, discountType, formatMoney);\n\n // Grey the tiers the pricing variant can't cover, and land the initial\n // selection on one it can.\n applyTierStockState(tierEls, stockCapForVariant(pricingVariant));\n let selectedIndex = resolveAvailableTierIndex(tierEls, defaultIdx);\n const noTierAvailable = selectedIndex === -1;\n if (noTierAvailable) selectedIndex = defaultIdx;\n\n const apply = () =>\n selectTier(shell.root, tierEls, basePrice, discountType, selectedIndex, t, formatMoney);\n\n group.addEventListener(\"click\", (e) => {\n const el = (e.target as HTMLElement).closest<HTMLElement>(\"[data-tier-index]\");\n if (!el || tierUnavailable(el)) return;\n selectedIndex = parseInt(el.getAttribute(\"data-tier-index\") ?? \"\", 10) || 0;\n apply();\n });\n\n // Arrow keys move within the group, matching the roving tabindex selectTier\n // maintains. Greyed tiers are skipped, not landed on.\n group.addEventListener(\"keydown\", (e) => {\n if (e.key === \"ArrowDown\" || e.key === \"ArrowRight\") {\n e.preventDefault();\n selectedIndex = stepToAvailableTier(tierEls, selectedIndex, 1);\n apply();\n tierEls[selectedIndex].focus();\n } else if (e.key === \"ArrowUp\" || e.key === \"ArrowLeft\") {\n e.preventDefault();\n selectedIndex = stepToAvailableTier(tierEls, selectedIndex, -1);\n apply();\n tierEls[selectedIndex].focus();\n }\n });\n\n if (noTierAvailable) {\n shell.cta.disabled = true;\n const label = shell.cta.querySelector(\"[data-cta-label]\");\n if (label) label.textContent = t.outOfStock || \"Out of stock\";\n } else {\n shell.cta.addEventListener(\"click\", () => {\n const qty = parseInt(\n tierEls[selectedIndex].getAttribute(\"data-tier-qty\") ?? \"\",\n 10,\n );\n onAddToCart([\n {\n merchandiseId: pricingVariant.id,\n quantity: qty,\n attributes: bundleLineAttributes(bundle.id, bundle.bundleType),\n },\n ]);\n });\n }\n\n container.appendChild(shell.root);\n initCountdown(shell.root);\n apply();\n}\n","import type { FormatMoney } from \"../host\";\nimport { findVariant } from \"../product/variants\";\nimport type { BundleProduct, BundleVariant } from \"../product/types\";\n\n/** One side of the offer. Same shape as any bundle product, plus its role. */\nexport interface BogoSide extends BundleProduct {\n role: \"buy\" | \"get\";\n}\n\n/** Per-unit discounted price, floored the way Shopify's discount engine rounds. */\nexport function discountedUnit(priceCents: number, percent: number): number {\n return priceCents - Math.floor((priceCents * percent) / 100);\n}\n\nexport interface BogoTotals {\n totalPrice: number;\n salePrice: number;\n savings: number;\n}\n\n/**\n * Totals for one reward set: N of the buy side plus M of the get side, with\n * the discount applied only to the get side.\n *\n * Pure, so the arithmetic is testable without a DOM.\n */\nexport function computeBogoTotals(\n sides: BogoSide[],\n percent: number,\n buyQty: number,\n getQty: number,\n): BogoTotals {\n const buyVariant = findVariant(sides[0], sides[0].selectedVariantId);\n const getVariant = findVariant(sides[1], sides[1].selectedVariantId);\n\n const totalPrice = buyVariant.price * buyQty + getVariant.price * getQty;\n const salePrice =\n buyVariant.price * buyQty +\n discountedUnit(getVariant.price, percent) * getQty;\n\n return { totalPrice, salePrice, savings: totalPrice - salePrice };\n}\n\n/**\n * Reprice one card's inline price elements.\n *\n * The get card shows the struck original beside the discounted unit — the same\n * treatment the admin preview and the headless renderers use. Liquid renders\n * the plain price server-side and this applies the reward look.\n */\nexport function repriceRow(\n row: HTMLElement,\n variant: BundleVariant,\n percent: number,\n isGet: boolean,\n formatMoney: FormatMoney,\n): void {\n const priceEl = row.querySelector<HTMLElement>(\"[data-product-price]\");\n const compareEl = row.querySelector<HTMLElement>(\n \"[data-product-compare-price]\",\n );\n if (!priceEl) return;\n\n if (isGet && percent > 0) {\n priceEl.textContent = formatMoney(discountedUnit(variant.price, percent));\n if (compareEl) {\n compareEl.textContent = formatMoney(variant.price);\n compareEl.hidden = false;\n }\n return;\n }\n\n priceEl.textContent = formatMoney(variant.price);\n if (compareEl) {\n const compare = variant.compareAtPrice;\n if (compare && compare > variant.price) {\n compareEl.textContent = formatMoney(compare);\n compareEl.hidden = false;\n } else {\n compareEl.textContent = \"\";\n compareEl.hidden = true;\n }\n }\n}\n\n/** Recompute and paint the bundle summary. */\nexport function recalcPricing(\n container: HTMLElement,\n sides: BogoSide[],\n percent: number,\n buyQty: number,\n getQty: number,\n formatMoney: FormatMoney,\n): void {\n const { totalPrice, salePrice, savings } = computeBogoTotals(\n sides,\n percent,\n buyQty,\n getQty,\n );\n\n const saleEl = container.querySelector<HTMLElement>(\"[data-sale-price]\");\n const compareEl = container.querySelector<HTMLElement>(\"[data-compare-price]\");\n const savingsBar = container.querySelector<HTMLElement>(\"[data-savings-bar]\");\n const savingsAmountEl =\n container.querySelector<HTMLElement>(\"[data-savings-amount]\");\n const savingsPercentEl = container.querySelector<HTMLElement>(\n \"[data-savings-percent]\",\n );\n\n if (saleEl) saleEl.textContent = formatMoney(salePrice);\n\n if (compareEl) {\n if (savings > 0) {\n compareEl.textContent = formatMoney(totalPrice);\n compareEl.style.display = \"\";\n } else {\n compareEl.style.display = \"none\";\n }\n }\n\n if (savingsBar) {\n if (savings > 0) {\n if (savingsAmountEl) savingsAmountEl.textContent = formatMoney(savings);\n if (savingsPercentEl) {\n const pct =\n totalPrice > 0 ? Math.round((savings * 100) / totalPrice) : 0;\n savingsPercentEl.textContent = \"(\" + pct + \"%)\";\n }\n savingsBar.style.display = \"\";\n } else {\n savingsBar.style.display = \"none\";\n }\n }\n}\n","import { discountedUnit, type BogoSide } from \"./pricing\";\nimport { findVariant } from \"../product/variants\";\nimport {\n BUNDLE_ROLE_ATTRIBUTE,\n BUNDLE_ROLE_GET,\n type CartItem,\n} from \"../host\";\n\n/**\n * Build the add-to-cart lines for one reward set.\n *\n * When both sides resolve to the same variant they merge into a single line of\n * buyQty + getQty, so the theme's cart doesn't show the same product twice.\n * The discount function's same-product pool maths is line-agnostic, so\n * checkout charges identically either way.\n *\n * A distinct reward line carries `_lime_bundle_role: \"get\"`, which is how the\n * discount function knows which line the shopper chose. Without it the\n * function fell back to cheapest-first over every line of the get product —\n * including the buy line on a same-product offer — and discounted a variant\n * the widget had not struck through (issue #421). The merged line carries no\n * role: one line, nothing to choose.\n */\nexport function buildBogoCartItems(\n sides: BogoSide[],\n percent: number,\n buyQty: number,\n getQty: number,\n): CartItem[] {\n const buyVariant = findVariant(sides[0], sides[0].selectedVariantId);\n const getVariant = findVariant(sides[1], sides[1].selectedVariantId);\n\n const buyLine: CartItem = {\n variantId: Number(sides[0].selectedVariantId),\n quantity: buyQty,\n priceCents: (buyVariant.price || 0) * buyQty,\n };\n const getPriceCents = discountedUnit(getVariant.price || 0, percent) * getQty;\n\n if (String(sides[0].selectedVariantId) === String(sides[1].selectedVariantId)) {\n return [\n {\n variantId: buyLine.variantId,\n quantity: buyQty + getQty,\n priceCents: (buyLine.priceCents ?? 0) + getPriceCents,\n },\n ];\n }\n\n return [\n buyLine,\n {\n variantId: Number(sides[1].selectedVariantId),\n quantity: getQty,\n priceCents: getPriceCents,\n attributes: [{ key: BUNDLE_ROLE_ATTRIBUTE, value: BUNDLE_ROLE_GET }],\n },\n ];\n}\n","import type { BogoBundleData, CartLineInput } from \"@lime-bundles/core\";\nimport { adaptProduct } from \"@lime-bundles/render/adapt\";\nimport { buildBogoCartItems } from \"@lime-bundles/render/bogo/cart\";\nimport { recalcPricing, repriceRow, type BogoSide } from \"@lime-bundles/render/bogo/pricing\";\nimport { bundleLineAttributes, intlFormatMoney } from \"@lime-bundles/render/host\";\nimport { bindVariantSelects } from \"@lime-bundles/render/product/option-selects\";\nimport { hydrateRowControls } from \"@lime-bundles/render/product/hydrate\";\nimport { updateThumbnail } from \"@lime-bundles/render/product/row\";\nimport { findVariant } from \"@lime-bundles/render/product/variants\";\nimport { buildRowSkeleton, buildWidgetShell } from \"@lime-bundles/render/skeleton\";\nimport { initCountdown } from \"@lime-bundles/render/countdown\";\nimport { DEFAULT_STRINGS, formatItemsOutOfStock } from \"@lime-bundles/render/strings\";\nimport { bindDropdowns } from \"./dropdown-host\";\n\n/** The \"+\" medallion between the buy and get cards — the DOM twin of the one\n * `lb-bogo.liquid` renders. */\nfunction buildBogoPlus(): HTMLElement {\n const plus = document.createElement(\"div\");\n plus.className = \"lb-bogo__plus\";\n plus.setAttribute(\"aria-hidden\", \"true\");\n plus.innerHTML =\n '<svg width=\"12\" height=\"12\" viewBox=\"0 0 18 18\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\">' +\n '<line x1=\"9\" y1=\"3\" x2=\"9\" y2=\"15\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"/>' +\n '<line x1=\"3\" y1=\"9\" x2=\"15\" y2=\"9\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"/>' +\n \"</svg>\";\n return plus;\n}\n\n/**\n * Buy X Get Y, headless.\n *\n * Two rows, one per side of the offer, built from the same skeleton the theme\n * gets from Liquid and hydrated through the same path. The reward side's\n * discount is applied on hydration rather than baked into the markup, which is\n * exactly why its price box has to be reserved.\n *\n * All-or-nothing: either side being unfulfillable locks the CTA, and `hide`\n * suppresses the widget entirely.\n */\nexport function renderBogoBundle(\n container: HTMLElement,\n bundle: BogoBundleData,\n onAddToCart: (lines: CartLineInput[]) => void,\n onCleanup?: (fn: () => void) => void,\n): void {\n const wc = bundle.widgetConfig;\n const buyProduct = bundle.products.find((p) => p.id === bundle.buyProductId);\n const getProduct = bundle.products.find((p) => p.id === bundle.getProductId);\n if (!buyProduct || !getProduct) return;\n\n const buyQty = bundle.buyQuantity;\n const getQty = bundle.getQuantity;\n const percent = bundle.discountConfig.discountValue;\n\n const quantities = {\n productQuantities: bundle.productQuantities,\n variantQuantities: bundle.variantQuantities,\n };\n // Per-side narrowing (issue #436): the positional selectedVariantIds\n // cannot hold two lists for a same-product BOGO, so each side carries its\n // own. An empty (or absent, for externally assembled data) list means the\n // merchant left that side open.\n const buyAllowed = bundle.buyVariantIds?.length ? bundle.buyVariantIds : null;\n const getAllowed = bundle.getVariantIds?.length ? bundle.getVariantIds : null;\n const sides: BogoSide[] = [\n {\n ...adaptProduct(buyProduct, bundle.id, {\n quantities,\n allowedVariantIds: buyAllowed,\n }),\n role: \"buy\",\n },\n {\n ...adaptProduct(getProduct, bundle.id, {\n quantities,\n allowedVariantIds: getAllowed,\n }),\n role: \"get\",\n },\n ];\n\n // Sold-out sides render greyed with the CTA locked; the status flip\n // hides a bundle that can't be bought.\n const oosCount = sides.filter((s) => !s.variants.some((v) => v.available)).length;\n\n const formatMoney = intlFormatMoney(\n buyProduct.priceRange.minVariantPrice.currencyCode,\n );\n const t = DEFAULT_STRINGS;\n\n const shell = buildWidgetShell(\"lb-bogo\", {\n title: bundle.title,\n subtitle: bundle.description,\n showSavingsBar: wc.savingsBar?.visible !== false,\n showComparePrice: wc.pricing?.showCompareAtPrice !== false,\n ctaText: wc.cta?.ctaText || t.addToCart || \"Add to cart\",\n endsAt: wc.countdown?.showCountdown === false ? null : bundle.endsAt,\n });\n shell.root.setAttribute(\"data-discount-value\", String(percent));\n shell.root.setAttribute(\"data-buy-qty\", String(buyQty));\n shell.root.setAttribute(\"data-get-qty\", String(getQty));\n\n const badgeText = percent >= 100 ? \"Free\" : percent + \"% off\";\n\n sides.forEach((side, i) => {\n const isGet = i === 1;\n // The medallion sits between the two cards, outside either row, exactly\n // where lb-bogo.liquid puts it.\n if (isGet) shell.products.appendChild(buildBogoPlus());\n const isOos = !side.variants.some((v) => v.available);\n const row = buildRowSkeleton(side, t, {\n isOos,\n href: side.url,\n bogoRole: side.role,\n inlineQuantity: isGet ? getQty : buyQty,\n badgeText: isGet && !isOos ? badgeText : null,\n thumbnailRatio: wc.productList?.thumbnailRatio,\n });\n shell.products.appendChild(row);\n if (isOos) return;\n\n const initial = findVariant(side, side.selectedVariantId);\n hydrateRowControls(row, side, initial);\n updateThumbnail(row, initial, side.featuredImage);\n repriceRow(row, initial, percent, isGet, formatMoney);\n\n bindVariantSelects(\n row,\n () => side,\n (variant) => {\n updateThumbnail(row, variant, side.featuredImage);\n repriceRow(row, variant, percent, isGet, formatMoney);\n recalcPricing(shell.root, sides, percent, buyQty, getQty, formatMoney);\n },\n );\n });\n\n if (oosCount > 0) {\n shell.cta.disabled = true;\n const label = shell.cta.querySelector(\"[data-cta-label]\");\n if (label) {\n label.textContent = formatItemsOutOfStock(t, oosCount);\n }\n } else {\n shell.cta.addEventListener(\"click\", () => {\n onAddToCart(\n buildBogoCartItems(sides, percent, buyQty, getQty).map((item) => ({\n merchandiseId: \"gid://shopify/ProductVariant/\" + item.variantId,\n quantity: item.quantity,\n // Per-line extras (the reward marker) on top of the attribution\n // every line carries.\n attributes: [\n ...bundleLineAttributes(bundle.id, bundle.bundleType),\n ...(item.attributes ?? []),\n ],\n })),\n );\n });\n }\n\n container.appendChild(shell.root);\n initCountdown(shell.root);\n recalcPricing(shell.root, sides, percent, buyQty, getQty, formatMoney);\n\n onCleanup?.(bindDropdowns(shell.root));\n}\n","import { fill } from \"../picker/i18n\";\nimport type { MultiStepTranslations } from \"./types\";\n\nexport { formatNeedsSpots } from \"../picker/i18n\";\n\nexport function stepOfLabel(\n i: number,\n total: number,\n t: MultiStepTranslations,\n): string {\n return fill(t.stepOf || \"Step __STEP__ of __TOTAL__\", {\n STEP: i + 1,\n TOTAL: total,\n });\n}\n\nexport function ofSelectedLabel(\n count: number,\n total: number,\n t: MultiStepTranslations,\n): string {\n return fill(t.ofSelected || \"__COUNT__ of __TOTAL__ added\", {\n COUNT: count,\n TOTAL: total,\n });\n}\n\n/**\n * Overall progress counts steps, not units.\n *\n * Unit detail lives on the per-step count chips, so the top-level bar stays\n * legible on a wizard with several steps.\n */\nexport function stepsCompletedLabel(\n completed: number,\n total: number,\n t: MultiStepTranslations,\n): string {\n return fill(t.stepsCompleted || \"__COUNT__ of __TOTAL__ steps completed\", {\n COUNT: completed,\n TOTAL: total,\n });\n}\n\nexport function moreToGoLabel(\n remaining: number,\n t: MultiStepTranslations,\n): string {\n return remaining > 0\n ? fill(t.moreToGo || \"__COUNT__ more to go\", { COUNT: remaining })\n : t.complete || \"Complete\";\n}\n\n/** Resolve a plural-form map (Intl.PluralRules categories) for a count. */\nfunction fillPlural(\n map: Record<string, string>,\n count: number,\n locale: string | undefined,\n): string {\n const form = new Intl.PluralRules(locale || \"en\").select(count);\n return fill(map[form] || map.other || \"__COUNT__\", { COUNT: count });\n}\n\n/** The door's main label: invitation before any pick, continuation after. */\nexport function doorLabel(\n anyPicks: boolean,\n t: MultiStepTranslations,\n): string {\n return anyPicks\n ? t.doorContinue || \"Continue building\"\n : t.doorChoose || \"Choose your products\";\n}\n\n/**\n * The door's sublabel: total step count while untouched (\"4 quick steps\"),\n * remaining unsatisfied steps mid-build (\"2 steps to go\").\n */\nexport function doorSubLabel(\n anyPicks: boolean,\n remaining: number,\n total: number,\n t: MultiStepTranslations,\n): string {\n if (!anyPicks) {\n const quick = t.doorQuickSteps;\n if (typeof quick === \"string\") return fill(quick, { COUNT: total });\n return fillPlural(\n quick || { one: \"__COUNT__ quick step\", other: \"__COUNT__ quick steps\" },\n total,\n t.locale,\n );\n }\n return fillPlural(\n t.doorStepsToGo || {\n one: \"__COUNT__ step to go\",\n other: \"__COUNT__ steps to go\",\n },\n remaining,\n t.locale,\n );\n}\n","import {\n ruleFor,\n variantRuleFor,\n type ProductRule,\n type SelectedItem,\n} from \"../picker/rules\";\n\n/**\n * Step-scoped capacity maths for the multi-step wizard.\n *\n * Selections are an array of arrays — one flat selection per step — so the\n * step-agnostic primitives in ../picker/rules apply per step, and everything\n * here is about how the steps relate. Pure: the state comes in as arguments.\n */\nexport type StepSelections = SelectedItem[][];\n\nexport interface WizardStep {\n title?: string;\n /** Units the shopper must pick before the step is satisfied. */\n minQuantity?: number;\n /** Units the step will accept; null or absent means unbounded. */\n maxQuantity?: number | null;\n eligibleProducts?: unknown[];\n}\n\nexport function unitsInStep(selections: StepSelections, i: number): number {\n let qty = 0;\n for (let j = 0; j < selections[i].length; j++) {\n qty += selections[i][j].quantity || 1;\n }\n return qty;\n}\n\nexport function stepSatisfied(\n selections: StepSelections,\n steps: WizardStep[],\n i: number,\n): boolean {\n return unitsInStep(selections, i) >= (steps[i].minQuantity || 1);\n}\n\n/** Open units left in a step; Infinity when the step declares no maximum. */\nexport function stepHeadroom(\n selections: StepSelections,\n steps: WizardStep[],\n i: number,\n): number {\n const max = steps[i].maxQuantity;\n if (max == null) return Infinity;\n return Math.max(0, max - unitsInStep(selections, i));\n}\n\nexport function allSatisfied(\n selections: StepSelections,\n steps: WizardStep[],\n): boolean {\n for (let i = 0; i < steps.length; i++) {\n if (!stepSatisfied(selections, steps, i)) return false;\n }\n return true;\n}\n\n/**\n * Units of a variant across every step.\n *\n * Stock is one pool for the whole bundle, so a variant picked in step 1 and\n * step 3 draws from the same inventory — this is what stops the wizard\n * overselling across steps.\n */\nexport function variantUnitsInBundle(\n selections: StepSelections,\n variantId: number | string,\n): number {\n let qty = 0;\n for (const step of selections) {\n for (const pick of step) {\n if (pick.variantId === variantId) qty += pick.quantity || 0;\n }\n }\n return qty;\n}\n\n/**\n * Units of a product held anywhere except one step's slot for one variant.\n *\n * `rule.max` caps a product bundle-wide, not per step, so a row's ceiling has\n * to account for the same product picked in other steps.\n */\nexport function productUnitsInOtherSlots(\n selections: StepSelections,\n productId: number | string,\n excludeStep: number,\n excludeVariantId: number | string,\n): number {\n let qty = 0;\n for (let i = 0; i < selections.length; i++) {\n for (const it of selections[i]) {\n if (it.productId !== productId) continue;\n if (i === excludeStep && it.variantId === excludeVariantId) continue;\n qty += it.quantity || 0;\n }\n }\n return qty;\n}\n\n/**\n * Whether a pick may be removed.\n *\n * Step-agnostic, like the rules themselves and the discount function's check:\n * removal is allowed only while the product's other slots — in any step — keep\n * it at or above rule.min.\n *\n * A required VARIANT is stricter and needs no tally. Slots are one per\n * (product, variant), so a required variant has no other slot to fall back on\n * and no sibling may stand in for it — that is the whole point of naming it.\n */\nexport function canRemovePick(\n selections: StepSelections,\n item: SelectedItem,\n rulesMap: Record<string, ProductRule | undefined>,\n): boolean {\n if (variantRuleFor(item.variantId, rulesMap)?.required) return false;\n\n const rule = ruleFor(item.productId, rulesMap);\n if (!rule.required) return true;\n\n let unitsElsewhere = 0;\n for (const step of selections) {\n for (const pick of step) {\n if (pick !== item && pick.productId === item.productId) {\n unitsElsewhere += pick.quantity || 1;\n }\n }\n }\n return unitsElsewhere >= rule.min;\n}\n\n/**\n * Units available for an atomic variant swap within one step, or 0.\n *\n * Cross-step displacement is deliberately off the table: taking units from\n * another step to satisfy this one would drain that step below its own\n * minimum.\n */\nexport function swapUnitsForStep(\n selections: StepSelections,\n i: number,\n productId: number | string,\n variantId: number | string,\n rulesMap: Record<string, ProductRule | undefined>,\n): number {\n const rule = ruleFor(productId, rulesMap);\n if (!rule.required || rule.max !== rule.min) return 0;\n if (indexOfVariantInStep(selections, i, variantId) !== -1) return 0;\n\n let qty = 0;\n for (const it of selections[i]) {\n if (it.productId === productId && it.variantId !== variantId) {\n qty += it.quantity || 1;\n }\n }\n return qty;\n}\n\nexport function indexOfVariantInStep(\n selections: StepSelections,\n i: number,\n variantId: number | string,\n): number {\n for (let j = 0; j < selections[i].length; j++) {\n if (selections[i][j].variantId === variantId) return j;\n }\n return -1;\n}\n\nexport function committedQtyForVariantInStep(\n selections: StepSelections,\n i: number,\n variantId: number | string,\n): number {\n const idx = indexOfVariantInStep(selections, i, variantId);\n return idx === -1 ? 0 : selections[i][idx].quantity || 0;\n}\n\n/** Every pick across every step, in step order — the add-to-cart payload. */\nexport function flattenSelections(selections: StepSelections): SelectedItem[] {\n const out: SelectedItem[] = [];\n for (const step of selections) out.push(...step);\n return out;\n}\n","import {\n doorLabel,\n doorSubLabel,\n moreToGoLabel,\n ofSelectedLabel,\n stepsCompletedLabel,\n} from \"./i18n\";\nimport {\n allSatisfied,\n canRemovePick,\n stepSatisfied,\n unitsInStep,\n type StepSelections,\n} from \"./rules\";\nimport { buildSlotCard } from \"../picker/slot-card\";\nimport type { ProductRule } from \"../picker/rules\";\nimport type { MultiStepData, MultiStepStep, MultiStepTranslations } from \"./types\";\n\nexport interface BodyDeps {\n container: HTMLElement;\n steps: MultiStepStep[];\n selections: StepSelections;\n t: MultiStepTranslations;\n rulesMap: Record<string, ProductRule | undefined>;\n formatMoney: (cents: number) => string;\n onRemove: (step: number, index: number) => void;\n /** Reopen the wizard at a given step. */\n onEdit: (step: number) => void;\n}\n\n/**\n * Rebuild each step's mode, count chips, and slot list.\n *\n * A step with picks presents as an open group (heading + chosen cards); a\n * step without presents as a slim checklist row. Both representations are in\n * the host's markup — `data-step-mode` on the section picks which one shows.\n * The count chip exists in each (heading and row), so chips are updated by\n * attribute across the whole section.\n */\nexport function updateStepGroups(deps: BodyDeps): void {\n const { container, steps, selections, t } = deps;\n\n for (let i = 0; i < steps.length; i++) {\n const min = steps[i].minQuantity || 1;\n\n const section = container.querySelector<HTMLElement>(\n '[data-step-group=\"' + i + '\"]',\n );\n if (section) {\n section.setAttribute(\n \"data-step-mode\",\n selections[i].length > 0 ? \"open\" : \"row\",\n );\n }\n\n const countLabel = ofSelectedLabel(\n Math.min(unitsInStep(selections, i), min),\n min,\n t,\n );\n const met = stepSatisfied(selections, steps, i);\n const countEls = container.querySelectorAll<HTMLElement>(\n '[data-step-count=\"' + i + '\"]',\n );\n for (let c = 0; c < countEls.length; c++) {\n countEls[c].textContent = countLabel;\n countEls[c].classList.toggle(\"lb-multi-step__group-count--met\", met);\n }\n\n const slotsWrap = container.querySelector<HTMLElement>(\n '[data-step-slots=\"' + i + '\"]',\n );\n if (!slotsWrap) continue;\n\n slotsWrap.innerHTML = \"\";\n for (let j = 0; j < selections[i].length; j++) {\n const stepIdx = i;\n const slotIdx = j;\n const item = selections[stepIdx][slotIdx];\n slotsWrap.appendChild(\n buildSlotCard(\n item,\n t,\n deps.formatMoney,\n canRemovePick(selections, item, deps.rulesMap),\n () => deps.onRemove(stepIdx, slotIdx),\n () => deps.onEdit(stepIdx),\n ),\n );\n }\n }\n}\n\n/**\n * The single dashed door that opens the wizard.\n *\n * It sits directly above the first unsatisfied step — the thing it acts on —\n * and migrates down as steps complete, then disappears once every step is\n * satisfied (adding beyond a satisfied step's minimum stays available through\n * the step's cards, which reopen the wizard at that step). Its\n * `data-step-door` value is kept at the wizard step it should open, so the\n * host's click handler reads it live instead of re-deriving the target.\n */\nexport function updateDoor(deps: BodyDeps): void {\n const { container, steps, selections, t } = deps;\n const door = container.querySelector<HTMLElement>(\"[data-step-door]\");\n if (!door) return;\n\n let firstUnsatisfied = -1;\n let remaining = 0;\n let anyPicks = false;\n for (let i = 0; i < steps.length; i++) {\n if (selections[i].length > 0) anyPicks = true;\n if (!stepSatisfied(selections, steps, i)) {\n remaining += 1;\n if (firstUnsatisfied === -1) firstUnsatisfied = i;\n }\n }\n\n if (firstUnsatisfied === -1) {\n // Park the hidden door at the top of the container: it stays in the DOM\n // (display:none), and mid-list it would still count as a sibling for the\n // CSS pair-spacing selectors. All-satisfied means every section is open,\n // so nothing ever needs to sit above it.\n door.style.display = \"none\";\n door.classList.add(\"lb-multi-step__door--hidden\");\n const parent = door.parentElement;\n if (parent && parent.firstElementChild !== door) {\n parent.insertBefore(door, parent.firstElementChild);\n }\n return;\n }\n door.style.display = \"\";\n door.classList.remove(\"lb-multi-step__door--hidden\");\n door.setAttribute(\"data-step-door\", String(firstUnsatisfied));\n\n const label = doorLabel(anyPicks, t);\n const labelEl = door.querySelector<HTMLElement>(\n \".lb-mix-match__add-product-label\",\n );\n if (labelEl) labelEl.textContent = label;\n const subEl = door.querySelector<HTMLElement>(\".lb-multi-step__door-sub\");\n if (subEl) {\n subEl.textContent = doorSubLabel(anyPicks, remaining, steps.length, t);\n }\n const stepName = steps[firstUnsatisfied].name || \"\";\n door.setAttribute(\"aria-label\", stepName ? label + \": \" + stepName : label);\n\n const target = container.querySelector<HTMLElement>(\n '[data-step-group=\"' + firstUnsatisfied + '\"]',\n );\n if (target && target.parentElement && target.previousElementSibling !== door) {\n target.parentElement.insertBefore(door, target);\n }\n}\n\nexport function updatePricing(\n deps: BodyDeps,\n data: MultiStepData,\n calcDiscount: (total: number, type: string, value: number) => number,\n paint: (cont: HTMLElement, sale: number, compare: number) => void,\n): void {\n const { container, steps, selections } = deps;\n const pricingSection = container.querySelector<HTMLElement>(\n \"[data-pricing-section]\",\n );\n\n if (!allSatisfied(selections, steps)) {\n if (pricingSection) pricingSection.style.display = \"none\";\n return;\n }\n if (pricingSection) pricingSection.style.display = \"\";\n\n let totalPrice = 0;\n for (const step of selections) {\n for (const pick of step) totalPrice += pick.price * (pick.quantity || 1);\n }\n\n if (totalPrice > 0 && data.discountType) {\n paint(\n container,\n calcDiscount(totalPrice, data.discountType, data.discountValue ?? 0),\n totalPrice,\n );\n }\n}\n\n/** One segment per step, filled as each step's minimum is met. */\nexport function updateProgress(deps: BodyDeps): void {\n const { container, steps, selections, t } = deps;\n\n let completed = 0;\n for (let i = 0; i < steps.length; i++) {\n if (stepSatisfied(selections, steps, i)) completed += 1;\n }\n const remaining = steps.length - completed;\n\n const segments = container.querySelectorAll<HTMLElement>(\n \"[data-progress] [data-progress-segment]\",\n );\n for (let s = 0; s < segments.length; s++) {\n segments[s].classList.toggle(\n \"lb-mix-match__progress-segment--filled\",\n s < completed,\n );\n }\n\n const progressCount = container.querySelector<HTMLElement>(\n \"[data-progress-count]\",\n );\n if (progressCount) {\n progressCount.textContent = stepsCompletedLabel(completed, steps.length, t);\n }\n\n const progressRemaining = container.querySelector<HTMLElement>(\n \"[data-progress-remaining]\",\n );\n if (progressRemaining) {\n progressRemaining.textContent = moreToGoLabel(remaining, t);\n }\n\n const segWrap = container.querySelector<HTMLElement>(\n \".lb-mix-match__progress-segments\",\n );\n if (segWrap) segWrap.setAttribute(\"aria-valuenow\", String(completed));\n}\n\nexport function updateCta(deps: BodyDeps): void {\n const ctaBtn = deps.container.querySelector<HTMLButtonElement>(\n \"[data-add-bundle]\",\n );\n if (!ctaBtn) return;\n\n ctaBtn.disabled = !allSatisfied(deps.selections, deps.steps);\n // Written to the label child so the sibling spinner survives.\n const ctaLabel = ctaBtn.querySelector<HTMLElement>(\"[data-cta-label]\");\n if (ctaLabel) {\n ctaLabel.textContent =\n ctaBtn.getAttribute(\"data-cta-text\") || deps.t.addToCart || \"Add to cart\";\n }\n}\n","import type { StepSelections } from \"./rules\";\nimport type { CartItem } from \"../host\";\n\n/**\n * Flatten the wizard's per-step selections into cart lines.\n *\n * Merged by variant: the discount function sees cart lines, not steps, so the\n * same variant picked in two steps has to arrive as one line of the combined\n * quantity. Unmerged, the shopper would get duplicate lines and the pool maths\n * would count them separately.\n *\n * First-seen order is preserved so the cart reads in wizard order.\n */\nexport function mergeCartLines(selections: StepSelections): CartItem[] {\n const byVariant: Record<string, CartItem> = Object.create(null);\n const order: string[] = [];\n\n for (const step of selections) {\n for (const item of step) {\n const qty = item.quantity || 1;\n const key = String(item.variantId);\n if (byVariant[key]) {\n byVariant[key].quantity += qty;\n byVariant[key].priceCents =\n (byVariant[key].priceCents ?? 0) + (item.price || 0) * qty;\n } else {\n byVariant[key] = {\n variantId: item.variantId,\n quantity: qty,\n priceCents: (item.price || 0) * qty,\n };\n order.push(key);\n }\n }\n }\n\n return order.map((key) => byVariant[key]);\n}\n","import { ofSelectedLabel, stepOfLabel } from \"./i18n\";\nimport {\n indexOfVariantInStep,\n stepHeadroom,\n stepSatisfied,\n swapUnitsForStep,\n unitsInStep,\n type StepSelections,\n} from \"./rules\";\nimport { fill } from \"../picker/i18n\";\nimport { getFocusable } from \"../picker/focus-trap\";\nimport { lock, unlock } from \"../picker/scroll-lock\";\nimport { sinkAddedRows } from \"../picker/sort\";\nimport { buildPickerRow, type PickerRow, type RowCapacity } from \"../picker/row\";\nimport { defaultPickQuantity, type ProductRule } from \"../picker/rules\";\nimport { debounce, normalizeText } from \"../utils\";\nimport type { DropdownApi } from \"../host\";\nimport type {\n EligibleProduct,\n MultiStepStep,\n MultiStepTranslations,\n SelectedItem,\n} from \"./types\";\n\nconst ALL_TYPES = \"__all__\";\nconst CLOSE_TRANSITION_MS = 300;\nconst SEARCH_DEBOUNCE_MS = 200;\n/**\n * How long the step-change cascade runs, plus a small margin. Kept in step\n * with the animation delays in `bundle-multi-step.css`, whose last card\n * lands at 0.42s.\n */\nconst STEP_CASCADE_MS = 500;\n/**\n * Auto-advance waits this long before moving on. Without it the step change\n * starts in the same frame as the shopper's click, so the Add button never\n * renders its \"Added\" state before the list is torn down and the advance\n * reads as a glitch rather than as a consequence of what they just did.\n */\nconst AUTO_ADVANCE_HOLD_MS = 160;\n\nexport interface WizardDeps {\n container: HTMLElement;\n steps: MultiStepStep[];\n selections: StepSelections;\n t: MultiStepTranslations;\n rulesMap: Record<string, ProductRule | undefined>;\n showQtySelector: boolean;\n showTypeFilters: boolean;\n /** Move to the next step automatically once picks reach the step's\n * maximum. Fires only on a commit that raises the unit count, never on\n * the last step; steps without a maximum are unaffected. */\n autoAdvance: boolean;\n formatMoney: (cents: number) => string;\n dropdown: DropdownApi | undefined;\n /** Where to portal the overlay, or null to leave it in place. */\n portalTarget: HTMLElement | null;\n /** Focus target when the wizard closes and the step's trigger is hidden. */\n ctaButton: HTMLElement;\n /** Repaint the widget body after any selection change. */\n onSelectionChanged: () => void;\n /** Displace in-step sibling units with this pick (atomic variant swap). */\n swapVariant: (step: number, item: SelectedItem, qty: number) => void;\n canRemove: (item: SelectedItem) => boolean;\n}\n\nexport interface Wizard {\n open: (atStep?: number) => void;\n close: () => void;\n isOpen: () => boolean;\n refresh: () => void;\n}\n\nexport function createWizard(deps: WizardDeps): Wizard {\n const { container, steps, selections, t } = deps;\n\n const overlay = container.querySelector<HTMLElement>(\"[data-modal-overlay]\");\n // See the note in mix-match/modal.ts: the theme host portals this to <body>\n // to escape a transformed ancestor; a shadow root neither needs that nor\n // survives it.\n if (overlay && deps.portalTarget) deps.portalTarget.appendChild(overlay);\n\n const q = <T extends HTMLElement>(sel: string): T | null =>\n overlay ? overlay.querySelector<T>(sel) : null;\n\n const modalDialog = q<HTMLElement>('[role=\"dialog\"]');\n const modalTitle = q<HTMLElement>(\".lb-mix-match__modal-title\");\n const searchInput = q<HTMLInputElement>(\"[data-modal-search]\");\n const searchClear = q<HTMLElement>(\"[data-modal-search-clear]\");\n const modalList = q<HTMLElement>(\"[data-modal-list]\");\n const modalEmpty = q<HTMLElement>(\"[data-modal-empty]\");\n const modalLive = q<HTMLElement>(\"[data-modal-live]\");\n const closeBtn = q<HTMLElement>(\"[data-modal-close]\");\n const backBtn = q<HTMLButtonElement>(\"[data-modal-back]\");\n const nextBtn = q<HTMLButtonElement>(\"[data-modal-next]\");\n const doneBtn = q<HTMLButtonElement>(\"[data-modal-done]\");\n const footerCountEl = q<HTMLElement>(\"[data-modal-footer-count]\");\n const subtitleEl = q<HTMLElement>(\"[data-modal-subtitle]\");\n const segmentsEl = q<HTMLElement>(\"[data-modal-step-segments]\");\n const filtersContainer = q<HTMLElement>(\"[data-modal-filters]\");\n\n let activeType = ALL_TYPES;\n let modalOpen = false;\n /** The step whose rows the list currently holds. */\n let stepIndex = 0;\n let rows: PickerRow[] = [];\n /** Pending clear of `data-step-transition` once the cascade has played. */\n let cascadeTimer: ReturnType<typeof setTimeout> | null = null;\n /** Pending auto-advance, held briefly so the shopper's pick lands first. */\n let autoAdvanceTimer: ReturnType<typeof setTimeout> | null = null;\n\n const productsFor = (i: number): EligibleProduct[] =>\n steps[i].eligibleProducts || [];\n\n /**\n * Capacity scoped to the step being displayed.\n *\n * Reads `stepIndex` at call time rather than closing over a value, so one\n * adapter serves every step the wizard walks through.\n */\n const rowCapacity: RowCapacity = {\n remainingSpots: () => stepHeadroom(selections, steps, stepIndex),\n ownQty: (variantId) => {\n const idx = indexOfVariantInStep(selections, stepIndex, variantId);\n return idx === -1 ? 0 : selections[stepIndex][idx].quantity || 0;\n },\n // Stock is one pool for the whole bundle, so units in other steps count.\n variantUnitsElsewhere: (variantId) => {\n let qty = 0;\n for (const step of selections) {\n for (const pick of step) {\n if (pick.variantId === variantId) qty += pick.quantity || 0;\n }\n }\n return qty;\n },\n productUnitsElsewhere: (productId, variantId) => {\n let qty = 0;\n for (let i = 0; i < selections.length; i++) {\n for (const it of selections[i]) {\n if (it.productId !== productId) continue;\n if (i === stepIndex && it.variantId === variantId) continue;\n qty += it.quantity || 0;\n }\n }\n return qty;\n },\n swapUnits: (productId, variantId) =>\n swapUnitsForStep(selections, stepIndex, productId, variantId, deps.rulesMap),\n isInBundle: (variantId) =>\n indexOfVariantInStep(selections, stepIndex, variantId) !== -1,\n canRemove: (variantId) => {\n const idx = indexOfVariantInStep(selections, stepIndex, variantId);\n return idx !== -1 && deps.canRemove(selections[stepIndex][idx]);\n },\n isFull: () => stepHeadroom(selections, steps, stepIndex) === 0,\n commitQty: (variantId, qty) => {\n const idx = indexOfVariantInStep(selections, stepIndex, variantId);\n if (idx === -1 || selections[stepIndex][idx].quantity === qty) return false;\n selections[stepIndex][idx].quantity = qty;\n return true;\n },\n };\n\n /* ── Step progress segments ────────────────────────────────── */\n\n /** One segment per required unit of the step, filled from its unit count. */\n function buildStepSegments(i: number): void {\n if (!segmentsEl) return;\n const min = steps[i].minQuantity || 1;\n segmentsEl.innerHTML = \"\";\n segmentsEl.setAttribute(\"aria-valuemax\", String(min));\n for (let s = 0; s < min; s++) {\n const seg = document.createElement(\"span\");\n seg.className = \"lb-mix-match__progress-segment\";\n seg.setAttribute(\"data-progress-segment\", \"\");\n segmentsEl.appendChild(seg);\n }\n paintStepSegments();\n }\n\n function paintStepSegments(): void {\n if (!segmentsEl) return;\n const min = steps[stepIndex].minQuantity || 1;\n const count = Math.min(unitsInStep(selections, stepIndex), min);\n const segs = segmentsEl.querySelectorAll<HTMLElement>(\n \"[data-progress-segment]\",\n );\n for (let s = 0; s < segs.length; s++) {\n segs[s].classList.toggle(\n \"lb-mix-match__progress-segment--filled\",\n s < count,\n );\n }\n segmentsEl.setAttribute(\"aria-valuenow\", String(count));\n }\n\n /* ── Footer and nav ────────────────────────────────────────── */\n\n function updateModalMeta(): void {\n const min = steps[stepIndex].minQuantity || 1;\n const count = Math.min(unitsInStep(selections, stepIndex), min);\n\n if (footerCountEl) {\n footerCountEl.textContent = ofSelectedLabel(count, min, t);\n }\n if (subtitleEl) {\n subtitleEl.textContent =\n stepOfLabel(stepIndex, steps.length, t) +\n \" · \" +\n (steps[stepIndex].name || \"\");\n }\n\n const satisfied = stepSatisfied(selections, steps, stepIndex);\n const isLast = stepIndex === steps.length - 1;\n\n if (backBtn) backBtn.style.display = stepIndex > 0 ? \"\" : \"none\";\n if (nextBtn) {\n nextBtn.style.display = isLast ? \"none\" : \"\";\n // Can't advance until this step's minimum is met.\n nextBtn.disabled = !satisfied;\n }\n if (doneBtn) {\n doneBtn.style.display = isLast ? \"\" : \"none\";\n doneBtn.disabled = !satisfied;\n }\n paintStepSegments();\n }\n\n function refresh(): void {\n for (const r of rows) r.refresh();\n updateModalMeta();\n }\n\n /**\n * A step filled to its maximum has nothing left to do, so with the\n * merchant setting on the wizard moves on by itself. Called only from the\n * two commit paths that can raise the step's unit count (Add and the\n * committed-row stepper) — a swap keeps the total fixed and opening at an\n * already-full step is the shopper revisiting, not filling.\n *\n * The move is held for AUTO_ADVANCE_HOLD_MS so the pick the shopper just\n * made renders before its step is replaced. Any step change during the\n * hold cancels it (`goToStep` clears the timer) and so does closing the\n * wizard: navigation the shopper asked for outranks navigation we were\n * about to do for them.\n */\n function maybeAutoAdvance(): void {\n if (!deps.autoAdvance) return;\n // The last step never auto-advances; the shopper reviews and closes.\n if (stepIndex >= steps.length - 1) return;\n // A step without a maximum has Infinity headroom and never hits 0.\n if (stepHeadroom(selections, steps, stepIndex) !== 0) return;\n\n const from = stepIndex;\n if (autoAdvanceTimer !== null) clearTimeout(autoAdvanceTimer);\n autoAdvanceTimer = setTimeout(() => {\n autoAdvanceTimer = null;\n if (!modalOpen) return;\n goToStep(from + 1);\n }, AUTO_ADVANCE_HOLD_MS);\n }\n\n /* ── Product list ──────────────────────────────────────────── */\n\n function buildProductList(forStep: number): void {\n if (!modalList) return;\n\n deps.dropdown?.unbindAll(modalList);\n modalList.innerHTML = \"\";\n rows = [];\n\n const pool = productsFor(forStep);\n for (let i = 0; i < pool.length; i++) {\n const row = buildPickerRow(pool[i], i, {\n t,\n rulesMap: deps.rulesMap,\n capacity: rowCapacity,\n showQtySelector: deps.showQtySelector,\n formatMoney: deps.formatMoney,\n onCommittedQtyChanged: () => {\n deps.onSelectionChanged();\n refresh();\n maybeAutoAdvance();\n },\n });\n rows.push(row);\n modalList.appendChild(row.el);\n }\n\n // Products already picked in this step sink below the ones still to\n // pick. The list is only rebuilt on open and step change, so the order\n // holds still while the shopper works through the step.\n const stepPicks = selections[forStep] || [];\n const selectedProductIds: Record<string, true> = Object.create(null);\n for (const it of stepPicks) selectedProductIds[String(it.productId)] = true;\n sinkAddedRows(\n modalList,\n rows.map((r) => r.el),\n (i) => selectedProductIds[String(pool[i].id)] === true,\n );\n\n deps.dropdown?.bindAll(modalList);\n }\n\n function onListClick(e: MouseEvent): void {\n const addBtn = (e.target as HTMLElement).closest<HTMLButtonElement>(\n \"[data-add-product]\",\n );\n if (!addBtn || addBtn.disabled) return;\n\n const pool = productsFor(stepIndex);\n const prod = pool[parseInt(addBtn.getAttribute(\"data-add-product\") ?? \"\", 10)];\n if (!prod) return;\n\n const row = addBtn.closest<HTMLElement>(\".lb-mix-match__modal-product\");\n const selectedId = row && row.getAttribute(\"data-selected-variant-id\");\n let selectedVariant =\n (selectedId\n ? prod.variants.find((v) => v.id === parseInt(selectedId, 10))\n : null) ?? null;\n if (!selectedVariant) {\n selectedVariant = prod.variants.find((v) => v.available) ?? null;\n }\n if (!selectedVariant) return;\n\n // Toggle off, scoped to this step: the same variant in another step keeps\n // its slot.\n const existing = indexOfVariantInStep(\n selections,\n stepIndex,\n selectedVariant.id,\n );\n if (existing !== -1) {\n if (!deps.canRemove(selections[stepIndex][existing])) return;\n selections[stepIndex].splice(existing, 1);\n deps.onSelectionChanged();\n refresh();\n return;\n }\n\n const rowQtyEl = row?.querySelector<HTMLElement>(\"[data-row-qty]\");\n const pickedQty = rowQtyEl\n ? Math.max(\n 1,\n parseInt(\n rowQtyEl.getAttribute(\"data-qty\") || rowQtyEl.textContent || \"\",\n 10,\n ) || 1,\n )\n : defaultPickQuantity(prod.id, deps.rulesMap, selectedVariant.id);\n\n const newItem: SelectedItem = {\n productId: prod.id,\n variantId: selectedVariant.id,\n title: prod.title,\n url: prod.url || null,\n variantTitle: selectedVariant.title,\n featuredImage: selectedVariant.image || prod.featuredImage || null,\n price: selectedVariant.price,\n compareAtPrice: selectedVariant.compareAtPrice || null,\n unitPrice: selectedVariant.unitPrice || null,\n quantity: pickedQty,\n };\n\n // A swap keeps the step total fixed, so it runs instead of the headroom\n // guard rather than after it.\n const swapUnits = swapUnitsForStep(\n selections,\n stepIndex,\n prod.id,\n selectedVariant.id,\n deps.rulesMap,\n );\n if (swapUnits > 0) {\n deps.swapVariant(stepIndex, newItem, Math.min(pickedQty, swapUnits));\n refresh();\n return;\n }\n\n const room = stepHeadroom(selections, steps, stepIndex);\n if (room !== Infinity && pickedQty > room) return;\n\n selections[stepIndex].push(newItem);\n deps.onSelectionChanged();\n refresh();\n maybeAutoAdvance();\n }\n\n if (modalList) modalList.addEventListener(\"click\", onListClick);\n\n /* ── Filters and search ────────────────────────────────────── */\n\n function buildFilters(forStep: number): void {\n if (!filtersContainer || !deps.showTypeFilters) return;\n\n const types: string[] = [];\n const seen: Record<string, boolean> = Object.create(null);\n for (const p of productsFor(forStep)) {\n const ty = (p.type || \"\").trim();\n if (ty && !seen[ty]) {\n seen[ty] = true;\n types.push(ty);\n }\n }\n\n if (types.length < 2) {\n filtersContainer.style.display = \"none\";\n modalDialog?.classList.add(\"lb-mix-match__modal--filters-hidden\");\n return;\n }\n // Re-shown explicitly: a previous step may have hidden the row.\n filtersContainer.style.display = \"\";\n modalDialog?.classList.remove(\"lb-mix-match__modal--filters-hidden\");\n\n filtersContainer.innerHTML = \"\";\n for (const value of [ALL_TYPES, ...types]) {\n const pill = document.createElement(\"button\");\n pill.type = \"button\";\n pill.className = \"lb-mix-match__filter\";\n pill.setAttribute(\"data-filter\", value);\n pill.setAttribute(\"aria-pressed\", value === activeType ? \"true\" : \"false\");\n if (value === activeType) pill.classList.add(\"lb-mix-match__filter--active\");\n pill.textContent = value === ALL_TYPES ? t.allTypes || \"All\" : value;\n pill.addEventListener(\"click\", () => {\n activeType = value;\n syncActiveFilterPill();\n filterProducts(searchInput ? searchInput.value : \"\");\n });\n filtersContainer.appendChild(pill);\n }\n }\n\n function syncActiveFilterPill(): void {\n if (!filtersContainer) return;\n const pills = filtersContainer.querySelectorAll<HTMLElement>(\"[data-filter]\");\n for (let i = 0; i < pills.length; i++) {\n const on = pills[i].getAttribute(\"data-filter\") === activeType;\n pills[i].classList.toggle(\"lb-mix-match__filter--active\", on);\n pills[i].setAttribute(\"aria-pressed\", on ? \"true\" : \"false\");\n }\n }\n\n function filterProducts(query: string): void {\n if (!modalList) return;\n const normalizedQuery = normalizeText(query);\n const listItems = modalList.querySelectorAll<HTMLElement>(\n \"[data-product-item]\",\n );\n let visibleCount = 0;\n\n for (let i = 0; i < listItems.length; i++) {\n const title = listItems[i].getAttribute(\"data-title\") ?? \"\";\n const type = listItems[i].getAttribute(\"data-type\") || \"\";\n const matches =\n (!normalizedQuery || title.indexOf(normalizedQuery) !== -1) &&\n (activeType === ALL_TYPES || type === activeType);\n listItems[i].classList.toggle(\"lb-hidden\", !matches);\n if (matches) visibleCount++;\n }\n\n if (modalEmpty) {\n modalEmpty.style.display =\n visibleCount === 0 && normalizedQuery.length > 0 ? \"\" : \"none\";\n }\n if (searchClear) {\n searchClear.style.display = query.length > 0 ? \"\" : \"none\";\n }\n if (modalLive) {\n modalLive.textContent = fill(\n t.nProductsShown || \"__COUNT__ products shown\",\n { COUNT: visibleCount },\n );\n }\n }\n\n if (searchInput) {\n const handleSearch = debounce(\n () => filterProducts(searchInput.value),\n SEARCH_DEBOUNCE_MS,\n );\n searchInput.addEventListener(\"input\", handleSearch);\n }\n if (searchClear) {\n searchClear.addEventListener(\"click\", () => {\n if (!searchInput) return;\n searchInput.value = \"\";\n filterProducts(\"\");\n searchInput.focus();\n });\n }\n\n /* ── Navigation ────────────────────────────────────────────── */\n\n /**\n * Move to a step: rebuild its product list, filters and segments, reset the\n * search, and announce the change.\n *\n * Every step change (manual Next/Back and auto-advance) stamps\n * `data-step-transition=\"forward\" | \"back\"` on the dialog so the CSS can\n * cascade the incoming step's content in — without it the whole region\n * swaps in one paint and the shopper can't tell they've moved.\n * `animate: false` (the open path) removes the attribute instead: the\n * modal entrance carries that moment's motion, and a leftover value would\n * replay the cascade every time `display: none` is lifted.\n *\n * The attribute is cleared again once the cascade has played. That is not\n * tidiness: the cascade animates the product cards individually, and\n * search/filter hide cards with `.lb-hidden` (`display: none`), so\n * restoring one restarts its animation. A live attribute would replay the\n * lift on every keystroke that reveals a card.\n */\n function goToStep(\n i: number,\n opts: { skipFocus?: boolean; animate?: boolean } = {},\n ): void {\n const target = Math.max(0, Math.min(steps.length - 1, i));\n // Any step change supersedes a held auto-advance, including the one\n // firing right now (which has already cleared its own handle).\n if (autoAdvanceTimer !== null) {\n clearTimeout(autoAdvanceTimer);\n autoAdvanceTimer = null;\n }\n if (modalDialog) {\n // Cleared first with a reflow between: re-stamping the same value\n // would leave a mid-flight animation running instead of restarting it.\n if (cascadeTimer !== null) clearTimeout(cascadeTimer);\n cascadeTimer = null;\n modalDialog.removeAttribute(\"data-step-transition\");\n if (opts.animate !== false && target !== stepIndex) {\n void modalDialog.offsetHeight;\n modalDialog.setAttribute(\n \"data-step-transition\",\n target > stepIndex ? \"forward\" : \"back\",\n );\n const dialog = modalDialog;\n cascadeTimer = setTimeout(() => {\n cascadeTimer = null;\n dialog.removeAttribute(\"data-step-transition\");\n }, STEP_CASCADE_MS);\n }\n }\n stepIndex = target;\n\n buildProductList(stepIndex);\n buildFilters(stepIndex);\n activeType = ALL_TYPES;\n syncActiveFilterPill();\n if (searchInput) searchInput.value = \"\";\n filterProducts(\"\");\n\n // A step always presents from the top of its list — arriving mid-scroll\n // from the previous step hides the new step's first products.\n if (modalList) modalList.scrollTop = 0;\n\n buildStepSegments(stepIndex);\n refresh();\n\n if (modalLive) {\n const min = steps[stepIndex].minQuantity || 1;\n modalLive.textContent =\n stepOfLabel(stepIndex, steps.length, t) +\n \". \" +\n (steps[stepIndex].name || \"\") +\n \". \" +\n ofSelectedLabel(\n Math.min(unitsInStep(selections, stepIndex), min),\n min,\n t,\n );\n }\n if (!opts.skipFocus && modalTitle && typeof modalTitle.focus === \"function\") {\n modalTitle.focus();\n }\n }\n\n function open(atStep?: number): void {\n if (!overlay || modalOpen) return;\n modalOpen = true;\n // Focus goes to the close icon below, so the step heading doesn't take\n // it on the initial open.\n goToStep(typeof atStep === \"number\" ? atStep : 0, {\n skipFocus: true,\n animate: false,\n });\n\n overlay.style.display = \"\";\n void overlay.offsetHeight;\n overlay.classList.add(\"lb-mix-match__modal-overlay--open\");\n\n lock();\n\n // The close icon, not the search input: focusing search on a phone pops\n // the keyboard over half the modal before the shopper asked for it.\n setTimeout(() => {\n if (closeBtn) closeBtn.focus();\n else modalDialog?.focus();\n }, 50);\n }\n\n function close(): void {\n if (!overlay || !modalOpen) return;\n modalOpen = false;\n\n // A held auto-advance must not fire into a closed wizard, and the\n // cascade clear is redundant once the next open resets the attribute.\n if (autoAdvanceTimer !== null) {\n clearTimeout(autoAdvanceTimer);\n autoAdvanceTimer = null;\n }\n if (cascadeTimer !== null) {\n clearTimeout(cascadeTimer);\n cascadeTimer = null;\n }\n\n overlay.classList.remove(\"lb-mix-match__modal-overlay--open\");\n unlock();\n\n setTimeout(() => {\n if (!modalOpen) overlay.style.display = \"none\";\n }, CLOSE_TRANSITION_MS);\n\n // Back to the current step's checklist row; a step with picks has no\n // row, so fall through to the door, and to the CTA once every step is\n // satisfied and the door has retired too.\n const visible = (el: HTMLElement | null): HTMLElement | null =>\n el && el.offsetParent !== null ? el : null;\n const elToFocus =\n visible(\n container.querySelector<HTMLElement>(\n '[data-step-row=\"' + stepIndex + '\"]',\n ),\n ) ||\n visible(container.querySelector<HTMLElement>(\"[data-step-door]\")) ||\n deps.ctaButton;\n if (elToFocus && typeof elToFocus.focus === \"function\") {\n setTimeout(() => elToFocus.focus(), 0);\n }\n }\n\n closeBtn?.addEventListener(\"click\", close);\n doneBtn?.addEventListener(\"click\", close);\n backBtn?.addEventListener(\"click\", () => {\n if (stepIndex > 0) goToStep(stepIndex - 1);\n });\n nextBtn?.addEventListener(\"click\", () => {\n if (\n stepIndex < steps.length - 1 &&\n stepSatisfied(selections, steps, stepIndex)\n ) {\n goToStep(stepIndex + 1);\n }\n });\n\n if (overlay) {\n let mouseDownTarget: EventTarget | null = null;\n overlay.addEventListener(\"mousedown\", (e) => {\n mouseDownTarget = e.target;\n });\n overlay.addEventListener(\"mouseup\", (e) => {\n if (e.target === overlay && mouseDownTarget === overlay) close();\n mouseDownTarget = null;\n });\n }\n\n document.addEventListener(\"keydown\", (e) => {\n if (!modalOpen) return;\n\n if (e.key === \"Escape\") {\n e.preventDefault();\n close();\n return;\n }\n\n if (e.key === \"Tab\" && modalDialog) {\n const focusable = getFocusable(modalDialog);\n if (focusable.length === 0) {\n e.preventDefault();\n return;\n }\n const first = focusable[0];\n const last = focusable[focusable.length - 1];\n if (e.shiftKey && document.activeElement === first) {\n e.preventDefault();\n last.focus();\n } else if (!e.shiftKey && document.activeElement === last) {\n e.preventDefault();\n first.focus();\n }\n }\n });\n\n return { open, close, isOpen: () => modalOpen, refresh };\n}\n","import {\n calculateDiscount,\n productsForStep,\n type CartLineInput,\n type MultiStepBundleData,\n} from \"@lime-bundles/core\";\nimport { adaptProduct } from \"@lime-bundles/render/adapt\";\nimport { bundleLineAttributes, intlFormatMoney } from \"@lime-bundles/render/host\";\nimport {\n updateCta,\n updateDoor,\n updatePricing,\n updateProgress,\n updateStepGroups,\n type BodyDeps,\n} from \"@lime-bundles/render/multi-step/body\";\nimport { mergeCartLines } from \"@lime-bundles/render/multi-step/cart\";\nimport {\n allSatisfied,\n canRemovePick,\n type StepSelections,\n} from \"@lime-bundles/render/multi-step/rules\";\nimport type { EligibleProduct } from \"@lime-bundles/render/multi-step/types\";\nimport { createWizard } from \"@lime-bundles/render/multi-step/wizard\";\nimport { mergeRuleMaps } from \"@lime-bundles/render/picker/rules\";\nimport { buildPickerModal } from \"@lime-bundles/render/picker-modal\";\nimport { buildProgressBar, buildWidgetShell } from \"@lime-bundles/render/skeleton\";\nimport { initCountdown } from \"@lime-bundles/render/countdown\";\nimport { DEFAULT_STRINGS } from \"@lime-bundles/render/strings\";\nimport { updatePricing as paintPricing } from \"./pricing-paint\";\nimport { bindDropdowns } from \"./dropdown-host\";\n\n/**\n * Multi-step wizard, headless.\n *\n * Like mix & match, the wizard itself was already client-rendered on both\n * hosts. What Liquid provides in a theme — the widget shell, the per-step\n * groups with their triggers, and the modal — is built here.\n */\nfunction buildPlusIcon(): SVGSVGElement {\n const NS = \"http://www.w3.org/2000/svg\";\n const svg = document.createElementNS(NS, \"svg\");\n svg.setAttribute(\"width\", \"18\");\n svg.setAttribute(\"height\", \"18\");\n svg.setAttribute(\"viewBox\", \"0 0 18 18\");\n svg.setAttribute(\"fill\", \"none\");\n for (const [x1, y1, x2, y2] of [\n [\"9\", \"3\", \"9\", \"15\"],\n [\"3\", \"9\", \"15\", \"9\"],\n ]) {\n const line = document.createElementNS(NS, \"line\");\n line.setAttribute(\"x1\", x1);\n line.setAttribute(\"y1\", y1);\n line.setAttribute(\"x2\", x2);\n line.setAttribute(\"y2\", y2);\n line.setAttribute(\"stroke\", \"currentColor\");\n line.setAttribute(\"stroke-width\", \"2\");\n line.setAttribute(\"stroke-linecap\", \"round\");\n svg.appendChild(line);\n }\n return svg;\n}\n\nexport function renderMultiStepBundle(\n container: HTMLElement,\n bundle: MultiStepBundleData,\n onAddToCart: (lines: CartLineInput[]) => void,\n onCleanup?: (fn: () => void) => void,\n): void {\n const wc = bundle.widgetConfig;\n const t = DEFAULT_STRINGS;\n const rulesMap = mergeRuleMaps(bundle.productRules, bundle.variantRules);\n const steps = bundle.steps ?? [];\n if (!steps.length) return;\n\n const quantities = {\n productQuantities: bundle.productQuantities,\n variantQuantities: bundle.variantQuantities,\n };\n\n /** Each step's pool, in the shape the picker row expects. */\n const pools: EligibleProduct[][] = steps.map((_, i) =>\n productsForStep(bundle, i).map((product) => {\n const p = adaptProduct(product, bundle.id, {\n quantities,\n allowedVariantIds: null,\n });\n return {\n id: Number(p.productId),\n title: p.title ?? \"\",\n url: p.url ?? null,\n type: product.productType ?? \"\",\n featuredImage: p.featuredImage ?? null,\n available: p.variants.some((v) => v.available),\n optionNames: p.optionNames,\n variants: p.variants.map((v) => ({\n id: Number(v.id),\n title: v.title,\n options: v.options,\n available: v.available,\n price: v.price,\n compareAtPrice: v.compareAtPrice,\n unitPrice: v.unitPrice,\n image: v.image,\n inventoryQuantity: v.inventoryQuantity ?? null,\n })),\n };\n }),\n );\n\n // Every step must be completable or the bundle can't be finished: hide,\n // matching the theme's per-step gate and React's stepHasInStockProduct.\n // Rendering on \"any one step has stock\" walked shoppers into a wizard\n // they could never complete.\n const everyStepPickable =\n pools.length > 0 && pools.every((pool) => pool.some((p) => p.available));\n if (!everyStepPickable) return;\n\n const formatMoney = intlFormatMoney(\n bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? \"USD\",\n );\n\n const shell = buildWidgetShell(\"lb-multi-step\", {\n title: bundle.title,\n subtitle: bundle.description,\n showSavingsBar: wc.savingsBar?.visible !== false,\n showComparePrice: wc.pricing?.showCompareAtPrice !== false,\n ctaText: wc.cta?.ctaText || t.addToCart || \"Add to cart\",\n endsAt: wc.countdown?.showCountdown === false ? null : bundle.endsAt,\n });\n\n // Overall progress counts steps, not units.\n shell.products.before(buildProgressBar(steps.length));\n\n // One dashed door plus one section per step. A step without picks shows a\n // slim checklist row, one with picks shows the heading + chosen cards —\n // data-step-mode picks the representation and updateDoor keeps the door\n // directly above the first unsatisfied step (both hooks the Liquid also\n // emits; tests and any merchant CSS target them).\n const groups = document.createElement(\"div\");\n groups.className = \"lb-multi-step__groups\";\n groups.setAttribute(\"data-step-groups\", \"\");\n\n const door = document.createElement(\"button\");\n door.type = \"button\";\n door.className = \"lb-mix-match__add-product lb-multi-step__door\";\n door.setAttribute(\"data-step-door\", \"0\");\n const doorIcon = document.createElement(\"span\");\n doorIcon.className = \"lb-mix-match__add-product-icon\";\n doorIcon.setAttribute(\"aria-hidden\", \"true\");\n doorIcon.appendChild(buildPlusIcon());\n door.appendChild(doorIcon);\n const doorCopy = document.createElement(\"span\");\n doorCopy.className = \"lb-multi-step__door-copy\";\n const doorLabel = document.createElement(\"span\");\n doorLabel.className = \"lb-mix-match__add-product-label\";\n doorCopy.appendChild(doorLabel);\n const doorSub = document.createElement(\"span\");\n doorSub.className = \"lb-multi-step__door-sub\";\n doorCopy.appendChild(doorSub);\n door.appendChild(doorCopy);\n groups.appendChild(door);\n\n steps.forEach((step, i) => {\n const group = document.createElement(\"section\");\n group.className = \"lb-multi-step__group\";\n group.setAttribute(\"data-step-group\", String(i));\n group.setAttribute(\"data-step-mode\", \"row\");\n\n const buildCount = (): HTMLElement => {\n const count = document.createElement(\"span\");\n count.className = \"lb-multi-step__group-count\";\n count.setAttribute(\"data-step-count\", String(i));\n return count;\n };\n\n const row = document.createElement(\"button\");\n row.type = \"button\";\n row.className = \"lb-multi-step__step-row\";\n row.setAttribute(\"data-step-row\", String(i));\n row.setAttribute(\"aria-label\", \"Choose products: \" + step.name);\n const ix = document.createElement(\"span\");\n ix.className = \"lb-multi-step__step-ix\";\n ix.setAttribute(\"aria-hidden\", \"true\");\n ix.textContent = String(i + 1);\n row.appendChild(ix);\n const rowName = document.createElement(\"span\");\n rowName.className = \"lb-multi-step__group-name\";\n rowName.textContent = step.name;\n row.appendChild(rowName);\n row.appendChild(buildCount());\n group.appendChild(row);\n\n const heading = document.createElement(\"div\");\n heading.className = \"lb-multi-step__group-heading\";\n const name = document.createElement(\"span\");\n name.className = \"lb-multi-step__group-name\";\n name.textContent = step.name;\n heading.appendChild(name);\n heading.appendChild(buildCount());\n group.appendChild(heading);\n\n const slots = document.createElement(\"div\");\n slots.className = \"lb-mix-match__slots lb-edge-fade lb-multi-step__group-slots\";\n slots.setAttribute(\"data-step-slots\", String(i));\n group.appendChild(slots);\n\n groups.appendChild(group);\n });\n shell.products.replaceWith(groups);\n\n // Same merchant toggles the Liquid host reads — multi-step shares the\n // mix-and-match picker keys (lb-multi-step.liquid takes the identical\n // show_search / show_type_filters / show_quantity_selector params).\n const overlay = buildPickerModal({\n bundleGid: bundle.id,\n domId: bundle.id.replace(/\\D/g, \"\"),\n showSearch: wc.showSearch !== false,\n showTypeFilters: wc.mixMatchShowTypeFilters !== false,\n segmentCount: null,\n wizard: true,\n });\n shell.root.appendChild(overlay);\n\n const selections: StepSelections = steps.map(() => []);\n\n const bodyDeps: BodyDeps = {\n container: shell.root,\n steps: steps.map((s, i) => ({\n name: s.name,\n minQuantity: s.minQuantity,\n maxQuantity: s.maxQuantity,\n eligibleProducts: pools[i],\n })),\n selections,\n t,\n rulesMap,\n formatMoney,\n onRemove: (step, index) => {\n const item = selections[step][index];\n if (!item || !canRemovePick(selections, item, rulesMap)) return;\n selections[step].splice(index, 1);\n updateAll();\n if (wizard.isOpen()) wizard.refresh();\n },\n onEdit: (step) => wizard.open(step),\n };\n\n function updateAll(): void {\n updateStepGroups(bodyDeps);\n updateDoor(bodyDeps);\n updatePricing(\n bodyDeps,\n {\n discountType: bundle.discountConfig.discountType,\n discountValue: bundle.discountConfig.discountValue,\n },\n (total, type, value) =>\n calculateDiscount(total, type as \"percentage\" | \"fixed_amount\", value),\n paintPricing(formatMoney),\n );\n updateProgress(bodyDeps);\n updateCta(bodyDeps);\n }\n\n const wizard = createWizard({\n container: shell.root,\n steps: bodyDeps.steps,\n selections,\n t,\n rulesMap,\n showQtySelector: wc.mixMatchShowQuantitySelector !== false,\n showTypeFilters: wc.mixMatchShowTypeFilters !== false,\n // Off by default: a stored config predating the key must not advance.\n autoAdvance: wc.multiStepAutoAdvance === true,\n formatMoney,\n dropdown: undefined,\n portalTarget: null,\n ctaButton: shell.cta,\n onSelectionChanged: updateAll,\n swapVariant: (stepIdx, item, qty) => {\n let freed = 0;\n const step = selections[stepIdx];\n for (let j = step.length - 1; j >= 0 && freed < qty; j--) {\n const it = step[j];\n if (it.productId !== item.productId || it.variantId === item.variantId) {\n continue;\n }\n const take = Math.min(it.quantity || 1, qty - freed);\n if (take >= (it.quantity || 1)) step.splice(j, 1);\n else it.quantity = (it.quantity || 1) - take;\n freed += take;\n }\n if (freed > 0) {\n item.quantity = freed;\n step.push(item);\n updateAll();\n }\n },\n canRemove: (item) => canRemovePick(selections, item, rulesMap),\n });\n\n // The door opens the wizard at whatever step updateDoor points it at;\n // each empty step's checklist row deep-links its own step.\n door.addEventListener(\"click\", () => {\n if (wizard.isOpen()) return;\n wizard.open(parseInt(door.getAttribute(\"data-step-door\") ?? \"\", 10) || 0);\n });\n shell.root.querySelectorAll<HTMLElement>(\"[data-step-row]\").forEach((el) => {\n el.addEventListener(\"click\", () => {\n if (wizard.isOpen()) return;\n wizard.open(parseInt(el.getAttribute(\"data-step-row\") ?? \"\", 10) || 0);\n });\n });\n\n shell.cta.addEventListener(\"click\", () => {\n if (!allSatisfied(selections, bodyDeps.steps)) return;\n onAddToCart(\n mergeCartLines(selections).map((item) => ({\n merchandiseId: \"gid://shopify/ProductVariant/\" + item.variantId,\n quantity: item.quantity,\n attributes: bundleLineAttributes(bundle.id, bundle.bundleType),\n })),\n );\n });\n\n container.appendChild(shell.root);\n initCountdown(shell.root);\n updateAll();\n\n onCleanup?.(bindDropdowns(shell.root));\n}\n","/**\n * Input-mode tracker — toggles `using-mouse` / `using-keyboard` classes on a\n * target element so CSS can scope focus styles by the customer's current\n * input device. Default is mouse; the first keyboard-navigation keypress\n * (Tab, arrow keys, Enter, Space, Escape, Home/End/PageUp/PageDown) flips\n * to keyboard mode, and the next pointer click flips back.\n *\n * Multiple targets share one pair of document-level listeners — installed\n * on the first `trackInputMode` call, removed when the last target is\n * released. Safe to call across every widget instance on a page without\n * stacking listeners.\n *\n * CSS shape (see bundle-base.css):\n * .using-mouse .lb-bundle-widget :focus { outline: none; }\n *\n * The Liquid theme mirrors this behaviour from `bundle-widget.js` against\n * `document.documentElement` so classic and headless storefronts render the\n * same focus rings.\n */\n\nconst NAV_KEYS = new Set([\n \"Tab\",\n \"ArrowUp\",\n \"ArrowDown\",\n \"ArrowLeft\",\n \"ArrowRight\",\n \"Home\",\n \"End\",\n \"PageUp\",\n \"PageDown\",\n \"Enter\",\n \" \",\n \"Escape\",\n]);\n\nconst targets = new Set<HTMLElement>();\nlet listenersAttached = false;\n\nfunction setAll(on: \"using-mouse\" | \"using-keyboard\"): void {\n const off = on === \"using-mouse\" ? \"using-keyboard\" : \"using-mouse\";\n for (const el of targets) {\n el.classList.add(on);\n el.classList.remove(off);\n }\n}\n\nfunction onKeyDown(e: KeyboardEvent): void {\n if (NAV_KEYS.has(e.key)) setAll(\"using-keyboard\");\n}\n\nfunction onPointerDown(): void {\n setAll(\"using-mouse\");\n}\n\nfunction attachListeners(): void {\n if (listenersAttached) return;\n listenersAttached = true;\n document.addEventListener(\"keydown\", onKeyDown, true);\n document.addEventListener(\"pointerdown\", onPointerDown, true);\n}\n\nfunction detachListeners(): void {\n if (!listenersAttached) return;\n listenersAttached = false;\n document.removeEventListener(\"keydown\", onKeyDown, true);\n document.removeEventListener(\"pointerdown\", onPointerDown, true);\n}\n\nexport function trackInputMode(target: HTMLElement): () => void {\n target.classList.add(\"using-mouse\");\n target.classList.remove(\"using-keyboard\");\n targets.add(target);\n attachListeners();\n\n return () => {\n targets.delete(target);\n target.classList.remove(\"using-mouse\");\n target.classList.remove(\"using-keyboard\");\n if (targets.size === 0) detachListeners();\n };\n}\n","/**\n * Base + type-specific widget CSS, inlined as template literals so the web\n * component can dump them into its shadow root. Source of truth for these\n * rules is `extensions/bundle-theme/assets/*.css` — the classic Shopify\n * theme app block reads the same files. Keep the two in lockstep; the\n * bundle-css-parity.test.ts golden test enforces byte equality.\n *\n * `BUNDLE_SKELETON_CSS` (at the bottom of this file) is intentionally\n * web-component-only. The theme app block never renders a loading\n * state — its Liquid render is synchronous on the server — so the\n * skeleton styles would be dead rules there. Excluding from parity.\n */\nexport const BUNDLE_BASE_CSS = `/* Lime Bundles — shared base styles for all bundle widget types */\n\n.lb-bundle-widget.lb-bundle-widget,\n.lb-bundle-widget.lb-bundle-widget * {\n line-height: normal;\n}\n\n.lb-bundle-widget {\n /* Internal CSS-only vars (not merchant-configurable). */\n --lb-thumbnail-bg: #F0F0F0;\n --lb-widget-pad: 20px;\n /* Cap on the per-bundle product/slot/tier list height — keeps long\n bundles from pushing the CTA off-screen. The list scrolls\n internally (with the .lb-edge-fade treatment) when content exceeds\n this. */\n --lb-list-max-height: 360px;\n /* Depth of the soft fade at a scroll list's clipped edge. Sized to dim\n roughly half of the peeking product row (~90px tall): at 20px shoppers\n read the cut as the end of the list and missed the products below. */\n --lb-edge-fade-size: 48px;\n /* Derived \"small\" corner radius for chips and badges — the low-stock badge,\n the volume tier badge, the multi-step count chip. Controls and media\n (thumbnails, selects, steppers, buttons) take the full --lb-radius: they\n are the elements the merchant is looking at when they pick a preset, and\n the picker's own help text already promises they follow it.\n\n Proportional rather than a fixed subtraction. At \"subtle\" (6px) minus-4px\n left 2px, which reads as square — the merchant saw a picker set to Subtle\n render with sharp corners (issue #363). Two thirds holds the proportion at\n every preset: none 0, subtle 4px, rounded 8px, round 12px. No max() needed,\n since 0 × ⅔ is 0. */\n --lb-radius-sm: calc(var(--lb-radius) * 2 / 3);\n\n font-family: inherit;\n font-size: 16px;\n background: var(--lb-bg);\n border: var(--lb-border-width) solid var(--lb-border);\n border-radius: var(--lb-radius);\n padding: var(--lb-widget-pad) var(--lb-widget-pad) 20px;\n box-sizing: border-box;\n /* Cap the widget at a comfortable reading width on desktop. Below\n 440px viewports the container is already narrower than the cap,\n so the rule is inert on mobile. */\n max-width: 440px;\n}\n\n/* Edge fade — the shared treatment for inset scroll lists (fixed products,\n mix & match slots, volume tiers). The native scrollbar channel is hidden;\n instead the clipped edge of the list softly fades so content melts away\n rather than ending in a hard cut against an app-chrome scrollbar. JS\n (LB.attachEdgeFade) toggles data-edge-fade so an edge fades ONLY when content\n is actually scrolled past it — never at rest — which keeps the first/last\n card and the volume \"Most popular\" badge from ever being dimmed. Without JS\n the list simply scrolls with no bar and no fade (a safe, quiet fallback). */\n.lb-edge-fade {\n scrollbar-width: none;\n}\n.lb-edge-fade::-webkit-scrollbar {\n width: 0;\n height: 0;\n}\n.lb-edge-fade[data-edge-fade=\"top\"] {\n -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 var(--lb-edge-fade-size));\n mask-image: linear-gradient(to bottom, transparent 0, #000 var(--lb-edge-fade-size));\n}\n.lb-edge-fade[data-edge-fade=\"bottom\"] {\n -webkit-mask-image: linear-gradient(to top, transparent 0, #000 var(--lb-edge-fade-size));\n mask-image: linear-gradient(to top, transparent 0, #000 var(--lb-edge-fade-size));\n}\n.lb-edge-fade[data-edge-fade=\"both\"] {\n -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 var(--lb-edge-fade-size), #000 calc(100% - var(--lb-edge-fade-size)), transparent 100%);\n mask-image: linear-gradient(to bottom, transparent 0, #000 var(--lb-edge-fade-size), #000 calc(100% - var(--lb-edge-fade-size)), transparent 100%);\n}\n\n/* Countdown timer bar — a full-bleed \"limited time\" strip sitting below the\n title, with the same breathing room the title gives the product list. */\n.lb-bundle-countdown {\n margin: 0 calc(-1 * var(--lb-widget-pad)) 20px;\n padding: 12px 20px;\n background: var(--lb-countdown-bg);\n display: flex;\n align-items: center;\n justify-content: space-between;\n}\n\n.lb-bundle-countdown__label {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.lb-bundle-countdown__label svg {\n width: 16px;\n height: 16px;\n flex-shrink: 0;\n color: var(--lb-countdown-text);\n}\n\n.lb-bundle-countdown__label span {\n font-size: 12px;\n font-weight: 500;\n line-height: 1;\n color: var(--lb-countdown-text);\n}\n\n.lb-bundle-countdown__timer {\n font-family: 'SF Mono', 'Roboto Mono', ui-monospace, monospace;\n font-size: 12px;\n font-weight: 600;\n line-height: 1;\n color: var(--lb-countdown-text);\n letter-spacing: 0.02em;\n}\n\n/* Hide wrapper when the inner snippet rendered nothing (product OOS / unfulfillable).\n The --loading class is excluded: the web component's skeleton has none of the\n snippet root classes, so this rule matched it and beat its own display:block,\n hiding the skeleton it renders on the first frame. */\n.lb-bundle-widget:not(.lb-bundle-widget--loading):not(\n :has(.lb-fixed, .lb-mix-match, .lb-volume, .lb-bogo, .lb-multi-step)\n ) {\n display: none;\n}\n\n/* Sibling widget spacing — separates multiple bundles on the same product page.\n Uses \\`~\\` (general sibling) rather than \\`+\\` (adjacent) because each Liquid\n loop iteration emits a {% style %} block before its widget div, so the\n rendered DOM alternates <style><widget><style><widget>. The \\`+\\` combinator\n requires immediate adjacency and would match nothing; \\`~\\` matches every\n widget after the first regardless of elements between. Single-widget pages\n stay unaffected (no prior \\`.lb-bundle-widget\\` sibling to match against). */\n.lb-bundle-widget ~ .lb-bundle-widget {\n margin-top: 24px;\n}\n\n/* Header — a plain section-heading title block in the merchant's own type.\n No colored band, no full-bleed, no save-badge pill: the widget reads as a\n part of the store, not as third-party app chrome. The 16px below holds\n whether the title is followed by the product list or the countdown strip. */\n.lb-bundle-header {\n margin: 0 0 16px;\n}\n\n.lb-bundle-header__content {\n min-width: 0;\n}\n\n.lb-bundle-title {\n /* Weight 600 (not 700): the receipt's sale price is the widget's heaviest,\n largest element, so the title steps down to clear the hierarchy. */\n font-size: 18px;\n font-weight: 600;\n line-height: 24px;\n letter-spacing: -0.01em;\n color: var(--lb-text);\n margin: 0;\n}\n\n.lb-bundle-subtitle {\n font-size: 14px;\n font-weight: 400;\n line-height: 20px;\n color: color-mix(in srgb, var(--lb-text) 60%, transparent);\n margin: 4px 0 0;\n}\n\n/* Override Dawn's \\`div:empty { display: none }\\` reset for decorative elements */\n.lb-bundle-divider:empty,\n.lb-mix-match__progress-segment:empty {\n display: block;\n}\n\n/* Divider */\n.lb-bundle-divider {\n height: var(--lb-border-width);\n background: var(--lb-border);\n margin: 16px 0;\n}\n\n/* Product rows */\n.lb-bundle-product-row {\n display: flex;\n gap: 20px;\n padding: 12px 0;\n}\n\n.lb-bundle-thumbnail {\n position: relative;\n width: 48px;\n min-width: 48px;\n /* Aspect-ratio comes from the merchant \\`thumbnailRatio\\` enum via Liquid;\n \"original\" sets it to \\`auto\\` so the box sizes to the image's intrinsic\n ratio. Default keeps the historical 1:1 behaviour. */\n aspect-ratio: var(--lb-thumbnail-aspect-ratio, 1 / 1);\n background: var(--lb-thumbnail-bg);\n border-radius: var(--lb-radius);\n overflow: hidden;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.lb-bundle-thumbnail img {\n width: 100%;\n /* Height + fit come from the same merchant enum; \"original\" sets them to\n \\`auto\\` / \\`contain\\` so the image renders uncropped at intrinsic ratio. */\n height: var(--lb-thumbnail-img-height, 100%);\n object-fit: var(--lb-thumbnail-img-fit, cover);\n}\n\n.lb-bundle-thumbnail svg {\n width: 28px;\n height: 28px;\n color: color-mix(in srgb, var(--lb-text) 35%, transparent);\n}\n\n.lb-bundle-product-info {\n flex: 1;\n min-width: 0;\n}\n\n.lb-bundle-product-name {\n font-size: 16px;\n font-weight: 600;\n line-height: 20px;\n color: var(--lb-text);\n margin: 0;\n text-decoration: none;\n display: block;\n}\n\n/* The underline fades in — text-decoration itself can't transition, but\n its color can. Links only: plain titles carry no hover affordance. */\na.lb-bundle-product-name {\n text-decoration: underline;\n text-decoration-color: transparent;\n transition: text-decoration-color 0.25s ease;\n}\n\na.lb-bundle-product-name:hover {\n text-decoration-color: currentColor;\n}\n\na.lb-bundle-product-name:focus-visible {\n outline: 2px solid var(--lb-primary-color);\n outline-offset: 0;\n box-shadow: none;\n border-radius: 2px;\n}\n\n.lb-bundle-product-price {\n /* --lb-product-price-display fallback is the \"on\" branch; Liquid sets 'none' when merchant disables. */\n display: var(--lb-product-price-display, block);\n font-size: 14px;\n font-weight: 400;\n line-height: 20px;\n color: var(--lb-text);\n font-variant-numeric: tabular-nums;\n}\n\n.lb-bundle-product-prices {\n display: flex;\n align-items: baseline;\n gap: 6px;\n flex-wrap: wrap;\n margin-top: 4px;\n}\n\n.lb-bundle-product-compare-price {\n /* --lb-product-compare-display fallback is the \"on\" branch; Liquid sets 'none' when merchant disables. */\n display: var(--lb-product-compare-display, inline);\n font-size: 14px;\n font-weight: 400;\n line-height: 20px;\n color: color-mix(in srgb, var(--lb-text) 60%, transparent);\n text-decoration: line-through;\n font-variant-numeric: tabular-nums;\n}\n\n/* Setting \\`display\\` above outranks the UA [hidden] rule — restore it so rows\n without a compare-at price don't leave a phantom flex item + gap. */\n.lb-bundle-product-compare-price[hidden] {\n display: none;\n}\n\n/* Unit price (e.g. \"$0.50/100ml\") — only rendered when the merchant has\n configured unit pricing on the variant in the Shopify admin. No merchant\n toggle: present in admin → shown; absent → hidden. Styled as muted\n secondary text beneath the price row so it doesn't compete visually. */\n.lb-bundle-product-unit-price {\n /* Fallback is the \"on\" branch; Liquid sets 'none' when the merchant disables\n productList.showUnitPrice. The [hidden] rule below still wins for rows\n whose variant has no unit price at all. */\n display: var(--lb-product-unit-price-display, block);\n font-size: 12px;\n line-height: 16px;\n color: color-mix(in srgb, var(--lb-text) 60%, transparent);\n margin-top: 2px;\n}\n\n.lb-bundle-product-unit-price[hidden] {\n display: none;\n}\n\n/* Read-only variant text shown below the product title when only one\n variant is in scope (single-variant product, or merchant pinned a\n single variant). Same visual treatment as the mix-match filled\n slot's variant text — see .lb-mix-match__filled-variant in\n bundle-mix-match.css. Both share this rule via the comma selector\n so the storefront UX stays consistent across bundle types. */\n.lb-bundle-variant-badge,\n.lb-mix-match__slot--filled .lb-mix-match__filled-variant {\n display: block;\n font-size: 14px;\n line-height: 20px;\n margin-top: 2px;\n color: color-mix(in srgb, var(--lb-text) 60%, transparent);\n}\n\n/* Per-option variant pickers. Each option's label + select sit inside a\n .lb-bundle-variant-option-group (flex column, 4px gap between label and\n select); the groups stack inside a .lb-bundle-variant-option-groups\n parent (flex column, 8px gap between groups). The parent owns the top\n offset from the preceding unit-price line, so individual labels and\n selects don't carry their own vertical margins. */\n.lb-bundle-variant-option-groups {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: 8px;\n}\n\n.lb-bundle-variant-option-group {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.lb-bundle-variant-option-label {\n display: block;\n margin: 0;\n font-size: 12px;\n line-height: 16px;\n font-weight: 600;\n letter-spacing: 0.05em;\n text-transform: uppercase;\n color: color-mix(in srgb, var(--lb-text) 60%, transparent);\n}\n\n.lb-mix-match__modal-product-info .lb-bundle-variant-option-groups {\n margin-top: 8px;\n}\n\n/* Desktop: lay variant option groups in a wrapping row inside the modal\n (Size + Color side-by-side). Mobile keeps the column stack from the\n base rule above. The 768px breakpoint mirrors bundle-mix-match.css. */\n@media (min-width: 768px) {\n .lb-mix-match__modal-product-info .lb-bundle-variant-option-groups {\n flex-direction: row;\n flex-wrap: wrap;\n }\n /* Direct children only — the qty-stepper-group is also a\n .lb-bundle-variant-option-group but lives in the actions wrapper\n and must keep its natural width. */\n .lb-mix-match__modal-product-info .lb-bundle-variant-option-groups > .lb-bundle-variant-option-group {\n flex: 1;\n }\n}\n\n/* Focus-ring suppression for mouse users. A small JS helper — see\n packages/widget/src/utils/input-mode.ts and bundle-widget.js — toggles\n .using-mouse / .using-keyboard on the widget root (or html in the Liquid\n path) based on the customer's current input device. Default is mouse, so\n click-to-focus doesn't leave a keyboard-style ring. The modal overlay\n gets its own selector because the Liquid path reparents it to body,\n outside the widget root. */\n.using-mouse .lb-bundle-widget :focus:not([aria-checked=\"true\"]),\n.using-mouse .lb-bundle-widget :focus-visible:not([aria-checked=\"true\"]),\n.using-mouse .lb-mix-match__modal-overlay :focus:not([aria-checked=\"true\"]),\n.using-mouse .lb-mix-match__modal-overlay :focus-visible:not([aria-checked=\"true\"]) {\n outline: none;\n outline-offset: 0;\n box-shadow: none;\n}\n\n.lb-bundle-quantity {\n font-size: 12px;\n line-height: 16px;\n color: color-mix(in srgb, var(--lb-text) 60%, transparent);\n margin-left: 8px;\n white-space: nowrap;\n}\n\n/* Receipt summary — a column: the total row, then one quiet savings line.\n The sale price is the widget's #1 element (largest, heaviest), so the eye\n lands on what the bundle costs. Inherits the widget's global text color. */\n.lb-bundle-summary {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n margin-bottom: 16px;\n}\n\n/* Left column — the label over the savings line. Paired with the price stack on\n the right and vertically centred against it, so the summary stays balanced\n whether or not a savings line / compare price is present (e.g. an undiscounted\n bundle reads as a clean \"label … total\" on one centred line). */\n.lb-bundle-summary__text {\n display: flex;\n flex-direction: column;\n gap: 2px;\n min-width: 0;\n}\n\n.lb-bundle-summary__label {\n font-size: 15px;\n font-weight: 400;\n line-height: 1.4;\n text-transform: uppercase;\n color: color-mix(in srgb, var(--lb-text) 70%, transparent);\n}\n\n/* Savings line — the one savings signal, quiet text under the label. No pill,\n no lime: lime is reserved for the CTA. JS toggles its display when savings\n reach/leave zero (the [data-savings-bar] hook name is unchanged). */\n.lb-bundle-savings-line {\n font-size: 13px;\n font-weight: 400;\n line-height: 1.4;\n color: color-mix(in srgb, var(--lb-text) 60%, transparent);\n margin: 0;\n}\n\n.lb-bundle-savings-line [data-savings-amount] {\n color: var(--lb-text);\n font-variant-numeric: tabular-nums;\n}\n\n/* Right column — the struck original over the bundle total (\"was / now\"),\n vertically centred against the label/savings column on the left. */\n.lb-bundle-summary__prices {\n display: flex;\n flex-direction: column;\n align-items: flex-end;\n gap: 1px;\n flex-shrink: 0;\n text-align: right;\n}\n\n.lb-bundle-sale-price {\n font-size: 24px;\n font-weight: 600;\n line-height: 1.1;\n letter-spacing: -0.02em;\n color: var(--lb-text);\n font-variant-numeric: tabular-nums;\n}\n\n.lb-bundle-compare-price {\n font-size: 14px;\n font-weight: 400;\n line-height: 1.3;\n color: color-mix(in srgb, var(--lb-text) 55%, transparent);\n text-decoration: line-through;\n font-variant-numeric: tabular-nums;\n}\n\n/* Inline quantity — \"×2\" beside the product price (mix & match chosen-product\n cards). Quiet, theme-inherited; the fixed widget uses .lb-bundle-qty-chip in\n the card's right slot instead. */\n.lb-bundle-qty-inline {\n /* Fallback is the \"on\" branch; Liquid sets 'none' when the merchant disables\n productList.showQuantity. The fixed widget's chip carries its own var\n because it is a flex box, not inline text. */\n display: var(--lb-product-qty-inline-display, inline);\n font-size: 13px;\n font-weight: 400;\n color: color-mix(in srgb, var(--lb-text) 55%, transparent);\n font-variant-numeric: tabular-nums;\n white-space: nowrap;\n}\n\n/* CTA button.\n * Label and spinner share a single 1×1 grid cell so the button's intrinsic\n * width/height stays fixed when swapping between them — no layout shift when\n * entering the loading state. Visibility (not display) is used so the hidden\n * child still contributes to the cell's min-content sizing. See the\n * [data-loading=\"true\"] rules below. */\n.lb-bundle-cta {\n display: grid;\n grid-template-rows: 1fr;\n grid-template-columns: 1fr;\n width: 100%;\n padding: 12px 16px;\n border: var(--lb-cta-border-width) solid var(--lb-cta-border-color);\n border-radius: var(--lb-radius);\n font-size: 16px;\n font-weight: 600;\n line-height: 20px;\n cursor: pointer;\n text-align: center;\n transition: opacity 0.25s ease;\n font-family: inherit;\n}\n\n.lb-bundle-cta:not(:disabled) {\n background: var(--lb-primary-color);\n color: var(--lb-btn-text);\n}\n\n.lb-bundle-cta:not(:disabled):hover {\n opacity: 0.9;\n}\n\n.lb-bundle-cta:disabled {\n background: color-mix(in srgb, var(--lb-primary-color) 35%, var(--lb-bg));\n color: color-mix(in srgb, var(--lb-btn-text) 85%, transparent);\n cursor: not-allowed;\n}\n\n/* Both the label and the spinner occupy grid cell (1, 1). Only one is\n * visible at a time; the other keeps its box for sizing but is invisible. */\n.lb-cta-label,\n.lb-cta-spinner {\n grid-row: 1;\n grid-column: 1;\n display: flex;\n align-items: center;\n justify-content: center;\n min-width: 0;\n}\n\n.lb-cta-spinner {\n visibility: hidden;\n}\n\n.lb-cta-spinner svg {\n width: 20px;\n height: 20px;\n animation: lb-cta-spin 0.8s linear infinite;\n}\n\n@keyframes lb-cta-spin {\n to { transform: rotate(360deg); }\n}\n\n.lb-bundle-cta[data-loading=\"true\"] {\n cursor: wait;\n pointer-events: none;\n}\n\n.lb-bundle-cta[data-loading=\"true\"] .lb-cta-label {\n visibility: hidden;\n}\n\n.lb-bundle-cta[data-loading=\"true\"] .lb-cta-spinner {\n visibility: visible;\n}\n\n/* Transient \"Added\" confirmation — JS swaps the label text on success; a quick\n pop draws the eye to the state change. No bounce. */\n.lb-bundle-cta[data-added=\"true\"] .lb-cta-label {\n animation: lb-cta-added-pop 0.22s cubic-bezier(0.25, 1, 0.5, 1);\n}\n\n@keyframes lb-cta-added-pop {\n from { opacity: 0.5; transform: scale(0.96); }\n to { opacity: 1; transform: scale(1); }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .lb-cta-spinner svg {\n animation-duration: 2.5s;\n }\n .lb-bundle-cta[data-added=\"true\"] .lb-cta-label {\n animation: none;\n }\n}\n\n/* Error / warning message — amber, never red. Stock and warning states share\n the merchant-overridable low-stock token; the leading icon keeps meaning\n from being carried by color alone. */\n.lb-bundle-error {\n font-size: 16px;\n color: var(--lb-low-stock-text);\n margin-top: 8px;\n display: none;\n}\n\n.lb-bundle-error[data-visible=\"true\"] {\n display: flex;\n align-items: center;\n gap: 6px;\n}\n\n.lb-bundle-error[data-visible=\"true\"]::before {\n content: \"\";\n flex-shrink: 0;\n width: 18px;\n height: 18px;\n background-color: currentColor;\n -webkit-mask: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z'/%3E%3Cline x1='12' y1='9' x2='12' y2='13'/%3E%3Cline x1='12' y1='17' x2='12.01' y2='17'/%3E%3C/svg%3E\") center / contain no-repeat;\n mask: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z'/%3E%3Cline x1='12' y1='9' x2='12' y2='13'/%3E%3Cline x1='12' y1='17' x2='12.01' y2='17'/%3E%3C/svg%3E\") center / contain no-repeat;\n}\n\n/* Visually hidden — accessible to screen readers only */\n.lb-visually-hidden {\n position: absolute !important;\n width: 1px !important;\n height: 1px !important;\n padding: 0 !important;\n margin: -1px !important;\n overflow: hidden !important;\n clip-path: inset(50%) !important;\n white-space: nowrap !important;\n border: 0 !important;\n}\n\n/* Placeholder SVG icon for missing images */\n.lb-bundle-placeholder-icon {\n width: 28px;\n height: 28px;\n stroke: color-mix(in srgb, var(--lb-text) 35%, transparent);\n stroke-width: 1.5;\n fill: none;\n}\n\n/* Out-of-stock product row */\n.lb-bundle-product-row--oos {\n opacity: 0.5;\n}\n\n.lb-bundle-oos-label {\n font-size: 12px;\n font-weight: 500;\n color: var(--lb-low-stock-text);\n white-space: nowrap;\n margin-left: auto;\n}\n\n/* \"Only X left\" low-stock badge — appears alongside product/variant\n info when ProductVariant.quantityAvailable falls at or below\n wc.lowStockThreshold. Hidden when quantityAvailable is unknown\n (Storefront token without read_product_inventory). */\n.lb-bundle-low-stock-badge {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n padding: 2px 8px;\n font-size: 11px;\n font-weight: 600;\n line-height: 1.4;\n color: var(--lb-low-stock-text);\n background-color: var(--lb-low-stock-bg);\n border-radius: var(--lb-radius-sm);\n white-space: nowrap;\n}\n\n/* A/B test: hide save badge until JS swaps the label (prevents flash of default) */\n.lb-ab-pending {\n visibility: hidden;\n}\n\n/* ── Geometry reservation ─────────────────────────────────────────────────\n Liquid renders each row's structure and its product title; everything else\n is filled by JS on hydration. These rules give the empty boxes the exact\n height their filled counterparts will occupy, so a row never changes size\n between first paint and hydration — cumulative layout shift stays at zero.\n\n \\`:empty\\` stops matching the moment JS puts content in, handing the height\n back to the real content, which measures the same.\n\n The title is the one box NOT reserved this way: text wrapping depends on the\n merchant's font, so it is server-rendered and its height is simply real. */\n\n.lb-bundle-product-price:empty {\n display: inline-block;\n min-height: 20px;\n}\n\n.lb-bundle-variant-badge:empty {\n display: block;\n min-height: 20px;\n}\n\n.lb-bundle-product-unit-price:empty {\n display: block;\n min-height: 16px;\n}\n\n/* Label (16) + gap (4) + trigger (8 + 16 + 8 padding/line-height) per group,\n plus the 8px gap between groups. --lb-row-option-count is set per row by\n Liquid, which knows how many options the product has. */\n.lb-bundle-variant-option-groups:empty {\n min-height: calc(\n var(--lb-row-option-count, 1) * (52px + 2 * var(--lb-border-width)) +\n (var(--lb-row-option-count, 1) - 1) * 8px\n );\n}\n\n/* The thumbnail is a fixed 48px wide with an aspect-ratio, so its box is sized\n before the image loads. The \"original\" ratio is the exception — it sizes to\n the image's intrinsic dimensions, which nothing knows up front. Liquid emits\n the real ratio per row so even that case reserves correctly. */\n.lb-bundle-thumbnail[style*=\"--lb-row-thumb-ratio\"] {\n aspect-ratio: var(--lb-row-thumb-ratio);\n}\n/* This selector outranks the \\`.lb-bundle-thumbnail\\` rule above, so Liquid must\n emit the inline var for the \"original\" ratio and nothing else. Emitting it\n unconditionally silently replaced every merchant ratio with the image's own,\n which looks like the setting being ignored. */\n`;\nexport const BUNDLE_FIXED_CSS = `/* Lime Bundles — Fixed bundle styles */\n\n.lb-fixed__products {\n display: flex;\n flex-direction: column;\n gap: 12px;\n margin: 0;\n max-height: var(--lb-list-max-height);\n overflow-y: auto;\n /* Scrollbar hidden + edge fade — see .lb-edge-fade in bundle-base.css. */\n}\n\n/* Fixed bundles: product cards. Each product is a bordered card — thumbnail,\n info, and a right-slot quantity chip — mirroring the mix & match chosen-\n product card so the two bundle types read the same. The info column flows\n name → variant → price → unit → pickers on the inherited type. */\n.lb-fixed .lb-bundle-product-row {\n display: flex;\n align-items: flex-start;\n gap: 12px;\n padding: 8px;\n border: var(--lb-border-width) solid var(--lb-border);\n border-radius: var(--lb-radius);\n box-sizing: border-box;\n}\n\n/* Larger thumbnails. Height comes from --lb-thumbnail-aspect-ratio\n (set on .lb-bundle-widget via Liquid / applyWidgetConfigVars). Thumbnails\n carry only the derived corner radius — no border. */\n.lb-fixed .lb-bundle-thumbnail {\n width: 60px;\n min-width: 60px;\n border-radius: var(--lb-radius);\n box-sizing: border-box;\n}\n\n.lb-fixed .lb-bundle-thumbnail img {\n border-radius: var(--lb-radius);\n border: none;\n}\n\n/* Quantity chip — the card's right slot. A quiet, non-interactive count (×N),\n deliberately border-less so it never reads as the mix & match remove button. */\n.lb-bundle-qty-chip {\n flex-shrink: 0;\n min-width: 34px;\n height: 28px;\n padding: 0 8px;\n /* Fallback is the \"on\" branch; Liquid sets 'none' when the merchant disables\n productList.showQuantity. Separate from --lb-product-qty-inline-display\n because this chip is a flex box and the other counts are inline text. */\n display: var(--lb-product-qty-chip-display, inline-flex);\n align-items: center;\n justify-content: center;\n border-radius: var(--lb-radius);\n background: color-mix(in srgb, var(--lb-text) 7%, transparent);\n color: color-mix(in srgb, var(--lb-text) 75%, transparent);\n font-size: 13px;\n font-weight: 500;\n line-height: 1;\n font-variant-numeric: tabular-nums;\n white-space: nowrap;\n}\n\n/* Variant picker select — styled to match the variant badge aesthetic.\n Sits inside .lb-bundle-variant-option-group so vertical spacing is owned\n by the group/groups flex gap, not the select itself. */\n.lb-bundle-variant-select {\n display: inline-block;\n border: var(--lb-border-width) solid var(--lb-border);\n border-radius: var(--lb-radius);\n padding: 8px 32px 8px 12px;\n font-size: 12px;\n line-height: 16px;\n color: var(--lb-text);\n background: var(--lb-bg);\n font-family: inherit;\n cursor: pointer;\n appearance: none;\n -webkit-appearance: none;\n background-image: var(--lb-variant-chevron);\n background-repeat: no-repeat;\n background-position: right 8px center;\n background-size: 12px;\n width: 50%;\n max-width: 50%;\n}\n\n.lb-bundle-variant-select:focus-visible {\n outline: 2px solid var(--lb-primary-color);\n outline-offset: 2px;\n box-shadow: none;\n}\n`;\nexport const BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles — Mix & Match styles */\n\n/* === Selected product list ================================================\n The chosen products render as bordered cards. Capped height so long\n bundles don't push the CTA off the page; internal scroll uses the same\n custom 4px scrollbar as the variant dropdown panel. */\n.lb-mix-match__slots {\n display: flex;\n flex-direction: column;\n gap: 12px;\n max-height: var(--lb-list-max-height);\n overflow-y: auto;\n /* Scrollbar hidden + edge fade — see .lb-edge-fade in bundle-base.css. */\n}\n\n.lb-mix-match__slots:empty {\n display: none;\n}\n\n/* === Progress Bar (segmented) ============================================\n One rounded segment per required pick; the first N (count) fill with the\n primary colour as the shopper adds products. */\n.lb-mix-match__progress {\n margin-bottom: 16px;\n}\n\n.lb-mix-match__progress-segments {\n display: flex;\n gap: 6px;\n width: 100%;\n}\n\n.lb-mix-match__progress-segment {\n flex: 1;\n height: 6px;\n /* Follows the global radius (like the widget's other small elements); the\n modal override below switches it to the picker radius. Capped by the 6px\n height, so it reads as a pill until the merchant picks a tighter radius. */\n border-radius: var(--lb-radius-sm);\n background: color-mix(in srgb, var(--lb-text) 12%, transparent);\n /* The fill is an inner layer that grows/retracts horizontally (0.3s per\n DESIGN.md's motion vocabulary) rather than a colour swap. */\n position: relative;\n overflow: hidden;\n}\n\n.lb-mix-match__progress-segment::after {\n content: '';\n position: absolute;\n inset: 0;\n background: var(--lb-text);\n border-radius: inherit;\n transform: scaleX(0);\n transform-origin: left;\n transition: transform 0.3s ease;\n}\n\n.lb-mix-match__progress-segment--filled::after {\n transform: scaleX(1);\n}\n\n.lb-mix-match__progress-labels {\n display: flex;\n justify-content: space-between;\n margin-top: 8px;\n}\n\n.lb-mix-match__progress-count {\n font-size: 12px;\n font-weight: 500;\n line-height: 16px;\n color: color-mix(in srgb, var(--lb-text) 60%, transparent);\n}\n\n.lb-mix-match__progress-remaining {\n font-size: 12px;\n font-weight: 500;\n line-height: 16px;\n color: var(--lb-text);\n}\n\n/* In the modal the segmented bar lives inside the header, below the subtitle\n and above the header's bottom border. */\n.lb-mix-match__modal-progress {\n margin: 0;\n padding: 0;\n}\n\n/* The modal's progress segments follow the picker radius, not the global\n one — and the picker PALETTE: inside the modal the track and fill read\n --lb-picker-text, not the widget's --lb-text. */\n.lb-mix-match__modal-progress .lb-mix-match__progress-segment {\n border-radius: var(--lb-picker-radius-sm);\n background: color-mix(in srgb, var(--lb-picker-text) 12%, transparent);\n}\n\n.lb-mix-match__modal-progress .lb-mix-match__progress-segment::after {\n background: var(--lb-picker-text);\n}\n\n/* === Selected product cards === */\n.lb-mix-match__slot--filled {\n display: flex;\n align-items: flex-start;\n gap: 12px;\n padding: 8px;\n border: var(--lb-border-width) solid var(--lb-border);\n border-radius: var(--lb-radius);\n box-sizing: border-box;\n /* The whole card is an edit target — clicking it reopens the picker. */\n cursor: pointer;\n transition: border-color 0.25s ease;\n}\n\n.lb-mix-match__slot--filled:hover {\n border-color: color-mix(in srgb, var(--lb-text) 45%, transparent);\n}\n\n/* Mix-match thumbnails. Height comes from --lb-thumbnail-aspect-ratio\n (set on .lb-bundle-widget via Liquid / applyWidgetConfigVars). */\n.lb-mix-match .lb-bundle-thumbnail {\n position: relative;\n width: 60px;\n min-width: 60px;\n border-radius: var(--lb-radius);\n box-sizing: border-box;\n}\n\n.lb-mix-match .lb-bundle-thumbnail img {\n border-radius: var(--lb-radius);\n border: none;\n}\n\n.lb-mix-match__slot--filled .lb-mix-match__filled-info {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n}\n\n/* The title is an edit BUTTON (reopens the picker), not a product link —\n reset the UA button chrome and inherit the theme font. */\n.lb-mix-match__slot--filled .lb-mix-match__filled-title {\n font-size: 16px;\n font-weight: 600;\n line-height: 20px;\n color: var(--lb-text);\n text-decoration: none;\n overflow-wrap: break-word;\n background: none;\n border: none;\n margin: 0;\n padding: 0;\n font-family: inherit;\n text-align: left;\n cursor: pointer;\n}\n\n.lb-mix-match__slot--filled .lb-mix-match__filled-title:focus-visible {\n outline: 2px solid var(--lb-primary-color);\n outline-offset: 2px;\n box-shadow: none;\n border-radius: 2px;\n}\n\n/* .lb-mix-match__filled-variant — styled jointly with\n .lb-bundle-variant-badge above to keep the variant text consistent\n across mix-match and fixed bundle widgets. */\n\n.lb-mix-match__slot--filled .lb-mix-match__filled-price {\n /* Canonical price row (WIDGET-DESIGN.md): flex container, 6px gap, no\n toggles here — show/hide vars live on the inner price elements. */\n display: flex;\n align-items: baseline;\n gap: 6px;\n flex-wrap: wrap;\n margin-top: 4px;\n}\n\n.lb-mix-match__filled-compare {\n /* --lb-product-compare-display fallback is the \"on\" branch; Liquid sets 'none' when merchant disables. */\n display: var(--lb-product-compare-display, inline);\n font-size: 14px;\n font-weight: 400;\n color: color-mix(in srgb, var(--lb-text) 60%, transparent);\n text-decoration: line-through;\n}\n\n/* Boxed × remove control on the right of each card. */\n/* Remove control — the interactive member of the right-slot chip family\n (WIDGET-DESIGN.md): same 28px / --lb-radius-sm / neutral-fill silhouette\n as the quantity and savings chips. The visual box is 28px; the ::before\n pseudo-element extends the touch target to 44px without changing layout. */\n.lb-mix-match__slot-remove {\n flex-shrink: 0;\n position: relative;\n width: 28px;\n height: 28px;\n min-width: 28px;\n display: flex;\n align-items: center;\n justify-content: center;\n background: color-mix(in srgb, var(--lb-text) 7%, transparent);\n border: none;\n border-radius: var(--lb-radius-sm);\n cursor: pointer;\n color: color-mix(in srgb, var(--lb-text) 75%, transparent);\n padding: 0;\n margin-left: auto;\n transition: background 0.25s ease, color 0.25s ease;\n}\n\n.lb-mix-match__slot-remove::before {\n content: '';\n position: absolute;\n inset: -8px;\n}\n\n.lb-mix-match__slot-remove:hover {\n background: color-mix(in srgb, var(--lb-text) 12%, transparent);\n color: var(--lb-text);\n}\n\n.lb-mix-match__slot-remove:focus-visible {\n outline: 2px solid var(--lb-primary-color);\n outline-offset: -2px;\n box-shadow: none;\n}\n\n/* \"Required\" chip — shown in the right slot in place of the × when a\n required product sits at its minimum (removing it would break the\n bundle). Right-slot chip family silhouette; pure state, not a control. */\n.lb-mix-match__slot-required {\n flex-shrink: 0;\n height: 28px;\n display: inline-flex;\n align-items: center;\n padding: 0 8px;\n background: color-mix(in srgb, var(--lb-text) 7%, transparent);\n border-radius: var(--lb-radius-sm);\n color: color-mix(in srgb, var(--lb-text) 75%, transparent);\n font-size: 13px;\n font-weight: 500;\n line-height: 1;\n white-space: nowrap;\n margin-left: auto;\n}\n\n/* === \"Add a product\" — a dashed empty slot === */\n/* Reads as the next card to fill, mirroring the filled cards' geometry (the +\n sits in the thumbnail position). Dashed + theme-inherited text, so lime stays\n reserved for the CTA rather than tinting a non-action element. */\n.lb-mix-match__add-product {\n display: flex;\n align-items: center;\n gap: 12px;\n width: 100%;\n margin-top: 12px;\n padding: 8px;\n background: none;\n border: 1px dashed color-mix(in srgb, var(--lb-text) 28%, transparent);\n border-radius: var(--lb-radius);\n color: color-mix(in srgb, var(--lb-text) 75%, transparent);\n font-family: inherit;\n font-size: 15px;\n font-weight: 500;\n line-height: 1.2;\n text-align: left;\n cursor: pointer;\n box-sizing: border-box;\n transition: border-color 0.25s ease, color 0.25s ease;\n}\n\n.lb-mix-match__add-product:hover {\n border-color: color-mix(in srgb, var(--lb-text) 45%, transparent);\n color: var(--lb-text);\n}\n\n.lb-mix-match__add-product:focus-visible {\n outline: 2px solid var(--lb-primary-color);\n outline-offset: 2px;\n box-shadow: none;\n}\n\n.lb-mix-match__add-product-icon {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 60px;\n min-width: 60px;\n height: 40px;\n flex-shrink: 0;\n color: inherit;\n}\n\n/* Derived \"small\" radius for the picker's chips and badges — the progress\n segments, the \"Added\" badge, the close button, the filter pills. The\n picker-scoped mirror of --lb-radius-sm, and proportional for the same reason\n (see bundle-base.css).\n\n Everything the merchant would call a control or an image — the modal, search,\n product tiles, thumbnails, variant dropdown, quantity stepper and the\n buttons — takes the full --lb-picker-radius instead. That is what the\n editor's own Borders copy promises them, and the reduced tier had it\n rendering square at the Subtle preset (issue #363).\n\n Declared on BOTH hosts the modal can live under. On the storefront it portals\n to [data-modal-overlay], which the variant listbox portals up to as well. In\n the admin widget editor there is no overlay at all — the preview renders the\n modal directly under .lb-bundle-widget — so scoping this to the overlay alone\n left the variable undefined there and every control using it fell back to\n square corners while the modal and tiles rounded correctly (issue #363). */\n.lb-bundle-widget,\n[data-modal-overlay] {\n --lb-picker-radius-sm: calc(var(--lb-picker-radius) * 2 / 3);\n}\n\n/* === Modal Overlay === */\n.lb-mix-match__modal-overlay {\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(0, 0, 0, 0.5);\n z-index: 9999;\n display: flex;\n align-items: center;\n justify-content: center;\n opacity: 0;\n transition: opacity 0.2s ease-out;\n}\n\n.lb-mix-match__modal-overlay--open {\n opacity: 1;\n}\n\n/* === Modal Panel === */\n.lb-mix-match__modal {\n background: var(--lb-picker-bg);\n color: var(--lb-picker-text);\n border-radius: var(--lb-picker-radius);\n border: var(--lb-picker-border-width) solid var(--lb-picker-border-color);\n box-sizing: border-box;\n width: 100%;\n max-width: 520px;\n max-height: 70vh;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.16);\n transform: translateY(24px);\n transition: transform 0.25s ease-out;\n will-change: transform;\n}\n\n.lb-mix-match__modal-overlay--open .lb-mix-match__modal {\n transform: translateY(0);\n}\n\n/* === Modal Header === */\n.lb-mix-match__modal-header {\n display: flex;\n flex-direction: column;\n gap: 16px;\n padding: 20px;\n border-bottom: 1px solid color-mix(in srgb, var(--lb-picker-text) 15%, transparent);\n flex-shrink: 0;\n}\n\n.lb-mix-match__modal-header-top {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: 12px;\n}\n\n.lb-mix-match__modal-heading {\n min-width: 0;\n}\n\n.lb-mix-match__modal-title {\n font-size: 20px;\n font-weight: 600;\n line-height: 24px;\n margin: 0;\n color: inherit;\n}\n\n/* Dynamic \"Choose N more to unlock X% off\" line under the title. */\n.lb-mix-match__modal-subtitle {\n margin: 4px 0 0;\n font-size: 13px;\n line-height: 18px;\n color: color-mix(in srgb, var(--lb-picker-text) 60%, transparent);\n}\n\n.lb-mix-match__modal-subtitle:empty {\n display: none;\n}\n\n.lb-mix-match__modal-close {\n min-width: 44px;\n min-height: 44px;\n display: flex;\n align-items: center;\n justify-content: center;\n background: none;\n border: none;\n cursor: pointer;\n color: var(--lb-picker-text);\n border-radius: var(--lb-picker-radius-sm);\n padding: 0;\n margin: -12px -12px 0 0;\n transition: background 0.25s ease;\n}\n\n.lb-mix-match__modal-close:hover {\n background: color-mix(in srgb, var(--lb-picker-text) 5%, transparent);\n}\n\n.lb-mix-match__modal-close:focus-visible {\n outline: 2px solid var(--lb-primary-color);\n outline-offset: 0;\n box-shadow: none;\n}\n\n/* === Modal Filter Pills === */\n.lb-mix-match__filters {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n /* Bottom padding lives on the filters (a fixed section) rather than the\n scrolling list, so the gap below the pills stays put as the list scrolls. */\n padding: 16px 20px 16px;\n flex-shrink: 0;\n}\n\n.lb-mix-match__filter {\n padding: 6px 14px;\n border: var(--lb-picker-border-width) solid var(--lb-picker-border-color);\n /* Pills followed the merchant's picker radius as of #363 — a fixed 999px\n read as a stray rounded shape in a picker set to None or Subtle. */\n border-radius: var(--lb-picker-radius-sm);\n background: transparent;\n color: var(--lb-picker-text);\n font-family: inherit;\n font-size: 13px;\n font-weight: 500;\n line-height: 1;\n cursor: pointer;\n white-space: nowrap;\n transition: background 0.25s ease, border-color 0.25s ease, color 0.25s ease;\n}\n\n.lb-mix-match__filter:focus-visible {\n outline: 2px solid var(--lb-primary-color);\n outline-offset: 2px;\n box-shadow: none;\n}\n\n/* Picker vars, not widget vars — the modal is its own colour context;\n mixing the two palettes can render the active label invisible. */\n.lb-mix-match__filter--active {\n background: var(--lb-picker-text);\n border-color: var(--lb-picker-text);\n color: var(--lb-picker-bg);\n}\n\n/* === Modal Search === */\n.lb-mix-match__modal-search {\n padding: 16px 20px 0;\n position: relative;\n flex-shrink: 0;\n}\n\n/* Search icon pinned inside the left edge of the input. Rendered as a masked\n pseudo-element on the search row (an <input> can't host ::before) so the\n icon colour tracks the merchant's picker text at 60% — consistent with the\n variant-select chevron and the rest of the picker. */\n.lb-mix-match__modal-search::before {\n content: \"\";\n position: absolute;\n left: 36px;\n top: 16px;\n bottom: 0;\n width: 18px;\n background-color: color-mix(in srgb, var(--lb-picker-text) 60%, transparent);\n -webkit-mask: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0'/%3E%3Cpath d='M21 21l-6 -6'/%3E%3C/svg%3E\") center / 18px no-repeat;\n mask: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0'/%3E%3Cpath d='M21 21l-6 -6'/%3E%3C/svg%3E\") center / 18px no-repeat;\n pointer-events: none;\n}\n\n.lb-mix-match__modal-search-input {\n width: 100%;\n padding: 12px 40px 12px 44px;\n border: var(--lb-picker-border-width) solid var(--lb-picker-border-color);\n border-radius: var(--lb-picker-radius);\n font-size: 16px;\n line-height: 20px;\n color: var(--lb-picker-text);\n /* padding-left (above) clears the search icon, which is drawn by\n .lb-mix-match__modal-search::before so it can take a themed colour. */\n background-color: var(--lb-picker-bg);\n box-sizing: border-box;\n -webkit-appearance: none;\n appearance: none;\n}\n\n.lb-mix-match__modal-search-input::placeholder {\n color: color-mix(in srgb, var(--lb-picker-text) 50%, transparent);\n}\n\n.lb-mix-match__modal-search-input:focus-visible {\n outline: 2px solid var(--lb-primary-color);\n outline-offset: 2px;\n box-shadow: none;\n}\n\n.lb-mix-match__modal-search-clear {\n position: absolute;\n right: 32px;\n /* Anchor to the input area only — the search row's top padding (16px) plus\n the input height keeps the button centred on it. */\n top: 16px;\n bottom: 0;\n min-width: 32px;\n min-height: 32px;\n display: flex;\n align-items: center;\n justify-content: center;\n background: none;\n border: none;\n cursor: pointer;\n color: color-mix(in srgb, var(--lb-picker-text) 60%, transparent);\n padding: 0;\n}\n\n/* When the filter row isn't shown (filters off, or fewer than two product\n types), the search row owns the gap above the product list. The icon and\n clear button re-anchor to bottom: 16px so they stay centred on the input. */\n.lb-mix-match__modal--filters-hidden .lb-mix-match__modal-search {\n padding-bottom: 16px;\n}\n\n.lb-mix-match__modal--filters-hidden .lb-mix-match__modal-search::before,\n.lb-mix-match__modal--filters-hidden .lb-mix-match__modal-search-clear {\n bottom: 16px;\n}\n\n/* === Modal Product List === */\n.lb-mix-match__modal-list {\n overflow-y: auto;\n flex: 1;\n /* Always keep a small top padding so the first row's count bubbles\n (which sit at top: -8px) aren't clipped by overflow. The larger gap\n above the list comes from the filters' bottom padding, or — when no\n filters show — the search's bottom padding (see --filters-hidden). */\n padding: 8px 20px 20px;\n -webkit-overflow-scrolling: touch;\n /* Two-column product grid. Each product is a vertical card (image on\n top, info below) built by JS. Stays 2-up on mobile too (the modal\n is a bottom sheet there). */\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n column-gap: 24px;\n row-gap: 28px;\n align-content: start;\n}\n\n/* When neither search nor filters render, the list follows the header\n directly and needs the full top gap restored. */\n.lb-mix-match__modal-header + .lb-mix-match__modal-list {\n padding-top: 16px;\n}\n\n.lb-mix-match__modal-product {\n display: flex;\n flex-direction: column;\n padding: 8px;\n border: var(--lb-picker-border-width) solid var(--lb-picker-border-color);\n border-radius: var(--lb-picker-radius);\n box-sizing: border-box;\n transition: border-color 0.25s ease, background 0.25s ease;\n}\n\n.lb-mix-match__modal-product-thumb {\n /* Fills the grid cell — the image sets the visual size now, not a fixed\n 60px thumb. Aspect-ratio + object-fit (below) crop to the merchant's\n pickerThumbnailRatio; \"original\" shows the full uncropped image. */\n width: 100%;\n /* Positioning context for the \"Added\" badge. */\n position: relative;\n /* Modal picker thumbs follow the merchant's pickerThumbnailRatio —\n independent from the main widget's thumbnailRatio so a merchant can\n e.g. show tall picker thumbs with square main thumbs. */\n aspect-ratio: var(--lb-picker-thumbnail-aspect-ratio, 1 / 1);\n border-radius: var(--lb-picker-radius);\n box-sizing: border-box;\n overflow: hidden;\n background: var(--lb-thumbnail-bg);\n margin-bottom: 10px;\n}\n\n.lb-mix-match__modal-product-thumb img {\n width: 100%;\n /* Height + fit come from pickerThumbnailRatio — \"original\" sets both\n to auto/contain so the image renders uncropped at intrinsic ratio. */\n height: var(--lb-picker-thumbnail-img-height, 100%);\n object-fit: var(--lb-picker-thumbnail-img-fit, cover);\n border-radius: var(--lb-picker-radius);\n}\n\n.lb-mix-match__modal-product-info {\n flex: 1;\n min-width: 0;\n}\n\n.lb-mix-match__modal-product-title {\n font-size: 16px;\n font-weight: 600;\n line-height: 20px;\n color: inherit;\n margin: 0;\n}\n\n/* Modal titles link to the product page (new tab) — quiet by default,\n underline on hover so the row's Add affordance stays dominant. */\n.lb-mix-match__modal-product-title a {\n color: inherit;\n /* Underline fades in — text-decoration itself can't transition, but its\n color can. */\n text-decoration: underline;\n text-decoration-color: transparent;\n transition: text-decoration-color 0.25s ease;\n}\n\n.lb-mix-match__modal-product-title a:hover {\n text-decoration-color: currentColor;\n}\n\n.lb-mix-match__modal-product-title a:focus-visible {\n outline: 2px solid var(--lb-primary-color);\n outline-offset: 2px;\n border-radius: 2px;\n}\n\n.lb-mix-match__modal-product-price {\n display: flex;\n align-items: baseline;\n gap: 6px;\n font-size: 14px;\n line-height: 20px;\n color: inherit;\n margin: 2px 0 0;\n}\n\n.lb-mix-match__modal-product-compare {\n /* --lb-product-compare-display fallback is the \"on\" branch; Liquid sets 'none' when merchant disables. */\n display: var(--lb-product-compare-display, inline);\n font-size: 13px;\n font-weight: 400;\n color: color-mix(in srgb, var(--lb-text) 55%, transparent);\n text-decoration: line-through;\n}\n\n.lb-mix-match__modal-product-compare[hidden] {\n display: none;\n}\n\n.lb-mix-match__modal-product-unit-price {\n font-size: 12px;\n line-height: 16px;\n color: color-mix(in srgb, var(--lb-text) 60%, transparent);\n margin: 2px 0 0;\n}\n\n.lb-mix-match__modal-product-unit-price[hidden] {\n display: none;\n}\n\n.lb-mix-match__variant-select {\n font-size: 12px;\n padding: 4px 24px 4px 8px;\n border: var(--lb-picker-border-width) solid var(--lb-picker-border-color);\n border-radius: var(--lb-picker-radius);\n background-color: var(--lb-picker-bg);\n background-image: var(--lb-picker-variant-chevron);\n background-repeat: no-repeat;\n background-position: right 6px center;\n background-size: 12px;\n color: var(--lb-picker-text);\n font-family: inherit;\n box-sizing: border-box;\n min-height: 40px;\n max-height: 40px;\n cursor: pointer;\n width: 100%;\n max-width: 100%;\n appearance: none;\n -webkit-appearance: none;\n}\n\n.lb-mix-match__variant-select:focus-visible {\n outline: 2px solid var(--lb-primary-color);\n outline-offset: 2px;\n box-shadow: none;\n}\n\n/* Per-pick quantity stepper inside the picker modal product row.\n Three flex cells (− / number / +) sharing a single outer border that\n inherits the modal \"Borders\" (--lb-picker-border-* / --lb-picker-radius),\n like every other control in the modal. The cell dividers come from a 1px\n border on the centre value rather than per-button borders, so the rounded\n outer corners stay clean. */\n/* Quantity label + stepper sit together in a .lb-bundle-variant-option-group\n wrapper so the label-to-stepper gap inherits the same 2px the variant\n pickers use. The wrapper carries the margin-top that spaces the qty\n group from the unit-price / low-stock-badge above; the stepper itself\n has no top margin so the group can be repositioned without coupling. */\n.lb-mix-match__qty-stepper-group {\n /* Override the base .lb-bundle-variant-option-group column flex so the\n stepper child stretches on the cross-axis (vertical) — this is what\n lets the group track the Add button's height in the action row\n without pinning a fixed pixel value. */\n flex-direction: row;\n width: fit-content;\n}\n\n.lb-mix-match__qty-stepper {\n display: inline-flex;\n align-items: center;\n justify-content: space-between;\n width: 100%;\n flex-shrink: 0;\n border: var(--lb-picker-border-width) solid var(--lb-picker-border-color);\n border-radius: var(--lb-picker-radius);\n background-color: var(--lb-picker-bg);\n overflow: hidden;\n box-sizing: border-box;\n /* Pin the height so the stepper and Add button stay equal-height even when\n they wrap onto separate rows on narrow (mobile) cards — without this the\n stepper loses its cross-axis stretch reference and shrinks. */\n min-height: 32px;\n max-height: 32px;\n}\n\n.lb-mix-match__qty-stepper-button {\n appearance: none;\n -webkit-appearance: none;\n background: transparent;\n border: none;\n margin: 0;\n padding: 0 4px;\n width: 24px;\n height: 24px;\n box-sizing: border-box;\n font-family: inherit;\n font-size: 16px;\n font-weight: 500;\n line-height: 1;\n color: var(--lb-picker-text);\n cursor: pointer;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n}\n\n.lb-mix-match__qty-stepper-button:focus-visible {\n outline: 2px solid var(--lb-primary-color);\n outline-offset: -2px;\n box-shadow: none;\n}\n\n.lb-mix-match__qty-stepper-button:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n}\n\n.lb-mix-match__qty-stepper-value {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 24px;\n height: 24px;\n font-family: inherit;\n font-size: 13px;\n font-weight: 500;\n color: var(--lb-picker-text);\n box-sizing: border-box;\n}\n\n/* Action row — qty stepper (left) + Add button (right). Sits at the\n bottom of the info column with a 12px top margin so it visually\n separates from the variant pickers / price block above. When the\n stepper is hidden, Add fills the row via flex: 1 below.\n align-items: stretch so the stepper-group tracks the Add button's\n height (driven by font size + button padding) without a pinned px. */\n.lb-mix-match__modal-product-actions {\n display: flex;\n flex-wrap: wrap;\n align-items: stretch;\n row-gap: 8px;\n column-gap: 8px;\n margin-top: 12px;\n}\n\n.lb-mix-match__modal-add {\n flex: 1;\n padding: 8px 16px;\n background: var(--lb-picker-add-bg);\n color: var(--lb-picker-add-label);\n border: var(--lb-picker-add-border-width) solid var(--lb-picker-add-border-color);\n border-radius: var(--lb-picker-radius);\n box-sizing: border-box;\n font-size: 12px;\n font-weight: 600;\n cursor: pointer;\n /* Matches the qty stepper's pinned height so the two stay equal-height\n whether they sit on one row or wrap onto two on narrow (mobile) cards. */\n min-height: 32px;\n max-height: 32px;\n transition: opacity 0.25s ease, background 0.25s ease, color 0.25s ease, border-color 0.25s ease;\n}\n\n.lb-mix-match__modal-add:hover:not(:disabled) {\n opacity: 0.9;\n}\n\n.lb-mix-match__modal-add:focus-visible {\n outline: 2px solid var(--lb-primary-color);\n outline-offset: 2px;\n box-shadow: none;\n}\n\n.lb-mix-match__modal-add:disabled {\n background: color-mix(in srgb, var(--lb-picker-add-bg) 35%, var(--lb-picker-bg));\n color: color-mix(in srgb, var(--lb-picker-add-label) 85%, transparent);\n cursor: not-allowed;\n}\n\n/* In-bundle \"Remove\" — the Add button's colours INVERTED (label and\n background swap), no icons. The base :hover (opacity 0.9) applies as-is;\n state feedback lives on the thumbnail's \"Added\" badge, not the button. */\n.lb-mix-match__modal-add--added {\n background: var(--lb-picker-add-label);\n color: var(--lb-picker-add-bg);\n}\n\n/* Required-locked row — the Remove slot holds an inert quiet \"Required\"\n label (button disabled); the thumbnail's Added badge still carries the\n in-bundle state. Removal returns once another slot covers the product's\n minimum (variant swap flow). */\n.lb-mix-match__modal-add--added.lb-mix-match__modal-add--required {\n background: transparent;\n border-color: transparent;\n color: color-mix(in srgb, var(--lb-picker-text) 60%, transparent);\n cursor: default;\n}\n\n.lb-mix-match__modal-add--added.lb-mix-match__modal-add--required:hover {\n opacity: 1;\n}\n\n/* In-bundle card — border only, a touch darker than the resting border;\n the thumbnail badge carries the \"Added\" state (no background tint). */\n.lb-mix-match__modal-product--in-bundle {\n border-color: color-mix(in srgb, var(--lb-picker-text) 30%, transparent);\n}\n\n/* \"Added\" badge — top-right of the thumbnail, in the Add button's colours;\n pure state (pointer-events: none), the row's Remove button acts. */\n.lb-mix-match__modal-added-badge {\n position: absolute;\n top: 6px;\n right: 6px;\n background: var(--lb-picker-add-bg);\n color: var(--lb-picker-add-label);\n font-size: 11px;\n font-weight: 600;\n line-height: 1;\n padding: 8px 16px;\n border-radius: var(--lb-picker-radius-sm);\n pointer-events: none;\n}\n\n/* Sold out product row */\n.lb-mix-match__modal-product--sold-out {\n opacity: 0.5;\n}\n\n.lb-mix-match__modal-product--sold-out .lb-mix-match__modal-sold-out-label {\n font-size: 12px;\n color: inherit;\n font-weight: 500;\n white-space: nowrap;\n}\n\n/* \"Needs N spots\" hint — exact-size enforcement: the product's minimum\n pick exceeds the bundle's remaining open spots, so its Add and stepper\n are disabled. Same quiet label treatment as the sold-out state. */\n.lb-mix-match__modal-needs-spots {\n display: block;\n margin-top: 8px;\n font-size: 12px;\n color: inherit;\n font-weight: 500;\n white-space: nowrap;\n opacity: 0.7;\n}\n\n/* === Modal Empty State === */\n.lb-mix-match__modal-empty {\n padding: 32px 20px;\n text-align: center;\n}\n\n.lb-mix-match__modal-empty p {\n margin: 0;\n font-size: 16px;\n color: var(--lb-picker-text);\n}\n\n/* === Modal Footer === */\n.lb-mix-match__modal-footer {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n padding: 16px 20px;\n border-top: 1px solid color-mix(in srgb, var(--lb-picker-text) 15%, transparent);\n flex-shrink: 0;\n}\n\n.lb-mix-match__modal-footer-count {\n font-size: 13px;\n font-weight: 500;\n color: color-mix(in srgb, var(--lb-picker-text) 70%, transparent);\n}\n\n/* \"Done\" reuses the picker Add-button styling so it matches the Add buttons.\n The flex centring + min-height pin the footer-button silhouette: the wizard\n Back (bundle-multi-step.css) copies these metrics so the pair stays\n equal-height whatever border widths the merchant's picker tokens set. */\n.lb-mix-match__modal-done {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-height: 40px;\n padding: 10px 24px;\n transition: opacity 0.25s ease;\n background: var(--lb-picker-add-bg);\n color: var(--lb-picker-add-label);\n border: var(--lb-picker-add-border-width) solid var(--lb-picker-add-border-color);\n border-radius: var(--lb-picker-radius);\n box-sizing: border-box;\n font-family: inherit;\n font-size: 14px;\n font-weight: 600;\n cursor: pointer;\n}\n\n.lb-mix-match__modal-done:hover:not(:disabled) {\n opacity: 0.9;\n}\n\n.lb-mix-match__modal-done:focus-visible {\n outline: 2px solid var(--lb-primary-color);\n outline-offset: 2px;\n box-shadow: none;\n}\n\n/* Disabled Next/Done (wizard step minimum not met) — same washed-out\n treatment as a disabled row Add, so an unfinished step reads at a\n glance. Hover is gated off above; the solid button returns the moment\n the requirement is met. */\n.lb-mix-match__modal-done:disabled {\n background: color-mix(in srgb, var(--lb-picker-add-bg) 35%, var(--lb-picker-bg));\n color: color-mix(in srgb, var(--lb-picker-add-label) 85%, transparent);\n cursor: not-allowed;\n}\n\n/* Hidden utility for search filtering */\n.lb-hidden {\n display: none !important;\n}\n\n/* === Desktop: stepper group + Add button share the action row equally === */\n@media (min-width: 768px) {\n .lb-mix-match__qty-stepper-group {\n flex: 1;\n }\n}\n\n/* === Mobile Full-Screen Modal === */\n@media (max-width: 767px) {\n .lb-mix-match__modal-overlay {\n align-items: flex-end;\n }\n\n .lb-mix-match__modal {\n max-width: 100%;\n max-height: 90vh;\n /* Bottom sheet: only the top corners round, but they round to whatever the\n merchant picked rather than a fixed 16px (issue #363). */\n border-radius: var(--lb-picker-radius) var(--lb-picker-radius) 0 0;\n transform: translateY(100%);\n }\n\n .lb-mix-match__modal-overlay--open .lb-mix-match__modal {\n transform: translateY(0);\n }\n\n /* Touch targets: bump the picker controls to ~44px on touch devices.\n The stepper and Add button stay equal-height (both pinned to 44px). */\n .lb-mix-match__variant-select {\n width: 100%;\n max-width: 100%;\n min-height: 44px;\n max-height: none;\n }\n\n .lb-mix-match__qty-stepper {\n min-height: 44px;\n max-height: 44px;\n }\n\n .lb-mix-match__qty-stepper-button {\n width: 44px;\n height: 44px;\n }\n\n .lb-mix-match__modal-add {\n min-height: 44px;\n max-height: 44px;\n }\n\n /* Full-width qty stepper on mobile — once it wraps below the Add button,\n stretch it to the row and space the −/value/+ across it so it mirrors\n the full-width button instead of staying a compact control. */\n .lb-mix-match__qty-stepper-group {\n width: 100%;\n }\n}\n\n/* Small phones: a 2-up card grid leaves each picker card cramped (image +\n variant selects + stepper + Add at ~148px wide). Drop to a single column\n below 430px; larger phones and up keep the 2-up grid. */\n@media (max-width: 430px) {\n .lb-mix-match__modal-list {\n grid-template-columns: 1fr;\n }\n}\n\n/* === Reduced Motion === */\n@media (prefers-reduced-motion: reduce) {\n .lb-mix-match__modal-overlay,\n .lb-mix-match__modal,\n .lb-mix-match__progress-segment,\n .lb-mix-match__progress-segment::after {\n transition: none;\n }\n}\n`;\nexport const BUNDLE_VOLUME_CSS = `/* Lime Bundles — Volume / Quantity Breaks styles */\n\n.lb-volume__tiers {\n display: flex;\n flex-direction: column;\n gap: 16px;\n max-height: var(--lb-list-max-height);\n overflow-y: auto;\n /* Scrollbar hidden + edge fade — see .lb-edge-fade in bundle-base.css. */\n}\n\n/* Reserve room for the floating \"Most popular\" badge ONLY when the first tier\n is the popular one — that's the single case where the badge has no card above\n it to float into and would otherwise clip at the scroll container's top. */\n.lb-volume__tiers:has(> .lb-volume__tier:first-child .lb-volume__tier-badge) {\n padding-top: 12px;\n}\n\n/* Tier card — mirrors the fixed / mix & match bundle card so volume reads as\n the same app: a bordered card with matching geometry, a price row, and a\n right-slot chip. The radio sits where those cards put the thumbnail, since a\n volume tier is the same product at a different quantity (no thumbnail). */\n.lb-volume__tier {\n position: relative;\n display: flex;\n align-items: center;\n gap: 12px;\n border: var(--lb-border-width) solid var(--lb-border);\n border-radius: var(--lb-radius);\n padding: 12px;\n /* border-box so the selected tier's thicker (2px) border grows inward and\n doesn't shift the card by 1px when selected. */\n box-sizing: border-box;\n cursor: pointer;\n transition: border-color 0.25s ease, background 0.25s ease;\n}\n\n.lb-volume__tier:hover {\n border-color: color-mix(in srgb, var(--lb-text) 28%, transparent);\n}\n\n/* Suppress the UA default focus outline + Dawn's focus shadow so a freshly\n clicked tier doesn't briefly show the focus ring on top of the selected\n state. Keyboard focus is still indicated by the :focus-visible rule. */\n.lb-volume__tier:focus {\n outline: none;\n box-shadow: none;\n}\n\n.lb-volume__tier:focus-visible:not([aria-checked=\"true\"]) {\n outline: 2px solid var(--lb-primary-color);\n outline-offset: -2px;\n box-shadow: none;\n}\n\n/* Selected tier — the same quiet neutral highlight as the mix & match\n in-bundle / Added card (soft tint + stronger border), plus the filled\n radio, so \"selected\" looks the same everywhere in the app. */\n.lb-volume__tier[aria-checked=\"true\"] {\n border-color: var(--lb-text);\n border-width: 2px;\n}\n\n.lb-volume__radio {\n width: 20px;\n height: 20px;\n min-width: 20px;\n flex: none;\n box-sizing: border-box;\n border: 2px solid color-mix(in srgb, var(--lb-text) 45%, transparent);\n border-radius: 50%;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: border-color 0.25s ease;\n}\n\n.lb-volume__tier[aria-checked=\"true\"] .lb-volume__radio {\n border-color: var(--lb-text);\n}\n\n.lb-volume__radio-dot {\n width: 10px;\n height: 10px;\n border-radius: 50%;\n background: transparent;\n transition: background 0.25s ease;\n}\n\n.lb-volume__tier[aria-checked=\"true\"] .lb-volume__radio-dot {\n background: var(--lb-text);\n}\n\n/* Info column: the quantity over the price row. */\n.lb-volume__tier-info {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n}\n\n.lb-volume__tier-label {\n font-size: 16px;\n font-weight: 600;\n line-height: 20px;\n color: var(--lb-text);\n}\n\n/* Right column — stacks the \"Most popular\" badge (when present) over the\n savings chip, so the markers group on the right and the quantity row stays\n clean. */\n.lb-volume__tier-right {\n flex: none;\n display: flex;\n flex-direction: column;\n align-items: flex-end;\n gap: 6px;\n}\n\n/* \"Most popular\" pill — a floating crown straddling the card's top-right edge.\n The 16px gap on .lb-volume__tiers leaves room for it above each card. */\n.lb-volume__tier-badge {\n position: absolute;\n top: -12px;\n right: 12px;\n z-index: 1;\n font-size: 12px;\n font-weight: 500;\n line-height: 1;\n color: var(--lb-popular-badge-text);\n background: var(--lb-popular-badge-bg);\n border: var(--lb-popular-badge-border-width) solid var(--lb-popular-badge-border-color);\n border-radius: var(--lb-radius-sm);\n padding: 4px 9px;\n}\n\n/* Price row — the same struck → per-unit → \"each\" treatment the product cards\n use (compare-at muted and struck, per-unit price, tabular figures). */\n.lb-volume__tier-price {\n display: flex;\n align-items: baseline;\n gap: 6px;\n flex-wrap: wrap;\n margin-top: 4px;\n font-size: 14px;\n color: var(--lb-text);\n font-variant-numeric: tabular-nums;\n}\n\n.lb-volume__tier-compare {\n color: color-mix(in srgb, var(--lb-text) 55%, transparent);\n text-decoration: line-through;\n}\n\n.lb-volume__tier-unit {\n font-size: 12px;\n color: color-mix(in srgb, var(--lb-text) 60%, transparent);\n}\n\n/* Savings chip — the same soft neutral chip as the fixed card's quantity chip,\n here carrying the per-tier discount so \"buy more, save more\" is scannable. */\n.lb-volume__tier-savings {\n flex-shrink: 0;\n display: inline-flex;\n align-items: center;\n height: 28px;\n padding: 0 8px;\n border-radius: var(--lb-radius-sm);\n background: color-mix(in srgb, var(--lb-text) 7%, transparent);\n color: color-mix(in srgb, var(--lb-text) 75%, transparent);\n font-size: 13px;\n font-weight: 500;\n line-height: 1;\n white-space: nowrap;\n font-variant-numeric: tabular-nums;\n}\n\n/* Tier the selected variant can't cover — same greyed treatment as an\n out-of-stock product row, and not selectable. */\n.lb-volume__tier--oos {\n opacity: 0.5;\n cursor: not-allowed;\n}\n`;\n\nexport const BUNDLE_BOGO_CSS = `/* Lime Bundles — BOGO (Buy X Get Y) bundle styles */\n\n.lb-bogo__products {\n display: flex;\n flex-direction: column;\n gap: 12px;\n margin: 0;\n}\n\n/* Buy/get product cards — same bordered-card construction as fixed rows so\n the two bundle types read the same on a storefront. */\n.lb-bogo .lb-bundle-product-row {\n display: flex;\n align-items: flex-start;\n gap: 12px;\n padding: 8px;\n border: var(--lb-border-width) solid var(--lb-border);\n border-radius: var(--lb-radius);\n box-sizing: border-box;\n}\n\n.lb-bogo .lb-bundle-thumbnail {\n width: 60px;\n min-width: 60px;\n border-radius: var(--lb-radius);\n box-sizing: border-box;\n}\n\n.lb-bogo .lb-bundle-thumbnail img {\n border-radius: var(--lb-radius);\n border: none;\n}\n\n/* The \"+\" divider between the buy and get cards — a small accent medallion\n so the offer's hinge (\"this plus that\") stands out. Circular by design:\n it is a connector dot, not a card control, so it sits outside the\n --lb-radius system (blessed in WIDGET-DESIGN.md). */\n.lb-bogo__plus {\n align-self: center;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 24px;\n height: 24px;\n border-radius: 50%;\n background: var(--lb-primary-color);\n color: var(--lb-btn-text);\n}\n\n/* Reward badge on the get card: \"Free\" or \"P% off\". Neutral chip colors\n (right-slot chip family) — the accent stays reserved for the CTA and the\n plus medallion; 600 weight carries the reward emphasis. */\n.lb-bogo__badge {\n display: inline-flex;\n align-items: center;\n align-self: flex-start;\n flex-shrink: 0;\n height: 28px;\n padding: 0 8px;\n border-radius: var(--lb-radius-sm);\n background: color-mix(in srgb, var(--lb-text) 7%, transparent);\n color: color-mix(in srgb, var(--lb-text) 75%, transparent);\n font-size: 13px;\n font-weight: 600;\n line-height: 1;\n white-space: nowrap;\n}\n\n/* Struck-through original price on the get card when the reward applies. */\n.lb-bogo .lb-bundle-product-compare-price {\n text-decoration: line-through;\n opacity: 0.6;\n}\n`;\n\nexport const BUNDLE_MULTI_STEP_CSS = `/**\n * Lime Bundles — Multi-step bundle widget styles (wizard chrome ONLY).\n *\n * The multi-step widget reuses the mix & match classes wholesale —\n * bundle-widget.liquid emits bundle-mix-match.css whenever has_multi_step\n * is set. This file carries only what the wizard adds on top: step groups\n * in the widget body, the group heading + count chip, and the modal's\n * three-slot Back / count / Next-Done footer.\n *\n * Canon: WIDGET-DESIGN.md (spacing scale, radius system, chip family).\n */\n\n/* ── Step groups (widget body) ────────────────────────────────────\n One dashed door + one section per step. A step without picks shows a\n slim checklist row; a step with picks shows the heading + chosen\n cards. data-step-mode (\"row\" | \"open\") on the section picks the\n representation — both are in the markup, CSS hides the other, so the\n server-rendered untouched state IS the hydrated no-selection state.\n Spacing is per-pair rather than a flat gap: consecutive rows sit\n flush (hairline-separated), everything else breathes 16px. */\n\n.lb-multi-step__groups {\n display: flex;\n flex-direction: column;\n gap: 0;\n}\n\n.lb-multi-step__group {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n.lb-multi-step__group + .lb-multi-step__group {\n margin-top: 16px;\n}\n\n/* A run of empty-step rows reads as one checklist: flush, hairlines\n between (the first row of a run keeps no border). */\n.lb-multi-step__group[data-step-mode=\"row\"] + .lb-multi-step__group[data-step-mode=\"row\"] {\n margin-top: 0;\n}\n\n.lb-multi-step__group[data-step-mode=\"row\"]\n + .lb-multi-step__group[data-step-mode=\"row\"]\n .lb-multi-step__step-row {\n border-top: var(--lb-border-width) solid var(--lb-border);\n}\n\n/* Mode switch: a row-mode step hides its open chrome and vice versa. */\n.lb-multi-step__group[data-step-mode=\"row\"] .lb-multi-step__group-heading,\n.lb-multi-step__group[data-step-mode=\"row\"] .lb-multi-step__group-slots {\n display: none;\n}\n\n.lb-multi-step__group[data-step-mode=\"open\"] .lb-multi-step__step-row {\n display: none;\n}\n\n/* Checklist row — the empty step's whole representation, and a tap\n target that deep-links the wizard to that step. Quiet by design: the\n door above it is the primary action. */\n.lb-multi-step__step-row {\n display: flex;\n align-items: center;\n gap: 12px;\n width: 100%;\n min-height: 48px;\n padding: 10px 2px;\n background: none;\n border: none;\n font-family: inherit;\n text-align: left;\n cursor: pointer;\n box-sizing: border-box;\n transition: background-color 0.25s ease;\n /* Deliberately no border-radius. This row paints the checklist hairline as\n its own border-top (see the run rule above), and a radius tapers that\n border into the corner arcs — the dividers came out visibly curved at the\n \"round\" preset (issue #363). A full-bleed list row is meant to be square\n anyway; the hover tint below spans the card width, so it has no corner to\n round against. The :focus-visible radius is fine: an outline is a\n separate paint and never touches the border. */\n}\n\n.lb-multi-step__step-row:hover {\n background: color-mix(in srgb, var(--lb-text) 3%, transparent);\n}\n\n.lb-multi-step__step-row:focus-visible {\n outline: 2px solid var(--lb-primary-color);\n outline-offset: -2px;\n border-radius: var(--lb-radius-sm);\n}\n\n.lb-multi-step__step-row .lb-multi-step__group-count {\n margin-left: auto;\n}\n\n/* Step number — quiet index, tabular so multi-digit lists align. */\n.lb-multi-step__step-ix {\n width: 18px;\n min-width: 18px;\n font-size: 12px;\n font-weight: 500;\n color: color-mix(in srgb, var(--lb-text) 45%, transparent);\n font-variant-numeric: tabular-nums;\n}\n\n/* The door — the single wizard entry point, reusing the dashed\n add-product recipe with a two-line label. JS moves it directly above\n the first unsatisfied step and hides it once every step is met. The\n add-product recipe's own margin-top is for its mix & match position\n (below the slot list); here the pair rules own all spacing. */\n.lb-multi-step__door {\n margin-top: 0;\n}\n\n.lb-multi-step__door + .lb-multi-step__group {\n margin-top: 12px;\n}\n\n.lb-multi-step__door + .lb-multi-step__group[data-step-mode=\"row\"] {\n margin-top: 4px;\n}\n\n.lb-multi-step__group + .lb-multi-step__door {\n margin-top: 16px;\n}\n\n/* A hidden door is parked as the container's first child (updateDoor), so\n the group after it is really the first visible element — no door gap. */\n.lb-multi-step__door--hidden:first-child + .lb-multi-step__group {\n margin-top: 0;\n}\n\n.lb-multi-step__door-copy {\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.lb-multi-step__door-sub {\n font-size: 13px;\n font-weight: 400;\n line-height: 1.2;\n color: color-mix(in srgb, var(--lb-text) 55%, transparent);\n}\n\n/* Heading is a quiet, non-interactive row: step name left, live count\n chip right. The step's chosen cards below are the edit targets. */\n.lb-multi-step__group-heading {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n color: var(--lb-text);\n}\n\n.lb-multi-step__group-name {\n font-size: 14px;\n font-weight: 600;\n line-height: 1.4;\n}\n\n/* Count chip — right-slot chip family (28px, 0 8px, radius-sm, 13/500,\n neutral 7%/75% fill). Weight-only emphasis when the step is met. */\n.lb-multi-step__group-count {\n display: inline-flex;\n align-items: center;\n height: 28px;\n padding: 0 8px;\n border-radius: var(--lb-radius-sm);\n background: color-mix(in srgb, var(--lb-text) 7%, transparent);\n color: color-mix(in srgb, var(--lb-text) 75%, transparent);\n font-size: 13px;\n font-weight: 500;\n white-space: nowrap;\n}\n\n.lb-multi-step__group-count--met {\n font-weight: 600;\n}\n\n/* Empty step group: the slots container collapses so the heading rows\n read as a compact checklist until picks land. */\n.lb-multi-step__group-slots:empty {\n display: none;\n}\n\n/* ── Wizard modal footer: [Back] [count] [Next|Done] ───────────\n Three-column grid so the count stays dead-centre whether or not the\n Back button is showing (Next and Done share the right cell — only one\n is visible at a time). */\n.lb-multi-step__modal-footer {\n display: grid;\n grid-template-columns: 1fr auto 1fr;\n align-items: center;\n gap: 12px;\n}\n\n.lb-multi-step__modal-footer > .lb-multi-step__modal-back {\n grid-area: 1 / 1;\n justify-self: start;\n}\n\n.lb-multi-step__modal-footer > .lb-mix-match__modal-footer-count {\n grid-area: 1 / 2;\n text-align: center;\n}\n\n.lb-multi-step__modal-footer > .lb-mix-match__modal-done {\n grid-area: 1 / 3;\n justify-self: end;\n}\n\n/* Back — secondary (outlined) treatment beside the primary Next/Done\n (which reuse .lb-mix-match__modal-done). Same silhouette as Next —\n metrics copied from the modal-done recipe (padding, type, radius,\n min-height) — and picker tokens only: the modal is portaled out of the\n widget, so the widget vars (--lb-text, --lb-border-*) it previously\n used don't reliably resolve here and break palette isolation. */\n.lb-multi-step__modal-back {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-height: 40px;\n padding: 10px 24px;\n background: none;\n border: var(--lb-picker-border-width) solid var(--lb-picker-border-color);\n border-radius: var(--lb-picker-radius);\n box-sizing: border-box;\n color: var(--lb-picker-text);\n font-family: inherit;\n font-size: 14px;\n font-weight: 600;\n cursor: pointer;\n transition: background-color 0.25s ease;\n}\n\n.lb-multi-step__modal-back:hover {\n background: color-mix(in srgb, var(--lb-picker-text) 5%, transparent);\n}\n\n.lb-multi-step__modal-back:focus-visible {\n outline: 2px solid var(--lb-primary-color);\n outline-offset: 2px;\n}\n\n/* ── Step-change transition ────────────────────────────────────\n Moving between steps swaps the whole step-scoped region in a single\n paint, which reads as nothing happening (or as a glitch) rather than\n as arriving somewhere new — and worst of all under auto-advance,\n where the shopper never clicked anything. So the step assembles\n instead of arriving whole: the step label leads, the filter pills\n follow, then the product cards one after another. Staging is what\n makes the change legible; a uniform slide of everything at once only\n wobbles the card.\n\n The wizard stamps data-step-transition=\"forward\" | \"back\" on the\n dialog on every step change (manual Next/Back and auto-advance\n alike, never on open, where the modal entrance already carries the\n motion), and clears it again once the cascade has played — see\n STEP_CASCADE_MS in multi-step/wizard.ts. That clear is load-bearing,\n not tidiness: search and filter hide cards with .lb-hidden\n (display: none), and restoring display restarts a card's animation,\n so a live attribute would replay the whole cascade on every\n keystroke that reveals a card.\n\n Two elements deliberately hold still. The footer, because Back/Next\n are the shopper's anchor while everything above them moves. And the\n progress rail, because it is the one thing that answers \"where am I\n now\" — animating the shopper's only orientation cue is what made the\n previous version hard to read. Its segment fill carries its own\n state change already. */\n@keyframes lb-multi-step-cascade-fwd {\n from {\n opacity: 0;\n transform: translateY(10px);\n }\n}\n\n@keyframes lb-multi-step-cascade-back {\n from {\n opacity: 0;\n transform: translateY(-10px);\n }\n}\n\n.lb-mix-match__modal[data-step-transition] .lb-mix-match__modal-subtitle {\n animation: lb-multi-step-cascade-fwd 0.2s ease-out both;\n}\n\n.lb-mix-match__modal[data-step-transition] .lb-mix-match__filters {\n animation: lb-multi-step-cascade-fwd 0.2s ease-out 0.05s both;\n}\n\n/* Cards stagger 30ms apart, but only the first four: a step can hold\n dozens of products and the two-column grid shows about four at a\n time, so past that the stagger is below the fold and would only add\n lag. Everything from the fifth on shares the fourth's landing. */\n.lb-mix-match__modal[data-step-transition] .lb-mix-match__modal-list > * {\n animation: lb-multi-step-cascade-fwd 0.22s ease-out 0.2s both;\n}\n\n.lb-mix-match__modal[data-step-transition] .lb-mix-match__modal-list > :nth-child(1) {\n animation-delay: 0.09s;\n}\n\n.lb-mix-match__modal[data-step-transition] .lb-mix-match__modal-list > :nth-child(2) {\n animation-delay: 0.12s;\n}\n\n.lb-mix-match__modal[data-step-transition] .lb-mix-match__modal-list > :nth-child(3) {\n animation-delay: 0.15s;\n}\n\n.lb-mix-match__modal[data-step-transition] .lb-mix-match__modal-list > :nth-child(4) {\n animation-delay: 0.18s;\n}\n\n/* Going back runs the same cascade in the same order — only the offset\n flips, so the shopper still reads which way they moved. Overrides\n the name alone; the staggered delays above must survive. */\n.lb-mix-match__modal[data-step-transition=\"back\"] .lb-mix-match__modal-subtitle,\n.lb-mix-match__modal[data-step-transition=\"back\"] .lb-mix-match__filters,\n.lb-mix-match__modal[data-step-transition=\"back\"] .lb-mix-match__modal-list > * {\n animation-name: lb-multi-step-cascade-back;\n}\n\n@keyframes lb-multi-step-cascade-fade {\n from {\n opacity: 0;\n }\n}\n\n/* Reduced motion keeps the fade and drops the travel and the stagger:\n the step change still has to be perceptible, which is the whole\n point of this block. The card selector is :nth-child(n) rather than\n * on purpose — a plain * loses to the :nth-child(1..4) delays above\n on specificity, which would leave the stagger running with nothing\n moving. :nth-child(n) matches every child at equal specificity and\n wins on order. */\n@media (prefers-reduced-motion: reduce) {\n .lb-mix-match__modal[data-step-transition] .lb-mix-match__modal-subtitle,\n .lb-mix-match__modal[data-step-transition] .lb-mix-match__filters,\n .lb-mix-match__modal[data-step-transition] .lb-mix-match__modal-list > :nth-child(n) {\n animation: lb-multi-step-cascade-fade 0.15s ease-out both;\n }\n}\n`;\n\nexport const BUNDLE_DROPDOWN_CSS = `/**\n * Lime Bundles — Custom variant-picker dropdown styling.\n *\n * Reuses existing CSS variables: no new merchant-configurable surface.\n * --lb-border-width, --lb-border (global border), --lb-radius-sm (derived),\n * --lb-variant-chevron, --lb-bg, --lb-text, --lb-primary-color\n *\n * Mix-match modal context overrides via .lb-mix-match__modal scope to use\n * --lb-picker-variant-* and --lb-picker-bg.\n */\n\n/* Hide the native <select> while keeping it form-serializable and focusable\n programmatically. The .lb-dropdown-state marker is added by JS at bind\n time, so this rule matches every variant-select class (main widget,\n mix-match modal, future bundle types). aria-hidden + tabindex=-1\n (also set in JS) remove it from the accessibility tree. */\n.lb-dropdown-state {\n position: absolute !important;\n width: 1px !important;\n height: 1px !important;\n padding: 0 !important;\n margin: -1px !important;\n overflow: hidden !important;\n clip-path: inset(50%) !important;\n white-space: nowrap !important;\n border: 0 !important;\n pointer-events: none !important;\n}\n\n/* Shell fills its parent column. */\n.lb-dropdown {\n position: relative;\n display: inline-block;\n width: 100%;\n max-width: 100%;\n font-family: inherit;\n}\n\n/* Trigger styled identically to the closed-state native select */\n.lb-dropdown-trigger {\n display: inline-flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n width: 100%;\n border: var(--lb-border-width) solid var(--lb-border);\n border-radius: var(--lb-radius);\n padding: 8px 12px;\n font-size: 12px;\n line-height: 16px;\n color: var(--lb-text);\n background: var(--lb-bg);\n font-family: inherit;\n cursor: pointer;\n appearance: none;\n -webkit-appearance: none;\n text-align: start;\n transition: border-color 0.25s ease;\n}\n\n.lb-dropdown-trigger:focus-visible {\n outline: 2px solid var(--lb-primary-color);\n outline-offset: 2px;\n box-shadow: none;\n}\n\n.lb-dropdown-trigger[aria-expanded=\"true\"] {\n border-color: var(--lb-text);\n}\n\n.lb-dropdown-trigger-value {\n flex: 1 1 auto;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n text-align: start;\n}\n\n.lb-dropdown-chevron {\n flex: 0 0 auto;\n width: 12px;\n height: 12px;\n background: var(--lb-variant-chevron) center / contain no-repeat;\n transition: transform 0.25s ease;\n}\n\n.lb-dropdown-trigger[aria-expanded=\"true\"] .lb-dropdown-chevron {\n transform: rotate(180deg);\n}\n\n/* Popover panel — position: absolute against the .lb-dropdown shell\n (already position: relative). Top/left/width come from CSS so we\n never depend on JS having set inline coords by the time the panel\n becomes visible. JS only sets max-height. */\n.lb-dropdown-listbox {\n position: absolute;\n left: 0;\n /* Default to below-trigger placement so the panel doesn't overlap the\n trigger if data-placement is missing for any reason. The explicit\n [data-placement=\"down\"|\"up\"] rules below override this. */\n top: calc(100% + 4px);\n width: 100%;\n z-index: 9999;\n margin: 0;\n padding: 4px 0;\n list-style: none;\n background: var(--lb-bg);\n color: var(--lb-text);\n border: var(--lb-border-width) solid var(--lb-border);\n border-radius: var(--lb-radius);\n box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);\n overflow-y: auto;\n overflow-x: hidden;\n /* Quiet overlay scrollbar — transparent track, soft rounded thumb. The\n popover has a shadow + border, so it keeps a thumb rather than the inset\n lists' edge fade (a mask would clip the shadow). */\n scrollbar-width: thin;\n scrollbar-color: color-mix(in srgb, var(--lb-text) 18%, transparent) transparent;\n animation: lb-dropdown-in-down 120ms ease-out;\n transform-origin: top center;\n}\n\n.lb-dropdown-listbox[data-placement=\"down\"] {\n top: calc(100% + 4px);\n}\n\n.lb-dropdown-listbox[data-placement=\"up\"] {\n top: auto;\n bottom: calc(100% + 4px);\n animation-name: lb-dropdown-in-up;\n transform-origin: bottom center;\n}\n\n/* When portaled out of the .lb-dropdown shell (mix-match modal context:\n .lb-mix-match__modal applies translateY which would otherwise trap\n position:fixed), switch to fixed and let JS set viewport coords. */\n.lb-dropdown-listbox[data-lb-dropdown-portal] {\n position: fixed;\n top: auto;\n left: auto;\n bottom: auto;\n width: auto;\n}\n\n/* Quiet overlay scrollbar — Webkit/Blink: transparent track, soft rounded\n thumb that darkens on hover. */\n.lb-dropdown-listbox::-webkit-scrollbar {\n width: 6px;\n}\n.lb-dropdown-listbox::-webkit-scrollbar-track {\n background: transparent;\n}\n.lb-dropdown-listbox::-webkit-scrollbar-thumb {\n background: color-mix(in srgb, var(--lb-text) 16%, transparent);\n border-radius: 999px;\n}\n.lb-dropdown-listbox::-webkit-scrollbar-thumb:hover {\n background: color-mix(in srgb, var(--lb-text) 30%, transparent);\n}\n\n/* Options */\n.lb-dropdown-option {\n padding: 8px 12px;\n font-size: 12px;\n line-height: 16px;\n cursor: pointer;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n color: var(--lb-text);\n /* The active/hover tint below fills the option, so it rounds with the rest\n of the widget rather than squaring off against the listbox corner it sits\n in (issue #363). */\n border-radius: var(--lb-radius-sm);\n}\n\n.lb-dropdown-option[aria-selected=\"true\"] {\n font-weight: 600;\n}\n\n.lb-dropdown-option.is-active,\n.lb-dropdown-option:hover:not([aria-disabled=\"true\"]) {\n background: color-mix(in srgb, var(--lb-text) 2%, transparent);\n}\n\n.lb-dropdown-option[aria-disabled=\"true\"] {\n opacity: 0.4;\n cursor: not-allowed;\n}\n\n/* Animations */\n@keyframes lb-dropdown-in-down {\n from { opacity: 0; transform: translateY(-4px) scale(0.98); }\n to { opacity: 1; transform: translateY(0) scale(1); }\n}\n\n@keyframes lb-dropdown-in-up {\n from { opacity: 0; transform: translateY(4px) scale(0.98); }\n to { opacity: 1; transform: translateY(0) scale(1); }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .lb-dropdown-listbox { animation: none; }\n .lb-dropdown-chevron { transition: none; }\n .lb-dropdown-trigger { transition: none; }\n}\n\n/* Forced-colors mode (Windows high-contrast) */\n@media (forced-colors: active) {\n .lb-dropdown-trigger {\n border-color: ButtonBorder;\n color: ButtonText;\n background: ButtonFace;\n }\n .lb-dropdown-listbox {\n border-color: ButtonBorder;\n background: Canvas;\n color: CanvasText;\n }\n .lb-dropdown-option.is-active {\n background: Highlight;\n color: HighlightText;\n }\n}\n\n/* Mix-match modal context — use picker-scoped variables.\n No CSS fallbacks: --lb-picker-* are always emitted by bundle-widget.liquid\n because WidgetConfig.parse() fully hydrates the merchant config.\n See docs/solutions/ui-bugs/widget-css-single-source-defaults.md. */\n.lb-mix-match__modal .lb-dropdown-trigger {\n border-color: var(--lb-picker-border-color);\n border-width: var(--lb-picker-border-width);\n border-radius: var(--lb-picker-radius);\n background: var(--lb-picker-bg);\n color: var(--lb-picker-text);\n /* Match the picker qty stepper + Add button height (border-box so 32px is\n the full height including padding + border). */\n box-sizing: border-box;\n padding: 4px 8px;\n min-height: 32px;\n max-height: 32px;\n}\n\n.lb-mix-match__modal .lb-dropdown-chevron {\n background-image: var(--lb-picker-variant-chevron);\n}\n\n/* Listbox is portaled out of the transformed .lb-mix-match__modal up to\n its [data-modal-overlay] parent, so picker-scoped rules anchor on the\n overlay attribute, not the modal class. */\n[data-modal-overlay] > .lb-dropdown-listbox {\n border-color: var(--lb-picker-border-color);\n border-width: var(--lb-picker-border-width);\n border-radius: var(--lb-picker-radius);\n background: var(--lb-picker-bg);\n color: var(--lb-picker-text);\n scrollbar-color: color-mix(in srgb, var(--lb-picker-text) 15%, transparent)\n color-mix(in srgb, var(--lb-picker-text) 2%, transparent);\n}\n\n[data-modal-overlay] > .lb-dropdown-listbox .lb-dropdown-option {\n border-radius: var(--lb-picker-radius-sm);\n}\n\n[data-modal-overlay] > .lb-dropdown-listbox::-webkit-scrollbar-track {\n background: color-mix(in srgb, var(--lb-picker-text) 2%, transparent);\n border-radius: 2px;\n}\n[data-modal-overlay] > .lb-dropdown-listbox::-webkit-scrollbar-thumb {\n background: color-mix(in srgb, var(--lb-picker-text) 15%, transparent);\n}\n`;\n\n/**\n * Skeleton styles for the web component's loading state. Rendered\n * synchronously in connectedCallback → renderLoading() so the host\n * element has intrinsic size from render-0 and doesn't shift layout\n * when the real bundle paints. Web-component only (see file header).\n */\nexport const BUNDLE_SKELETON_CSS = `/* Lime Bundles — web-component loading skeleton (not mirrored to theme assets) */\n\n.lb-bundle-widget--loading {\n display: block;\n padding: var(--lb-widget-pad, 20px);\n border: 1px solid var(--lb-border, #E5E5E5);\n border-radius: var(--lb-radius, 12px);\n background: var(--lb-bg, #FFFFFF);\n /* Contain layout/paint so the skeleton doesn't influence ancestor\n layout once the real content swaps in. */\n contain: layout paint;\n}\n\n.lb-bundle-widget--loading .lb-skeleton {\n background: linear-gradient(\n 90deg,\n rgba(0, 0, 0, 0.06) 0%,\n rgba(0, 0, 0, 0.10) 50%,\n rgba(0, 0, 0, 0.06) 100%\n );\n background-size: 200% 100%;\n border-radius: 6px;\n animation: lb-skeleton-pulse 1.4s ease-in-out infinite;\n}\n\n.lb-bundle-widget--loading .lb-skeleton--title {\n height: 28px;\n width: 60%;\n margin-bottom: 16px;\n}\n\n.lb-bundle-widget--loading .lb-skeleton--products {\n display: grid;\n gap: 12px;\n margin-bottom: 16px;\n}\n\n.lb-bundle-widget--loading .lb-skeleton--row {\n display: grid;\n grid-template-columns: 56px 1fr 60px;\n gap: 12px;\n align-items: center;\n}\n\n.lb-bundle-widget--loading .lb-skeleton--thumb {\n height: 56px;\n width: 56px;\n border-radius: 8px;\n}\n\n.lb-bundle-widget--loading .lb-skeleton--line {\n height: 14px;\n border-radius: 4px;\n}\n\n.lb-bundle-widget--loading .lb-skeleton--line + .lb-skeleton--line {\n margin-top: 8px;\n width: 70%;\n}\n\n.lb-bundle-widget--loading .lb-skeleton--price {\n height: 20px;\n width: 60px;\n}\n\n.lb-bundle-widget--loading .lb-skeleton--footer {\n margin-top: 16px;\n padding-top: 16px;\n border-top: 1px solid var(--lb-border, #E5E5E5);\n display: grid;\n grid-template-columns: 1fr auto;\n gap: 12px;\n align-items: center;\n}\n\n.lb-bundle-widget--loading .lb-skeleton--total {\n height: 24px;\n width: 40%;\n}\n\n.lb-bundle-widget--loading .lb-skeleton--cta {\n height: 44px;\n width: 140px;\n border-radius: var(--lb-radius, 8px);\n}\n\n@keyframes lb-skeleton-pulse {\n 0% { background-position: 0% 50%; }\n 100% { background-position: -200% 50%; }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .lb-bundle-widget--loading .lb-skeleton {\n animation: none;\n }\n}\n`;\n\n","/**\n * <lime-bundle> Web Component — renders Lime Bundles on any storefront.\n *\n * ## The two modes\n *\n * Single-bundle (pinned):\n * <lime-bundle\n * shop-domain=\"my-shop.myshopify.com\"\n * storefront-token=\"shpat_...\"\n * bundle-gid=\"gid://shopify/Metaobject/42\"\n * ></lime-bundle>\n *\n * Product-aware (matches classic Liquid theme block behaviour — one snippet\n * on the product page renders every active bundle configured against that\n * product):\n * <lime-bundle\n * shop-domain=\"my-shop.myshopify.com\"\n * storefront-token=\"shpat_...\"\n * ></lime-bundle>\n *\n * Product resolution cascade (when no `bundle-gid` is set):\n * 1. explicit `product-handle` attribute\n * 2. <meta name=\"shopify:product-handle\" content=\"...\"> on the page\n * 3. /products/<handle> segment of window.location.pathname\n * 4. fallthrough: renders nothing, fires `lime-bundle:error`\n *\n * Multi-product renderers (volume) bind to the product this embed is\n * standing on: the explicit `product-id` attribute wins, then the handle\n * cascade above, then the bundle's first product when neither names one.\n *\n * ## Add-to-cart behaviour\n *\n * Merchants who do nothing get a default: the widget calls Shopify's\n * Storefront Cart API (tokenless — no extra scopes required) and redirects\n * the browser to the returned checkoutUrl. One-click-to-checkout is the\n * right UX for most merchants pasting the widget into Webflow / Wix /\n * Squarespace / static HTML.\n *\n * Merchants with their own cart state (Hydrogen's useCart, a custom cart\n * drawer, etc.) opt out by attaching a listener that calls\n * `event.preventDefault()`:\n *\n * document.querySelector(\"lime-bundle\").addEventListener(\n * \"lime-bundle:add-to-cart\",\n * (ev) => {\n * ev.preventDefault(); // suppress the default redirect\n * myCart.linesAdd(ev.detail.lines);\n * },\n * );\n *\n * The event is always dispatched; only the default action is conditional.\n */\nimport {\n createStorefrontClient,\n BUNDLE_METAOBJECT_QUERY,\n BUNDLES_FOR_PRODUCT_QUERY,\n CART_CREATE_MUTATION,\n CART_LINES_ADD_MUTATION,\n CART_LINES_QUERY,\n CART_LINES_REMOVE_MUTATION,\n SHOP_SETTINGS_QUERY,\n parseMetaobjectBundle,\n convertBundleToPresentment,\n observeImpression,\n reportImpression,\n reportAddToCart,\n injectCustomCss,\n sanitizeCustomCss,\n type ParsedBundle,\n type BundleMetaobjectResponse,\n type BundlesForProductResponse,\n type CartCreateResponse,\n type CartLinesAddResponse,\n type CartLinesQueryResponse,\n type CartLinesRemoveResponse,\n type CartMutationPayload,\n type ShopSettingsResponse,\n type CartLineInput,\n type StorefrontClient,\n type BuyerResolver,\n type BuyerInput,\n hasInContext,\n withInContext,\n isVisibleToBuyer,\n getVisitorId,\n resolveAbTests,\n} from \"@lime-bundles/core\";\nimport { BUNDLE_GID_ATTRIBUTE } from \"@lime-bundles/render/host\";\nimport { renderFixedBundle } from \"./renderers/fixed\";\nimport { renderMixMatchBundle } from \"./renderers/mix-match\";\nimport { renderVolumeBundle } from \"./renderers/volume\";\nimport { renderBogoBundle } from \"./renderers/bogo\";\nimport { renderMultiStepBundle } from \"./renderers/multi-step\";\nimport { applyWidgetConfigVars } from \"@lime-bundles/core\";\nimport { trackInputMode } from \"./utils/input-mode\";\nimport {\n BUNDLE_BASE_CSS,\n BUNDLE_DROPDOWN_CSS,\n BUNDLE_FIXED_CSS,\n BUNDLE_MIX_MATCH_CSS,\n BUNDLE_MULTI_STEP_CSS,\n BUNDLE_SKELETON_CSS,\n BUNDLE_VOLUME_CSS,\n BUNDLE_BOGO_CSS,\n} from \"./styles/bundle-css\";\n\n/**\n * Per-shop localStorage key for the active cart id. Scoping by shop domain\n * keeps the cart isolated when a single browser visits multiple Lime-\n * Bundles-powered storefronts (rare, but correct).\n */\nconst cartStorageKey = (shopDomain: string) => `lb_cart_id:${shopDomain}`;\n\n/**\n * Resolve the current product handle from the page. Runs the cascade\n * documented on LimeBundleElement and returns null if no source matches.\n */\nfunction resolveProductHandle(explicit: string | null): string | null {\n if (explicit) return explicit.trim() || null;\n\n if (typeof document !== \"undefined\") {\n const meta = document.querySelector<HTMLMetaElement>(\n 'meta[name=\"shopify:product-handle\"]',\n );\n if (meta?.content) return meta.content.trim() || null;\n }\n\n if (typeof window !== \"undefined\") {\n const match = window.location.pathname.match(/\\/products\\/([^/?#]+)/);\n if (match?.[1]) return decodeURIComponent(match[1]);\n }\n\n return null;\n}\n\nexport class LimeBundleElement extends HTMLElement {\n static observedAttributes = [\n \"shop-domain\",\n \"storefront-token\",\n \"bundle-gid\",\n \"product-handle\",\n \"product-id\",\n \"app-url\",\n \"analytics\",\n \"locale\",\n \"country\",\n \"language\",\n \"market-id\",\n \"currency-rate\",\n ];\n\n /**\n * B2B buyer identity. Set programmatically — `element.buyer = {...}` or\n * `element.buyer = () => fetchToken()`. NEVER expose as an HTML attribute\n * because the customer access token would land in DOM snapshots (Sentry,\n * analytics, browser extensions) and Referer headers. See the package\n * README (\"Markets & B2B\").\n */\n buyer: BuyerResolver | undefined = undefined;\n\n private shadow: ShadowRoot;\n private bundles: ParsedBundle[] = [];\n\n /**\n * The product handle this embed is standing on, when one can be resolved\n * (attribute → meta tag → /products/<handle> path). The mix-and-match\n * courtesy seed uses it to pre-add the current product the way the theme\n * widget does; null on pages with no product context.\n */\n private currentProductHandle: string | null = null;\n private abortController: AbortController | null = null;\n private impressionCleanups: Array<() => void> = [];\n /**\n * Tick / observer / listener cleanups registered by individual renderers\n * (e.g. countdown setInterval handles). Flushed in disconnectedCallback\n * so we never leak timers or event listeners when the widget is removed.\n */\n private renderCleanups: Array<() => void> = [];\n /**\n * Merchant custom CSS fetched from the shop metafield. Injected inside\n * the shadow root alongside the bundle stylesheets so selectors like\n * `.lb-bundle-widget { ... }` reach the widget's DOM.\n */\n private shopCustomCss: string | null = null;\n\n constructor() {\n super();\n this.shadow = this.attachShadow({ mode: \"open\" });\n }\n\n connectedCallback() {\n // `fetchBundle()` calls `renderLoading()` synchronously before any\n // await — the skeleton paints on the first frame, reserving\n // layout space before the Storefront API call resolves.\n this.fetchBundle();\n }\n\n disconnectedCallback() {\n this.abortController?.abort();\n this.teardownImpressions();\n this.teardownRenderers();\n }\n\n private teardownImpressions() {\n for (const cleanup of this.impressionCleanups) cleanup();\n this.impressionCleanups = [];\n }\n\n private teardownRenderers() {\n for (const cleanup of this.renderCleanups) cleanup();\n this.renderCleanups = [];\n }\n\n attributeChangedCallback(\n name: string,\n oldValue: string | null,\n newValue: string | null,\n ) {\n if (oldValue === newValue || !this.isConnected) return;\n if (\n name === \"bundle-gid\" ||\n name === \"product-handle\" ||\n name === \"product-id\" ||\n name === \"shop-domain\" ||\n name === \"storefront-token\" ||\n name === \"currency-rate\"\n ) {\n if (this.shopDomain && this.storefrontToken) {\n this.fetchBundle();\n }\n }\n }\n\n private get shopDomain(): string {\n return this.getAttribute(\"shop-domain\") ?? \"\";\n }\n\n private get storefrontToken(): string {\n return this.getAttribute(\"storefront-token\") ?? \"\";\n }\n\n private get bundleGid(): string {\n return this.getAttribute(\"bundle-gid\") ?? \"\";\n }\n\n private get productHandleAttr(): string {\n return this.getAttribute(\"product-handle\") ?? \"\";\n }\n\n /**\n * The current product's id — full GID (`gid://shopify/Product/123`) or\n * bare numeric id. Tells multi-product renderers (volume) which of the\n * bundle's products this embed is standing on; takes precedence over the\n * handle cascade. Without it (and with no resolvable handle) they bind to\n * the bundle's first product.\n */\n private get productIdAttr(): string {\n return this.getAttribute(\"product-id\") ?? \"\";\n }\n\n private get appUrl(): string {\n return this.getAttribute(\"app-url\") ?? \"\";\n }\n\n private get analyticsEnabled(): boolean {\n return this.getAttribute(\"analytics\") !== \"false\";\n }\n\n private get country(): string | undefined {\n return this.getAttribute(\"country\") ?? undefined;\n }\n\n private get language(): string | undefined {\n return this.getAttribute(\"language\") ?? undefined;\n }\n\n private get marketId(): string | undefined {\n return this.getAttribute(\"market-id\") ?? undefined;\n }\n\n /**\n * Store→presentment currency rate. Merchant-configured amounts\n * (fixed_amount / flat_price values, volume tier amounts) are stored in\n * the shop's store currency; set this to the buyer's currency rate so\n * quoted sale prices match what the checkout's discount function\n * charges. Anything unparseable or non-positive falls back to 1 —\n * no conversion — via `normalizeCurrencyRate`.\n */\n private get currencyRate(): string | undefined {\n return this.getAttribute(\"currency-rate\") ?? undefined;\n }\n\n /**\n * Returns the @inContext-wrapped query when any context field is set;\n * otherwise the plain query. Keeps responses publicly cacheable when\n * no buyer/country/language is set.\n */\n private wrapQueryForContext(query: string): string {\n return hasInContext({\n shopDomain: this.shopDomain,\n accessToken: this.storefrontToken,\n country: this.country,\n language: this.language,\n buyer: this.buyer,\n })\n ? withInContext(query)\n : query;\n }\n\n /** Buyer resolved once per fetch so the market gate can read the\n * company location without invoking a callback resolver twice. */\n private resolvedBuyer: BuyerInput | undefined = undefined;\n\n /**\n * Returns true if this bundle should be hidden for the current buyer.\n * For \"all\" bundles, always returns false (visible). For \"specific\"\n * bundles, visibility needs a matching signal: the `country` attribute\n * against the bundle's projected country codes (retail markets), the\n * resolved buyer's companyLocationId against its projected company\n * locations (B2B markets), or a legacy `market-id` match.\n */\n private isMarketHidden(bundle: ParsedBundle): boolean {\n return !isVisibleToBuyer(bundle, {\n marketId: this.marketId,\n countryCode: this.country,\n companyLocationId: this.resolvedBuyer?.companyLocationId,\n });\n }\n\n private async fetchBundle() {\n if (!this.shopDomain || !this.storefrontToken) {\n this.renderError(\n \"Missing required attributes: shop-domain, storefront-token\",\n );\n return;\n }\n\n this.abortController?.abort();\n const controller = new AbortController();\n this.abortController = controller;\n\n this.renderLoading();\n\n // Resolve the buyer once: the client needs it for @inContext and the\n // market gate needs its companyLocationId.\n try {\n this.resolvedBuyer =\n typeof this.buyer === \"function\" ? await this.buyer() : this.buyer;\n } catch {\n // A failing resolver shouldn't kill the widget — fall back to an\n // uncontextualized fetch, exactly as if no buyer was set.\n this.resolvedBuyer = undefined;\n }\n\n const client = createStorefrontClient({\n shopDomain: this.shopDomain,\n accessToken: this.storefrontToken,\n country: this.country,\n language: this.language,\n buyer: this.resolvedBuyer,\n });\n\n try {\n // Fire bundle query BEFORE CSS so order-sensitive call logs (and any\n // test harness that mocks fetch with sequential mockResolvedValueOnce)\n // see the bundle as call #1, CSS as call #2. Parallelism preserved\n // via Promise.all at the await site below.\n let bundlePromise: Promise<void>;\n let singleBundleMode = false;\n // Resolved in both modes: a pinned bundle-gid embed sitting on a\n // product page still gets the theme's courtesy-seed behaviour.\n this.currentProductHandle = resolveProductHandle(this.productHandleAttr);\n if (this.bundleGid) {\n singleBundleMode = true;\n bundlePromise = this.fetchSingleBundle(client, controller.signal);\n } else {\n const handle = this.currentProductHandle;\n if (!handle) {\n this.teardownImpressions();\n this.renderError(\n \"No bundle-gid or product-handle provided, and the current URL doesn't match /products/<handle>.\",\n );\n return;\n }\n bundlePromise = this.fetchProductBundles(\n client,\n controller.signal,\n handle,\n );\n }\n\n // Shop settings fetch kicks off AFTER bundle fetch for deterministic\n // call order. Best-effort — the widget renders with default styling and\n // the default out-of-stock behaviour if it fails. Both values are\n // awaited before `renderBundles()` below, which matters for the\n // out-of-stock behaviour specifically: a renderer can't retroactively\n // un-render a bundle, so the value has to be in hand before first paint.\n const shopSettingsPromise = client\n .query<ShopSettingsResponse>(SHOP_SETTINGS_QUERY, undefined, {\n signal: controller.signal,\n })\n .catch(() => null);\n\n await bundlePromise;\n if (controller.signal.aborted) return;\n\n // Single-bundle mode with an explicit GID that didn't resolve is a\n // genuine error — the merchant pinned a specific bundle and it's\n // missing. Product-handle mode with zero bundles is NOT an error:\n // the product legitimately has no bundles configured; widget stays\n // invisible (mirrors classic-theme Liquid block behaviour).\n if (singleBundleMode && this.bundles.length === 0) {\n this.teardownImpressions();\n this.renderError(\"Bundle not found\");\n return;\n }\n\n const shopSettings = await shopSettingsPromise;\n const customCss = shopSettings?.shop?.customCss?.value;\n if (customCss) {\n // Classic storefronts get the CSS in document.head; the web\n // component also stores the sanitized CSS for its own shadow\n // root injection below, since shadow DOM blocks inherited styles.\n injectCustomCss(this.shopDomain, customCss);\n const sanitized = sanitizeCustomCss(customCss);\n if (sanitized.ok) this.shopCustomCss = sanitized.css;\n }\n\n this.renderBundles();\n } catch (err) {\n if (controller.signal.aborted) return;\n this.bundles = [];\n this.teardownImpressions();\n this.renderError(\n err instanceof Error ? err.message : \"Failed to load bundle\",\n );\n }\n }\n\n private async fetchSingleBundle(\n client: StorefrontClient,\n signal: AbortSignal,\n ): Promise<void> {\n const data = await client.query<BundleMetaobjectResponse>(\n this.wrapQueryForContext(BUNDLE_METAOBJECT_QUERY),\n { id: this.bundleGid },\n { signal },\n );\n if (!data.metaobject) {\n this.bundles = [];\n return;\n }\n const parsed = parseMetaobjectBundle(\n data.metaobject.id,\n data.metaobject.fields,\n );\n this.bundles =\n parsed && !this.isMarketHidden(parsed)\n ? [convertBundleToPresentment(parsed, this.currencyRate)]\n : [];\n }\n\n private async fetchProductBundles(\n client: StorefrontClient,\n signal: AbortSignal,\n productHandle: string,\n ): Promise<void> {\n // fetchBundlesForProduct re-creates its own client; skip that indirection\n // and reuse the one we already built so the request shares the same\n // AbortSignal and header config.\n const data = await client.query<BundlesForProductResponse>(\n this.wrapQueryForContext(BUNDLES_FOR_PRODUCT_QUERY),\n { handle: productHandle },\n { signal },\n );\n if (!data.product) {\n this.bundles = [];\n return;\n }\n const refs = data.product.metafield?.references?.nodes ?? [];\n const bundles: ParsedBundle[] = [];\n for (const ref of refs) {\n const parsed = parseMetaobjectBundle(ref.id, ref.fields);\n if (parsed && !this.isMarketHidden(parsed)) {\n bundles.push(convertBundleToPresentment(parsed, this.currencyRate));\n }\n }\n // Two bundles in the same A/B test are alternatives, not a pair to stack.\n // Applied after the market filter so a side hidden in this market leaves\n // its sibling showing alone rather than eliminating the offer entirely.\n this.bundles = resolveAbTests(bundles, getVisitorId());\n }\n\n /**\n * Dispatch add-to-cart with a cancelable event, then — unless a listener\n * called preventDefault — execute the default cart-and-checkout flow.\n *\n * `fire-and-forget` against `reportAddToCart` runs regardless so merchants\n * with BYO cart still get analytics.\n */\n private handleAddToCart = async (\n bundle: ParsedBundle,\n lines: CartLineInput[],\n ): Promise<void> => {\n const ev = new CustomEvent(\"lime-bundle:add-to-cart\", {\n detail: { lines },\n bubbles: true,\n composed: true,\n cancelable: true,\n });\n // dispatchEvent returns false if preventDefault() was called on a\n // cancelable event. That's how merchants opt out of the default flow.\n const allowDefault = this.dispatchEvent(ev);\n\n // Analytics fire regardless of which cart path runs.\n this.reportAddToCartEvent(bundle, lines);\n\n if (allowDefault) {\n await this.defaultAddToCart(lines);\n }\n };\n\n /**\n * Default cart flow: Shopify's Storefront Cart API is tokenless, so we\n * don't need any additional scopes. Persist the cart ID in localStorage\n * so subsequent adds on the same browser session join the existing cart\n * instead of creating a new one every click.\n */\n private async defaultAddToCart(lines: CartLineInput[]): Promise<void> {\n if (typeof window === \"undefined\") return;\n\n const client = createStorefrontClient({\n shopDomain: this.shopDomain,\n accessToken: this.storefrontToken,\n });\n const storage = window.localStorage;\n const key = cartStorageKey(this.shopDomain);\n let cartId = storage?.getItem(key) ?? null;\n\n /** Errors whose only complaint is the cart id itself: the stored GID\n * expired or was merged on Shopify's side (~10 days of inactivity).\n * ONLY these fall through to cartCreate — any other error names a real\n * problem with the lines, and retrying the same lines into a fresh\n * cart used to silently drop them on the way to checkout. */\n const isStaleCartError = (\n errors: Array<{ field: string[] | null; message: string }>,\n ): boolean =>\n errors.length > 0 &&\n errors.every((e) => (e.field ?? []).includes(\"cartId\"));\n\n /** A capped line means the bundle is incomplete and its discount will\n * never apply — checkout at full price is worse than no checkout. */\n const stockCapped = (\n warnings: CartMutationPayload[\"warnings\"],\n ): boolean =>\n (warnings ?? []).some((w) => w.code === \"MERCHANDISE_NOT_ENOUGH_STOCK\");\n\n const fail = (message: string): void => {\n this.dispatchEvent(\n new CustomEvent(\"lime-bundle:error\", {\n detail: { message, code: \"CART_ERROR\" },\n bubbles: true,\n composed: true,\n }),\n );\n };\n\n try {\n let checkoutUrl: string | null = null;\n\n // The default flow sends every add straight to checkout, so a\n // lime-tagged line already sitting in the saved cart can only be an\n // attempt the shopper walked away from. Clear those before adding —\n // otherwise every abandoned attempt rides into the next checkout.\n // Returns null (after dropping the saved id) when the cart is gone\n // or couldn't be cleaned; the cartCreate path below takes over.\n if (cartId) {\n cartId = await this.clearAbandonedBundleLines(client, cartId, () =>\n storage?.removeItem(key),\n );\n }\n\n if (cartId) {\n const res = await client.query<CartLinesAddResponse>(\n CART_LINES_ADD_MUTATION,\n { cartId, lines },\n );\n const payload = res.cartLinesAdd;\n if (payload?.userErrors?.length) {\n if (!isStaleCartError(payload.userErrors)) {\n fail(payload.userErrors[0].message);\n return;\n }\n storage?.removeItem(key);\n } else if (stockCapped(payload?.warnings)) {\n fail(\"Some items in this bundle are out of stock.\");\n return;\n } else if (payload?.cart) {\n checkoutUrl = payload.cart.checkoutUrl;\n }\n }\n\n if (!checkoutUrl) {\n const res = await client.query<CartCreateResponse>(\n CART_CREATE_MUTATION,\n { input: { lines } },\n );\n const payload = res.cartCreate;\n // The retry's own result gets the same scrutiny the first attempt\n // did; a cart WITH userErrors is a partial cart, not a success.\n if (payload?.userErrors?.length) {\n fail(payload.userErrors[0].message);\n return;\n }\n if (stockCapped(payload?.warnings)) {\n fail(\"Some items in this bundle are out of stock.\");\n return;\n }\n if (payload?.cart) {\n storage?.setItem(key, payload.cart.id);\n checkoutUrl = payload.cart.checkoutUrl;\n }\n }\n\n if (checkoutUrl) {\n window.location.assign(checkoutUrl);\n } else {\n fail(\"Cart creation failed\");\n }\n } catch (err) {\n this.dispatchEvent(\n new CustomEvent(\"lime-bundle:error\", {\n detail: {\n message:\n err instanceof Error ? err.message : \"Cart mutation failed\",\n code: \"CART_ERROR\",\n },\n bubbles: true,\n composed: true,\n }),\n );\n }\n }\n\n /**\n * Remove this widget's own earlier lines — the ones tagged with the\n * `_lime_bundle_gid` attribute — from the saved cart, so a shopper who\n * clicked add three times while deciding checks out with only what they\n * just built. Lines without the tag were put in the cart by something\n * else (the host site, a previous non-widget purchase flow) and are\n * left alone.\n *\n * Returns the cart id when the cart is still usable. Returns null after\n * calling `dropSavedCart` when it isn't: either the id no longer\n * resolves (expired or merged on Shopify's side), or the removal itself\n * failed — in both cases a fresh cart is the only way to guarantee the\n * checkout matches the shopper's current build.\n */\n private async clearAbandonedBundleLines(\n client: StorefrontClient,\n cartId: string,\n dropSavedCart: () => void,\n ): Promise<string | null> {\n const res = await client.query<CartLinesQueryResponse>(CART_LINES_QUERY, {\n cartId,\n });\n if (!res.cart) {\n dropSavedCart();\n return null;\n }\n\n const abandonedLineIds = res.cart.lines.nodes\n .filter((line) =>\n line.attributes.some((attr) => attr.key === BUNDLE_GID_ATTRIBUTE),\n )\n .map((line) => line.id);\n if (abandonedLineIds.length === 0) return cartId;\n\n const removal = await client.query<CartLinesRemoveResponse>(\n CART_LINES_REMOVE_MUTATION,\n { cartId, lineIds: abandonedLineIds },\n );\n if (removal.cartLinesRemove?.userErrors?.length) {\n dropSavedCart();\n return null;\n }\n return cartId;\n }\n\n private reportAddToCartEvent(\n bundle: ParsedBundle,\n lines: CartLineInput[],\n ): void {\n if (!this.analyticsEnabled || !this.appUrl) return;\n\n const quantity = lines.reduce((sum, l) => sum + l.quantity, 0);\n const totalPrice = lines.reduce((sum, line) => {\n const product = bundle.products.find((p) =>\n p.variants.nodes.some((v) => v.id === line.merchandiseId),\n );\n const variant = product?.variants.nodes.find(\n (v) => v.id === line.merchandiseId,\n );\n const price = variant ? parseFloat(variant.price.amount) : 0;\n return sum + price * line.quantity;\n }, 0);\n\n reportAddToCart(\n { shopDomain: this.shopDomain, appUrl: this.appUrl },\n {\n bundleGid: bundle.id,\n bundleType: bundle.bundleType,\n productId: bundle.products[0]?.id ?? \"\",\n quantity,\n totalPrice: Math.round(totalPrice * 100) / 100,\n },\n );\n }\n\n private renderBundles() {\n this.teardownImpressions();\n this.teardownRenderers();\n this.shadow.innerHTML = \"\";\n\n // Bundle the base stylesheet and every type-specific sheet in one <style>.\n // This matches the Liquid theme app block's output (base.css +\n // bundle-{fixed,mix-match,volume}.css) so the shipped widget honours the\n // same selectors a merchant already styled against. Shadow DOM scope\n // keeps these rules from leaking out; the ported styles all target\n // `.lb-bundle-widget` descendants so there's no global bleed.\n const style = document.createElement(\"style\");\n style.textContent = [\n BUNDLE_BASE_CSS,\n BUNDLE_FIXED_CSS,\n BUNDLE_MIX_MATCH_CSS,\n BUNDLE_MULTI_STEP_CSS,\n BUNDLE_VOLUME_CSS,\n BUNDLE_BOGO_CSS,\n BUNDLE_DROPDOWN_CSS,\n ].join(\"\\n\");\n this.shadow.appendChild(style);\n\n // Merchant custom CSS — injected AFTER the built-in stylesheets so\n // the merchant's rules override defaults. Sanitized upstream via\n // sanitizeCustomCss (strips < > and script-safety patterns).\n if (this.shopCustomCss) {\n const customStyle = document.createElement(\"style\");\n customStyle.setAttribute(\"data-lime-bundles\", \"shop-custom-css\");\n customStyle.textContent = this.shopCustomCss;\n this.shadow.appendChild(customStyle);\n }\n\n // Empty state: the shadow root holds only the <style> tag — nothing\n // visible. Matches Liquid theme UX where a product with no bundles\n // simply shows no block. We still fire `lime-bundle:loaded` below so\n // consumers know the async work completed (important for test\n // synchronisation and for merchants who want to hide a parent\n // placeholder once the widget has decided whether to render).\n\n for (const bundle of this.bundles) {\n const container = document.createElement(\"div\");\n container.className = \"lb-bundle-widget\";\n container.setAttribute(\"role\", \"region\");\n container.setAttribute(\"aria-label\", bundle.title);\n container.setAttribute(\"data-bundle-type\", bundle.bundleType);\n container.setAttribute(\"data-bundle-gid\", bundle.id);\n // The countdown reads its end date off the wrapper, exactly where the\n // theme's Liquid writes it — without this the strip self-hides.\n if (bundle.endsAt) container.setAttribute(\"data-ends-at\", bundle.endsAt);\n\n // Emit every merchant-configurable --lb-* property so the ported CSS\n // renders the widget with the look the merchant set in the admin\n // editor.\n applyWidgetConfigVars(container, bundle.widgetConfig);\n\n // Toggle using-mouse / using-keyboard on this container so the CSS\n // focus-ring rules match the customer's current input device.\n this.renderCleanups.push(trackInputMode(container));\n\n const dispatch = (lines: CartLineInput[]) =>\n this.handleAddToCart(bundle, lines);\n const registerCleanup = (fn: () => void) =>\n this.renderCleanups.push(fn);\n\n switch (bundle.bundleType) {\n case \"fixed\":\n renderFixedBundle(container, bundle, dispatch, registerCleanup);\n break;\n case \"mix_match\":\n renderMixMatchBundle(\n container,\n bundle,\n dispatch,\n registerCleanup,\n this.currentProductHandle,\n );\n break;\n case \"volume\":\n // Volume has no teardown: it binds only to elements it owns, which\n // go with the shadow root on re-render.\n renderVolumeBundle(container, bundle, dispatch, {\n productId: this.productIdAttr || null,\n productHandle: this.currentProductHandle,\n });\n break;\n case \"bogo\":\n renderBogoBundle(container, bundle, dispatch, registerCleanup);\n break;\n case \"multi_step\":\n renderMultiStepBundle(container, bundle, dispatch, registerCleanup);\n break;\n }\n\n this.shadow.appendChild(container);\n this.setupImpressionFor(bundle, container);\n }\n\n const first = this.bundles[0];\n this.dispatchEvent(\n new CustomEvent(\"lime-bundle:loaded\", {\n detail: {\n bundleCount: this.bundles.length,\n bundleTypes: this.bundles.map((b) => b.bundleType),\n // Legacy fields — meaningful only in single-bundle mode. Preserved\n // for merchants who attached listeners against the pre-1.0 shape.\n bundleType: first?.bundleType,\n title: first?.title,\n },\n bubbles: true,\n composed: true,\n }),\n );\n }\n\n private setupImpressionFor(bundle: ParsedBundle, element: Element) {\n if (!this.analyticsEnabled || !this.appUrl) return;\n const cleanup = observeImpression(element, () => {\n reportImpression(\n { shopDomain: this.shopDomain, appUrl: this.appUrl },\n {\n bundleGid: bundle.id,\n bundleType: bundle.bundleType,\n },\n );\n });\n this.impressionCleanups.push(cleanup);\n }\n\n private renderLoading() {\n // Synchronous paint on every mount (before the Storefront fetch\n // resolves) so the host element has intrinsic size from render-0\n // and doesn't shift layout when the real bundle swaps in.\n this.shadow.innerHTML = `\n <style>${BUNDLE_BASE_CSS}</style>\n <style>${BUNDLE_SKELETON_CSS}</style>\n <div class=\"lb-bundle-widget lb-bundle-widget--loading\" aria-busy=\"true\" aria-label=\"Loading bundle\">\n <div class=\"lb-skeleton lb-skeleton--title\"></div>\n <div class=\"lb-skeleton--products\">\n <div class=\"lb-skeleton--row\">\n <div class=\"lb-skeleton lb-skeleton--thumb\"></div>\n <div>\n <div class=\"lb-skeleton lb-skeleton--line\"></div>\n <div class=\"lb-skeleton lb-skeleton--line\"></div>\n </div>\n <div class=\"lb-skeleton lb-skeleton--price\"></div>\n </div>\n <div class=\"lb-skeleton--row\">\n <div class=\"lb-skeleton lb-skeleton--thumb\"></div>\n <div>\n <div class=\"lb-skeleton lb-skeleton--line\"></div>\n <div class=\"lb-skeleton lb-skeleton--line\"></div>\n </div>\n <div class=\"lb-skeleton lb-skeleton--price\"></div>\n </div>\n <div class=\"lb-skeleton--row\">\n <div class=\"lb-skeleton lb-skeleton--thumb\"></div>\n <div>\n <div class=\"lb-skeleton lb-skeleton--line\"></div>\n <div class=\"lb-skeleton lb-skeleton--line\"></div>\n </div>\n <div class=\"lb-skeleton lb-skeleton--price\"></div>\n </div>\n </div>\n <div class=\"lb-skeleton--footer\">\n <div class=\"lb-skeleton lb-skeleton--total\"></div>\n <div class=\"lb-skeleton lb-skeleton--cta\"></div>\n </div>\n </div>\n `;\n }\n\n private renderError(message: string) {\n this.shadow.innerHTML = \"\";\n this.dispatchEvent(\n new CustomEvent(\"lime-bundle:error\", {\n detail: { message, code: \"LOAD_ERROR\" },\n bubbles: true,\n composed: true,\n }),\n );\n }\n\n}\n\n// Re-export the helper so consumers of the widget package can import it\n// when they want to share the URL-detection logic with their own code.\n"],"mappings":"6cAAA,IAAAA,GAAA,GAAAC,GAAAD,GAAA,uBAAAE,GAAA,mBAAAC,8FCUaC,GAAN,cAAiC,KAAM,CAC5C,YACkBC,EAChB,CACA,MAAMA,EAAO,IAAKC,GAAMA,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC,EAF7B,KAAA,OAAAD,EAGhB,KAAK,KAAO,oBACd,CACF,EAiEaE,GAAsB,UAEnC,eAAeC,GAAaC,EAAwD,CAClF,GAAKA,EACL,OAAI,OAAOA,GAAU,WACZ,MAAMA,EAAM,EAEdA,CACT,CAOO,SAASC,GAAaC,EAAyC,CACpE,MAAO,GAAQA,EAAO,SAAWA,EAAO,UAAYA,EAAO,MAC7D,CAEO,SAASC,GACdD,EACkB,CAClB,IAAME,EAAUF,EAAO,YAAcJ,GAC/BO,EAAW,WAAWH,EAAO,UAAU,QAAQE,CAAO,gBAE5D,MAAO,CACL,MAAM,MACJE,EACAC,EACAC,EACY,CACZ,IAAMC,EAAkC,CACtC,eAAgB,mBAChB,oCAAqCP,EAAO,WAC9C,EACIA,EAAO,UACTO,EAAQ,6BAA6B,EAAIP,EAAO,SAOlD,IAAMF,EAAQ,MAAMD,GAAaG,EAAO,KAAK,EACvCQ,EAA2C,CAAE,GAAIH,GAAa,CAAC,CAAG,EACpEL,EAAO,UAASQ,EAAgB,QAAUR,EAAO,SACjDA,EAAO,WAAUQ,EAAgB,SAAWR,EAAO,UACnDF,IAAOU,EAAgB,MAAQV,GAEnC,IAAMW,EAAW,MAAM,MAAMN,EAAU,CACrC,OAAQ,OACR,QAAAI,EACA,KAAM,KAAK,UAAU,CAAE,MAAAH,EAAO,UAAWI,CAAgB,CAAC,EAC1D,OAAQF,GAAS,MACnB,CAAC,EAED,GAAI,CAACG,EAAS,GACZ,MAAM,IAAIhB,GAAmB,CAC3B,CACE,QAAS,yBAAyBgB,EAAS,MAAM,IAAIA,EAAS,UAAU,EAC1E,CACF,CAAC,EAGH,IAAMC,EAAQ,MAAMD,EAAS,KAAK,EAKlC,GAAIC,EAAK,QAAQ,OACf,MAAM,IAAIjB,GAAmBiB,EAAK,MAAM,EAG1C,OAAOA,EAAK,IACd,CACF,CACF,CCrIO,SAASC,GAAcP,EAAuB,CACnD,OAAOA,EAAM,QACX,wCACA,CAACQ,EAAQC,EAAMC,IAAS,CACtB,IAAMC,GAAWD,GAAQ,IAAI,KAAK,EAC5BE,EAAQ,qEACRC,EAAWF,EAAU,GAAGA,CAAO,KAAKC,CAAK,GAAKA,EACpD,MAAO,SAASH,CAAI,IAAII,CAAQ,uEAClC,CACF,CACF,CAEO,IAAMC,GAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA8G1BC,GAAsB;;;;;;;;EA+BtBC,GAA4B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA+I5BC,GAAuB;;;;;;;;EAUvBC,GAA0B;;;;;;;;EAmB1BC,GAAmB;;;;;;;;;;;;EA4BnBC,GAA6B;;;;;;;;EE0EnC,IAAMC,GAAN,cAA+B,KAAM,CAC1C,YAAYC,EAAiCC,EAA+B,CAC1E,MAAMD,CAAO,EAD8B,KAAA,OAAAC,EAE3C,KAAK,KAAO,kBACd,CACF,ECpbaC,GAAiD,CAC5D,KAAM,EACN,OAAQ,EACR,QAAS,GACT,MAAO,EACT,EAEaC,GAAuC,CAClD,OAAQ,CACN,iBAAkB,YAClB,mBAAoB,SACtB,EACA,OAAQ,CACN,gBAAiB,UACjB,YAAa,UACb,YAAa,EACb,aAAc,QAChB,EACA,YAAa,CACX,UAAW,UACX,eAAgB,SAChB,UAAW,GACX,mBAAoB,GACpB,cAAe,GACf,aAAc,EAChB,EACA,QAAS,CACP,iBAAkB,GAClB,iBAAkB,GAClB,cAAe,GACf,mBAAoB,EACtB,EACA,IAAK,CACH,QAAS,cACT,aAAc,UACd,gBAAiB,UACjB,YAAa,EACb,YAAa,SACf,EACA,WAAY,CACV,QAAS,EACX,EACA,UAAW,CACT,cAAe,EACjB,EACA,aAAc,CACZ,QAAS,GACT,KAAM,eACN,QAAS,UACT,UAAW,UACX,YAAa,EACb,YAAa,SACf,EACA,kBAAmB,GACnB,kBAAmB,GACnB,gBAAiB,UACjB,kBAAmB,UAEnB,WAAY,GACZ,wBAAyB,GACzB,6BAA8B,GAC9B,cAAe,UACf,gBAAiB,UACjB,kBAAmB,EACnB,kBAAmB,UACnB,mBAAoB,SACpB,qBAAsB,SACtB,iBAAkB,UAClB,oBAAqB,UACrB,qBAAsB,EACtB,qBAAsB,UAEtB,qBAAsB,GAEtB,YAAa,OACf,EASO,SAASC,GAAkBC,EAA4B,CAC5D,GAAI,CAACA,GAAO,OAAOA,GAAQ,SACzB,OAAOF,GAET,IAAMG,EAAQD,EACd,MAAO,CACL,GAAGF,GACH,GAAGG,EACH,YAAaC,GAAoBD,EAAM,WAAW,EAElD,mBAAoBE,GAAqBF,EAAM,kBAAkB,EACjE,OAAQ,CAAE,GAAGH,GAAuB,OAAQ,GAAIG,EAAM,QAAU,CAAC,CAAG,EACpE,OAAQ,CACN,GAAGH,GAAuB,OAC1B,GAAIG,EAAM,QAAU,CAAC,EAErB,aAAcE,GACXF,EAAM,QAA8C,YACvD,CACF,EACA,YAAa,CACX,GAAGH,GAAuB,YAC1B,GAAIG,EAAM,aAAe,CAAC,CAC5B,EACA,QAAS,CACP,GAAGH,GAAuB,QAC1B,GAAIG,EAAM,SAAW,CAAC,CACxB,EACA,IAAK,CAAE,GAAGH,GAAuB,IAAK,GAAIG,EAAM,KAAO,CAAC,CAAG,EAC3D,WAAY,CACV,GAAGH,GAAuB,WAC1B,GAAIG,EAAM,YAAc,CAAC,CAC3B,EACA,UAAW,CACT,GAAGH,GAAuB,UAC1B,GAAIG,EAAM,WAAa,CAAC,CAC1B,EACA,aAAc,CACZ,GAAGH,GAAuB,aAC1B,GAAIG,EAAM,cAAgB,CAAC,CAC7B,CACF,CACF,CAEA,SAASC,GACPF,EACiC,CAEjC,OADIA,IAAQ,SAAWA,IAAQ,cAC3B,OAAOA,GAAQ,UAAY,OAAO,UAAUA,CAAG,GAAKA,GAAO,EAAUA,EAClEF,GAAuB,WAChC,CAEA,SAASK,GAAqBH,EAA4B,CACxD,OAAIA,IAAQ,QAAUA,IAAQ,UAAYA,IAAQ,WAAaA,IAAQ,QAC9DA,EAEFF,GAAuB,OAAO,YACvC,CAUO,SAASM,GACdC,EAC2C,CAC3C,MAAO,CACL,aAAcA,EAAO,IAAI,aACzB,gBAAiBA,EAAO,OAAO,gBAC/B,UAAWA,EAAO,YAAY,UAC9B,YAAaA,EAAO,OAAO,YAC3B,YAAaA,EAAO,OAAO,YAC3B,gBAAiBA,EAAO,IAAI,gBAC5B,eAAgBA,EAAO,IAAI,YAC3B,eAAgBA,EAAO,IAAI,YAG3B,aAAcR,GAAiBQ,EAAO,OAAO,YAAY,EACzD,iBAAkBA,EAAO,OAAO,iBAChC,mBAAoBA,EAAO,OAAO,mBAClC,eAAgBA,EAAO,YAAY,eACnC,iBAAkBA,EAAO,YAAY,UACrC,0BAA2BA,EAAO,YAAY,mBAC9C,qBAAsBA,EAAO,YAAY,cACzC,oBAAqBA,EAAO,YAAY,aACxC,cAAeA,EAAO,UAAU,cAChC,QAASA,EAAO,IAAI,QACpB,kBAAmBA,EAAO,kBAC1B,kBAAmBA,EAAO,kBAC1B,gBAAiBA,EAAO,gBACxB,kBAAmBA,EAAO,kBAC1B,iBAAkBA,EAAO,QAAQ,iBACjC,iBAAkBA,EAAO,QAAQ,iBACjC,cAAeA,EAAO,QAAQ,cAC9B,mBAAoBA,EAAO,QAAQ,mBACnC,gBAAiBA,EAAO,aAAa,QACrC,iBAAkBA,EAAO,aAAa,KACtC,oBAAqBA,EAAO,aAAa,QACzC,sBAAuBA,EAAO,aAAa,UAC3C,wBAAyBA,EAAO,aAAa,YAC7C,wBAAyBA,EAAO,aAAa,YAC7C,WAAYA,EAAO,WACnB,wBAAyBA,EAAO,wBAChC,cAAeA,EAAO,cACtB,gBAAiBA,EAAO,gBACxB,kBAAmBA,EAAO,kBAC1B,kBAAmBA,EAAO,kBAE1B,mBAAoBR,GAAiBQ,EAAO,kBAAkB,EAC9D,qBAAsBA,EAAO,qBAC7B,iBAAkBA,EAAO,iBACzB,oBAAqBA,EAAO,oBAC5B,qBAAsBA,EAAO,qBAC7B,qBAAsBA,EAAO,qBAC7B,YAAaA,EAAO,YACpB,GAAIA,EAAO,aAAa,YAAc,QAAa,CACjD,qBAAsBA,EAAO,aAAa,SAC5C,CACF,CACF,CASO,IAAMC,GAAsC,CACjD,aAAc,qBACd,gBAAiB,UACjB,UAAW,YACX,YAAa,cACb,YAAa,oBACb,gBAAiB,gBACjB,aAAc,cACd,iBAAkB,oBAClB,mBAAoB,sBACpB,eAAgB,wBAChB,eAAgB,wBAChB,oBAAqB,wBACrB,sBAAuB,0BACvB,wBAAyB,kCACzB,wBAAyB,kCACzB,cAAe,iBACf,gBAAiB,mBACjB,kBAAmB,2BACnB,kBAAmB,2BACnB,mBAAoB,qBACpB,iBAAkB,qBAClB,oBAAqB,wBACrB,qBAAsB,+BACtB,qBAAsB,8BACxB,EAGaC,GAA+B,IAAI,IAAI,CAClD,eACA,cACA,iBACA,0BACA,oBACA,qBACA,sBACF,CAAC,EAkBM,SAASC,GACdC,EACAJ,EACM,CACN,IAAMK,EAAON,GAAoBC,CAAM,EAEvC,OAAW,CAACM,EAASC,CAAM,IAAK,OAAO,QAAQN,EAAW,EAAG,CAC3D,IAAMO,EAAQH,EAAKC,CAAO,EAC1B,GAA2BE,GAAU,KAAM,SAC3C,IAAMC,EAAaP,GAAQ,IAAII,CAAO,EAAI,GAAGE,CAAK,KAAO,OAAOA,CAAK,EACrEJ,EAAG,MAAM,YAAYG,EAAQE,CAAU,CACzC,CAMAL,EAAG,MAAM,YACP,6BACAJ,EAAO,YAAY,UAAY,QAAU,MAC3C,EACAI,EAAG,MAAM,YACP,+BACAJ,EAAO,YAAY,mBAAqB,SAAW,MACrD,EACAI,EAAG,MAAM,YACP,kCACAJ,EAAO,YAAY,cAAgB,QAAU,MAC/C,EAIAI,EAAG,MAAM,YACP,gCACAJ,EAAO,YAAY,aAAe,cAAgB,MACpD,EACAI,EAAG,MAAM,YACP,kCACAJ,EAAO,YAAY,aAAe,SAAW,MAC/C,EAMAI,EAAG,MAAM,YACP,uBACAM,GAAkBV,EAAO,OAAO,WAAW,CAC7C,EACAI,EAAG,MAAM,YACP,8BACAM,GAAkBV,EAAO,iBAAiB,CAC5C,EAKA,IAAMW,EAAYC,GAAmBZ,EAAO,YAAY,cAAc,EACtEI,EAAG,MAAM,YAAY,8BAA+BO,EAAU,WAAW,EACzEP,EAAG,MAAM,YAAY,yBAA0BO,EAAU,MAAM,EAC/DP,EAAG,MAAM,YAAY,4BAA6BO,EAAU,SAAS,EAKrE,IAAME,EAAkBD,GAAmBZ,EAAO,oBAAoB,EACtEI,EAAG,MAAM,YACP,qCACAS,EAAgB,WAClB,EACAT,EAAG,MAAM,YACP,gCACAS,EAAgB,MAClB,EACAT,EAAG,MAAM,YACP,mCACAS,EAAgB,SAClB,CACF,CASA,SAASH,GAAkBI,EAA2B,CAEpD,MAAO,iKADSA,EAAU,QAAQ,KAAM,KAAK,CACkI,qFACjL,CAQO,SAASF,GAAmBG,EAIjC,CACA,OAAQA,EAAO,CACb,IAAK,OACH,MAAO,CAAE,YAAa,QAAS,OAAQ,QAAS,UAAW,MAAO,EACpE,IAAK,OACH,MAAO,CAAE,YAAa,QAAS,OAAQ,QAAS,UAAW,MAAO,EACpE,IAAK,WACH,MAAO,CAAE,YAAa,OAAQ,OAAQ,UAAW,UAAW,MAAO,EAErE,QACE,MAAO,CAAE,YAAa,QAAS,OAAQ,QAAS,UAAW,MAAO,CACtE,CACF,CC5XA,IAAMC,GAAqB,IAAI,IAAgB,CAC7C,QACA,YACA,SACA,OACA,YACF,CAAC,EACKC,GAAkB,IAAI,IAAkB,CAAC,QAAQ,CAAC,EAMjD,SAASC,GACdC,EACAC,EACqB,CACrB,GAAI,CACF,OAAOC,GAA4BF,EAAcC,CAAM,CACzD,OAASE,EAAK,CACZ,GAAIA,aAAejC,GAAkB,OAAO,KAC5C,MAAMiC,CACR,CACF,CAMO,SAASD,GACdF,EACAC,EACc,CACd,IAAMG,EAAW,IAAI,IAAIH,EAAO,IAAKI,GAAM,CAACA,EAAE,IAAKA,CAAC,CAAC,CAAC,EAEhDC,EAAQF,EAAS,IAAI,OAAO,GAAG,OAAS,SACxCG,EAAgBH,EAAS,IAAI,aAAa,GAAG,MAC7CI,EAAYJ,EAAS,IAAI,QAAQ,GAAG,MAE1C,GAAI,CAACG,GAAiB,CAACV,GAAmB,IAAIU,CAA2B,EACvE,MAAM,IAAIrC,GACR,mCAAmCqC,GAAiB,MAAM,GAC1D,cACF,EAEF,IAAME,EAAaF,EAEnB,GAAI,CAACC,GAAa,CAACV,GAAgB,IAAIU,CAAyB,EAC9D,MAAM,IAAItC,GACR,gCAAgCsC,GAAa,MAAM,GACnD,UACF,EAEF,IAAME,EAASF,EAETG,EAAWP,EAAS,IAAI,WAAW,GAAG,OAAS,KAC/CQ,EAASR,EAAS,IAAI,SAAS,GAAG,OAAS,KAC3CS,EAAM,IAAI,KAEhB,GAAIF,EAAU,CACZ,IAAMG,EAAQ,IAAI,KAAKH,CAAQ,EAC/B,GAAI,OAAO,MAAMG,EAAM,QAAQ,CAAC,EAC9B,MAAM,IAAI5C,GACR,sBAAsByC,CAAQ,GAC9B,cACF,EAEF,GAAIG,EAAQD,EACV,MAAM,IAAI3C,GACR,qCAAqCyC,CAAQ,GAC7C,aACF,CAEJ,CACA,GAAIC,EAAQ,CACV,IAAMG,EAAM,IAAI,KAAKH,CAAM,EAC3B,GAAI,OAAO,MAAMG,EAAI,QAAQ,CAAC,EAC5B,MAAM,IAAI7C,GACR,oBAAoB0C,CAAM,GAC1B,cACF,EAEF,GAAIG,EAAMF,EACR,MAAM,IAAI3C,GACR,+BAA+B0C,CAAM,GACrC,SACF,CAEJ,CAEA,IAAMI,EAAWC,GAAgBb,CAAQ,EACnCc,EAAiBC,GAAoBf,CAAQ,EAC7CgB,EAAeC,GAAkBjB,CAAQ,EAEzCkB,EAAO,CACX,GAAItB,EACJ,MAAAM,EACA,YAAaF,EAAS,IAAI,aAAa,GAAG,OAAS,KACnD,OAAAM,EACA,SAAAM,EACA,eAAAE,EACA,aAAAE,EACA,SAAAT,EACA,OAAAC,EACA,cAAeR,EAAS,IAAI,gBAAgB,GAAG,OAAS,KACxD,mBAAoBmB,GAAwBnB,CAAQ,EACpD,kBAAmBoB,GAAmBpB,EAAU,oBAAoB,EACpE,kBAAmBoB,GAAmBpB,EAAU,oBAAoB,EACpE,iBAAkBqB,GAAsBrB,CAAQ,EAChD,UAAWsB,GAAetB,CAAQ,EAClC,aAAcuB,GAAsBvB,EAAU,eAAe,EAC7D,mBAAoBuB,GAAsBvB,EAAU,sBAAsB,EAC1E,SAAUA,EAAS,IAAI,YAAY,GAAG,OAAS,KAC/C,SAAUwB,GAAcxB,CAAQ,CAClC,EAEA,OAAQK,EAAY,CAClB,IAAK,QAEH,MADgC,CAAE,GAAGa,EAAM,WAAY,OAAQ,EAGjE,IAAK,SAMH,MALiC,CAC/B,GAAGA,EACH,WAAY,SACZ,YAAaO,GAAiBzB,CAAQ,CACxC,EAGF,IAAK,YASH,MARmC,CACjC,GAAGkB,EACH,WAAY,YACZ,YAAaQ,GAAc1B,EAAU,eAAgB,CAAE,IAAK,CAAE,CAAC,EAC/D,YAAa,KACb,aAAc2B,GAAa3B,EAAU,eAAe,EACpD,aAAc2B,GAAa3B,EAAU,eAAe,CACtD,EAGF,IAAK,OAMH,MAL+B,CAC7B,GAAGkB,EACH,WAAY,OACZ,GAAGU,GAAgB5B,CAAQ,CAC7B,EAGF,IAAK,aASH,MARoC,CAClC,GAAGkB,EACH,WAAY,aACZ,MAAOW,GAAgB7B,CAAQ,EAC/B,aAAc2B,GAAa3B,EAAU,eAAe,EACpD,aAAc2B,GAAa3B,EAAU,eAAe,EACpD,qBAAsB8B,GAA0B9B,CAAQ,CAC1D,CAGJ,CACF,CAcA,SAAS6B,GACP7B,EACmB,CACnB,IAAM5B,EAAM2D,GAAe/B,EAAU,OAAO,EAC5C,GAAI,CAAC,MAAM,QAAQ5B,CAAG,EACpB,MAAM,IAAIN,GAAiB,2BAA4B,cAAc,EAEvE,IAAMkE,EAAcC,GAClB,MAAM,QAAQA,CAAC,EAAIA,EAAE,OAAQC,GAAmB,OAAOA,GAAM,QAAQ,EAAI,CAAC,EACtEC,EAA2B,CAAC,EAClC,QAAWC,KAAShE,EAAK,CACvB,GAAI,CAACgE,GAAS,OAAOA,GAAU,UAAY,MAAM,QAAQA,CAAK,EAAG,SACjE,IAAMC,EAAID,EACV,GAAI,OAAOC,EAAE,MAAS,SAAU,SAChC,IAAMC,EAAS,OAAOD,EAAE,aAAgB,SAAWA,EAAE,YAAc,OAAOA,EAAE,WAAW,EACjFE,EACJ,OAAO,SAASD,CAAM,GAAKA,GAAU,EAAI,KAAK,MAAMA,CAAM,EAAI,EAC5DE,EAA6B,KAC7B,OAAOH,EAAE,aAAgB,UAAY,OAAO,SAASA,EAAE,WAAW,IACpEG,EAAc,KAAK,IAAID,EAAa,KAAK,MAAMF,EAAE,WAAW,CAAC,GAE/DF,EAAM,KAAK,CACT,KAAME,EAAE,KACR,YAAAE,EACA,YAAAC,EACA,WAAYR,EAAWK,EAAE,UAAU,EACnC,cAAeL,EAAWK,EAAE,aAAa,CAC3C,CAAC,CACH,CACA,GAAIF,EAAM,SAAW,EACnB,MAAM,IAAIrE,GACR,uCACA,cACF,EAEF,OAAOqE,CACT,CAQA,SAASL,GACP9B,EAC0B,CAC1B,IAAMyC,EAAgC,CAAC,EACjCC,EAAkB1C,EAAS,IAAI,YAAY,EACjD,GAAI,CAAC0C,GAAiB,YAAY,MAAO,OAAOD,EAChD,QAAWE,KAAQD,EAAgB,WAAW,MACxC,EAAE,aAAcC,IAAS,CAACA,EAAK,UAAU,QAC7CF,EAAIE,EAAK,EAAE,EAAIA,EAAK,SAAS,MAAM,IAAKC,GAAMA,EAAE,EAAE,GAEpD,OAAOH,CACT,CAQA,SAASb,GAAgB5B,EAOvB,CACA,IAAM5B,EAAM2D,GAAe/B,EAAU,aAAa,EAClD,GAAI,CAAC5B,GAAO,OAAOA,GAAQ,UAAY,MAAM,QAAQA,CAAG,EACtD,MAAM,IAAIN,GAAiB,iCAAkC,cAAc,EAE7E,IAAM+E,EAAMzE,EACN0E,EAAe,OAAOD,EAAI,cAAiB,SAAWA,EAAI,aAAe,GACzEE,EAAe,OAAOF,EAAI,cAAiB,SAAWA,EAAI,aAAe,GAC/E,GAAI,CAACC,GAAgB,CAACC,EACpB,MAAM,IAAIjF,GAAiB,6CAA8C,cAAc,EAEzF,IAAMkF,EAAYf,GAAuB,CACvC,IAAMgB,EAAI,OAAOhB,GAAM,SAAWA,EAAI,OAAOA,CAAC,EAC9C,MAAI,CAAC,OAAO,SAASgB,CAAC,GAAKA,EAAI,EAAU,EAClC,KAAK,IAAI,GAAI,KAAK,MAAMA,CAAC,CAAC,CACnC,EAIMC,EAAgBjB,GACpB,MAAM,QAAQA,CAAC,EAAIA,EAAE,OAAQkB,GAAqB,OAAOA,GAAO,QAAQ,EAAI,CAAC,EAC/E,MAAO,CACL,aAAAL,EACA,aAAAC,EACA,YAAaC,EAASH,EAAI,WAAW,EACrC,YAAaG,EAASH,EAAI,WAAW,EACrC,cAAeK,EAAaL,EAAI,aAAa,EAC7C,cAAeK,EAAaL,EAAI,aAAa,CAC/C,CACF,CAEA,SAAShC,GAAgBb,EAAmD,CAC1E,IAAMY,EAAsB,CAAC,EAEvBwC,EAAgBpD,EAAS,IAAI,UAAU,EAK7C,GAJIoD,GAAe,WAAa,aAAcA,EAAc,WAC1DxC,EAAS,KAAKwC,EAAc,SAAoB,EAG9CA,GAAe,YAAY,MAC7B,QAAWT,KAAQS,EAAc,WAAW,MACtC,aAAcT,EAChB/B,EAAS,KAAK+B,CAAe,EACpB,aAAcA,GAAQA,EAAK,UAAU,OAC9C/B,EAAS,KAAK,GAAG+B,EAAK,SAAS,KAAK,EAK1C,IAAMD,EAAkB1C,EAAS,IAAI,YAAY,EACjD,GAAI0C,GAAiB,YAAY,MAC/B,QAAWC,KAAQD,EAAgB,WAAW,MACxC,aAAcC,GAAQA,EAAK,UAAU,OACvC/B,EAAS,KAAK,GAAG+B,EAAK,SAAS,KAAK,EAK1C,OAAO/B,CACT,CAEA,SAASG,GACPf,EACgB,CAChB,MAAO,CACL,aACGA,EAAS,IAAI,eAAe,GAAG,OAChC,aACF,cAAe,WAAWA,EAAS,IAAI,gBAAgB,GAAG,OAAS,GAAG,EACtE,cAAeA,EAAS,IAAI,gBAAgB,GAAG,QAAU,MAC3D,CACF,CASA,SAASiB,GACPjB,EACc,CACd,OAAO7B,GAAkB4D,GAAe/B,EAAU,eAAe,CAAC,CACpE,CAOA,SAASmB,GACPnB,EACmB,CACnB,IAAM5B,EAAM2D,GAAe/B,EAAU,sBAAsB,EAC3D,GAAI,CAAC,MAAM,QAAQ5B,CAAG,EAAG,OAAO,KAChC,IAAMiF,EAAqB,CAAC,EAC5B,QAAWjB,KAAShE,EACd,MAAM,QAAQgE,CAAK,EACrBiB,EAAO,KACLjB,EAAM,OAAQF,GAAmB,OAAOA,GAAM,QAAQ,CACxD,EAEAmB,EAAO,KAAK,CAAC,CAAC,EAGlB,OAAOA,CACT,CAOA,SAASjC,GACPpB,EACAsD,EACwB,CACxB,IAAMlF,EAAM2D,GAAe/B,EAAUsD,CAAG,EAIxC,GAAI,CAAClF,GAAO,OAAOA,GAAQ,UAAY,MAAM,QAAQA,CAAG,EAAG,MAAO,CAAC,EACnE,IAAMmF,EAAiC,CAAC,EACxC,OAAW,CAACC,EAAGvB,CAAC,IAAK,OAAO,QAAQ7D,CAAG,EAAG,CACxC,IAAMqF,EAAM,OAAOxB,GAAM,SAAWA,EAAI,OAAOA,CAAC,EAQ5C,OAAO,SAASwB,CAAG,GAAKA,GAAO,GAAKA,GAAO,KAC7CF,EAAOC,CAAC,EAAI,KAAK,MAAMC,CAAG,EAE9B,CACA,OAAOF,CACT,CAeA,SAAS5B,GACP3B,EACAsD,EACkE,CAClE,IAAMlF,EAAM2D,GAAe/B,EAAUsD,CAAG,EACxC,GAAI,CAAClF,GAAO,OAAOA,GAAQ,UAAY,MAAM,QAAQA,CAAG,EAAG,MAAO,CAAC,EACnE,IAAMqE,EACJ,CAAC,EACH,OAAW,CAACiB,EAAKC,CAAI,IAAK,OAAO,QAAQvF,CAAG,EAAG,CAC7C,GAAI,CAACuF,GAAQ,OAAOA,GAAS,UAAY,MAAM,QAAQA,CAAI,EAAG,SAC9D,GAAM,CAAE,IAAAC,EAAK,IAAAC,EAAK,SAAAC,CAAS,EAAIH,EAKzBI,EAAO,OAAOH,GAAQ,SAAWA,EAAM,OAAOA,CAAG,EACjDI,EAAO,OAAOH,GAAQ,SAAWA,EAAM,OAAOA,CAAG,EACvD,GAAI,CAAC,OAAO,SAASE,CAAI,GAAK,CAAC,OAAO,SAASC,CAAI,EAAG,SACtD,IAAMC,EAAa,KAAK,IAAI,EAAG,KAAK,IAAI,GAAI,KAAK,MAAMF,CAAI,CAAC,CAAC,EACvDG,EAAa,KAAK,IAAID,EAAY,KAAK,IAAI,GAAI,KAAK,MAAMD,CAAI,CAAC,CAAC,EACtEvB,EAAIiB,CAAG,EACLI,IAAa,GACT,CAAE,IAAKG,EAAY,IAAKC,EAAY,SAAU,EAAK,EACnD,CAAE,IAAKD,EAAY,IAAKC,CAAW,CAC3C,CACA,OAAOzB,CACT,CAEA,SAASpB,GACPrB,EACoB,CAEpB,OADYA,EAAS,IAAI,mBAAmB,GAAG,QAChC,WAAa,WAAa,KAC3C,CAEA,SAASsB,GAAetB,EAAkD,CACxE,OAAOuB,GAAsBvB,EAAU,SAAS,CAClD,CAIA,SAASuB,GACPvB,EACAsD,EACU,CACV,IAAMlF,EAAM4B,EAAS,IAAIsD,CAAG,GAAG,MAC/B,GAAI,CAAClF,EAAK,MAAO,CAAC,EAClB,GAAI,CACF,IAAM+F,EAAS,KAAK,MAAM/F,CAAG,EAC7B,OAAK,MAAM,QAAQ+F,CAAM,EAClBA,EAAO,OAAQlC,GAAmB,OAAOA,GAAM,QAAQ,EAD3B,CAAC,CAEtC,MAAQ,CACN,MAAO,CAAC,CACV,CACF,CASA,SAAST,GAAcxB,EAAgD,CACrE,IAAM5B,EAAM4B,EAAS,IAAI,WAAW,GAAG,MACvC,GAAI,CAAC5B,EAAK,MAAO,GACjB,IAAM+F,EAAS,OAAO,SAAS/F,EAAK,EAAE,EACtC,OAAI,OAAO,MAAM+F,CAAM,EAAU,EAC1B,KAAK,IAAI,IAAK,KAAK,IAAI,EAAGA,CAAM,CAAC,CAC1C,CAEA,SAAS1C,GACPzB,EACc,CACd,IAAM5B,EAAM2D,GAAe/B,EAAU,cAAc,EACnD,OAAK,MAAM,QAAQ5B,CAAG,EACfA,EACJ,OACEgG,GAAoC,OAAOA,GAAM,UAAYA,IAAM,IACtE,EACC,IAAIC,EAAkB,EALO,CAAC,CAMnC,CAMA,SAASA,GAAmBD,EAAwC,CAClE,IAAME,EAAS,OAAOF,EAAE,aAAe,CAAC,EAClCG,EAAmB,CAIvB,YAAa,OAAO,SAASD,CAAM,GAAKA,GAAU,EAAIA,EAAS,CACjE,EACA,OAAI,OAAOF,EAAE,YAAe,UAAY,OAAO,SAASA,EAAE,UAAU,IAClEG,EAAK,WAAaH,EAAE,YAElB,OAAOA,EAAE,QAAW,UAAY,OAAO,SAASA,EAAE,MAAM,IAC1DG,EAAK,OAASH,EAAE,QAEXG,CACT,CAEA,SAASxC,GACP/B,EACAsD,EACS,CACT,IAAMrE,EAAQe,EAAS,IAAIsD,CAAG,GAAG,MACjC,GAAI,CAACrE,EAAO,OAAO,KACnB,GAAI,CACF,OAAO,KAAK,MAAMA,CAAK,CACzB,MAAQ,CACN,OAAO,IACT,CACF,CAEA,SAASyC,GACP1B,EACAsD,EACAkB,EAA4B,CAAC,EACd,CACf,IAAMvF,EAAQe,EAAS,IAAIsD,CAAG,GAAG,MACjC,GAAI,CAACrE,EAAO,OAAO,KACnB,IAAMwE,EAAM,SAASxE,EAAO,EAAE,EAE9B,OADI,MAAMwE,CAAG,GACTe,EAAQ,MAAQ,QAAaf,EAAMe,EAAQ,IAAY,KACpDf,CACT,CC5gBO,SAASgB,GACdC,EACQ,CACR,IAAMjB,EAAM,OAAOiB,GAAS,SAAW,WAAWA,CAAI,EAAIA,EAC1D,OAAO,OAAOjB,GAAQ,UAAY,OAAO,SAASA,CAAG,GAAKA,EAAM,EAAIA,EAAM,CAC5E,CAgBO,SAASkB,GACdC,EACAF,EACG,CACH,IAAMG,EAAIJ,GAAsBC,CAAI,EACpC,GAAIG,IAAM,EAAG,OAAOD,EAEpB,IAAME,EAAY,CAAE,GAAGF,CAAO,EAE9B,OAAIA,EAAO,eAAe,eAAiB,eACzCE,EAAU,eAAiB,CACzB,GAAGF,EAAO,eACV,cAAeA,EAAO,eAAe,cAAgBC,CACvD,GAGE,gBAAiBD,GAAU,MAAM,QAAQA,EAAO,WAAW,IAC5DE,EAAyD,YACxDF,EAAO,YAAY,IAAKL,GACtB,OAAOA,EAAK,QAAW,SACnB,CAAE,GAAGA,EAAM,OAAQA,EAAK,OAASM,CAAE,EACnCN,CACN,GAGGO,CACT,CEgBO,SAASC,GACdC,EACAC,EACQ,CACR,IAAIC,EAAO,EACPC,EAAU,EACd,QAASC,EAAI,EAAGA,EAAIJ,EAAM,OAAQI,IAAK,CACrC,IAAMC,EACJJ,IAAiB,eACbD,EAAMI,CAAC,EAAE,QAAU,EACnBJ,EAAMI,CAAC,EAAE,YAAc,EACzBC,EAAUH,IACZA,EAAOG,EACPF,EAAUC,EAEd,CACA,OAAOD,CACT,CAQO,SAASG,GACdC,EACAC,EACAC,EACQ,CACR,OAAID,GAAa,EAAU,EACvBD,IAAgB,aAAqB,KAAK,IAAIE,EAAWD,EAAY,CAAC,EAEnE,KAAK,IAAI,EAAG,KAAK,IAAI,KAAK,MADvB,OAAOD,GAAgB,SAAWA,EAAc,CAClB,EAAGC,EAAY,CAAC,CAAC,CAC3D,CASO,SAASE,GACdC,EACAC,EACAH,EACQ,CACR,OAAKG,EACD,OAAOD,GAAc,UAAY,OAAO,SAASA,CAAS,EACrD,KAAK,MAAMA,CAAS,EAEtBF,EAJc,EAKvB,CCxGA,IAAMI,GAAW,IAGjB,SAASC,GAAMC,EAAoB,CACjC,OAAOA,EAAG,MAAM,GAAG,EAAE,IAAI,GAAKA,CAChC,CAEA,SAASC,GACPC,EACAC,EACAC,EACS,CACT,GAAI,CAACF,GAAQA,EAAK,SAAW,EAAG,MAAO,GACvC,GAAIA,EAAK,SAASJ,EAAQ,EAAG,MAAO,GACpC,GAAI,CAACK,EAAW,MAAO,GACvB,GAAI,CAACC,EAAmB,OAAOF,EAAK,SAASC,CAAS,EACtD,IAAME,EAAMN,GAAMI,CAAS,EAC3B,OAAOD,EAAK,KAAMF,GAAOD,GAAMC,CAAE,IAAMK,CAAG,CAC5C,CAYO,SAASC,GACdC,EACAC,EACS,CAST,GARID,EAAO,mBAAqB,YAG5BN,GAAYM,EAAO,aAAcC,EAAQ,YAAa,EAAK,GAM7DA,EAAQ,mBACRP,GAAYM,EAAO,mBAAoBC,EAAQ,kBAAmB,EAAI,EAEtE,MAAO,GAGT,GAAIA,EAAQ,SAAU,CACpB,IAAMH,EAAMN,GAAMS,EAAQ,QAAQ,EAClC,GAAID,EAAO,UAAU,KAAMP,GAAOD,GAAMC,CAAE,IAAMK,CAAG,EAAG,MAAO,EAC/D,CACA,MAAO,EACT,CCrEA,SAASI,GAAiBC,EAAaC,EAAsB,CAC3D,OAAID,IAAQC,EAAY,GACjB,QAAQ,KAAKA,CAAG,GAAKD,EAAI,SAAS,IAAIC,CAAG,EAAE,CACpD,CAaO,SAASC,GAEdC,EAAwBC,EAAyC,CACjE,IAAMC,EAAKD,GAAS,WAAW,KAAK,EACpC,GAAIC,EAAI,CACN,IAAMC,EAAOH,EAAS,KAAMI,GAAMR,GAAiBQ,EAAE,GAAIF,CAAE,CAAC,EAC5D,GAAIC,EAAM,OAAOA,CACnB,CAEA,IAAME,EAASJ,GAAS,eAAe,KAAK,EAC5C,GAAII,EAAQ,CACV,IAAMC,EAAWN,EAAS,KAAMI,GAAMA,EAAE,SAAWC,CAAM,EACzD,GAAIC,EAAU,OAAOA,CACvB,CAEA,OAAON,EAAS,CAAC,CACnB,CExBO,SAASO,GACdC,EACAC,EACAC,EACQ,CACR,IAAMC,EAAKH,EAAO,kBAAkBE,CAAS,EAC7C,OAAIC,IAAO,OAAkBA,EACtBH,EAAO,kBAAkBC,CAAS,GAAK,CAChD,CCFO,SAASG,GACdC,EACAC,EACW,CACX,IAAMC,EAAOF,EAAO,MAAMC,CAAS,EACnC,GAAI,CAACC,EAAM,MAAO,CAAC,EAEnB,IAAMC,EAAS,IAAI,IAAID,EAAK,UAAU,EAChCE,EAAkB,IAAI,IAC5B,QAAWC,KAAgBH,EAAK,cAC9B,QAAWI,KAAaN,EAAO,qBAAqBK,CAAY,GAAK,CAAC,EACpED,EAAgB,IAAIE,CAAS,EAIjC,IAAMC,EAAO,IAAI,IACXC,EAAoB,CAAC,EAC3B,QAAWC,KAAWT,EAAO,SACvBO,EAAK,IAAIE,EAAQ,EAAE,GACnB,CAACN,EAAO,IAAIM,EAAQ,EAAE,GAAK,CAACL,EAAgB,IAAIK,EAAQ,EAAE,IAC9DF,EAAK,IAAIE,EAAQ,EAAE,EACnBD,EAAO,KAAKC,CAAO,GAErB,OAAOD,CACT,CCnDA,eAAsBE,GACpBC,EACAC,EAKe,CACf,MAAMC,GAAUF,EAAQ,CACtB,WAAYA,EAAO,WACnB,UAAW,oBACX,UAAWC,EAAM,UACjB,WAAYA,EAAM,WAClB,UAAWA,EAAM,UACjB,WAAY,IAAI,KAAK,EAAE,YAAY,CACrC,CAAC,CACH,CAEA,eAAsBE,GACpBH,EACAC,EAee,CACf,MAAMC,GAAUF,EAAQ,CACtB,WAAYA,EAAO,WACnB,UAAW,qBACX,GAAGC,EACH,WAAY,IAAI,KAAK,EAAE,YAAY,CACrC,CAAC,CACH,CAMO,SAASG,GACdC,EACAC,EACY,CACZ,GAAI,OAAO,qBAAyB,IAElC,OAAAA,EAAS,EACF,IAAM,CAAC,EAGhB,IAAMC,EAAW,IAAI,qBAClBC,GAAY,CACX,QAAWC,KAASD,EACdC,EAAM,iBACRH,EAAS,EACTC,EAAS,UAAUE,EAAM,MAAM,EAGrC,EACA,CAAE,UAAW,EAAI,CACnB,EAEA,OAAAF,EAAS,QAAQF,CAAO,EACjB,IAAME,EAAS,WAAW,CACnC,CAIA,eAAeL,GACbF,EACAU,EACe,CACf,IAAMC,EAAM,GAAGX,EAAO,MAAM,iBACtBY,EAAO,KAAK,UAAUF,CAAO,EAEnC,GAAI,EACe,MAAM,MAAMC,EAAK,CAChC,OAAQ,OACR,QAAS,CAAE,eAAgB,kBAAmB,EAC9C,KAAAC,CACF,CAAC,GAEa,KAEZ,MAAM,IAAI,QAASC,GAAM,WAAWA,EAAG,GAAI,CAAC,EAC5C,MAAM,MAAMF,EAAK,CACf,OAAQ,OACR,QAAS,CAAE,eAAgB,kBAAmB,EAC9C,KAAAC,CACF,CAAC,EAEL,MAAQ,CAEN,GAAI,CAEE,OAAO,UAAc,KAAe,UAAU,YAChD,UAAU,WAAWD,EAAKC,CAAI,CAElC,MAAQ,CAER,CACF,CACF,CCrGO,IAAME,GAAiB,IAExBC,GAAoD,CACxD,CAAC,WAAY,eAAe,EAC5B,CAAC,YAAa,uBAAuB,EACrC,CAAC,mBAAoB,iBAAiB,EACtC,CAAC,gBAAiB,cAAc,EAChC,CAAC,mBAAoB,iBAAiB,EACtC,CAAC,gBAAiB,mBAAmB,CACvC,EAEMC,GAAiB,iCAMhB,SAASC,GAAkBC,EAA6B,CAC7D,GAAIA,EAAI,OAASJ,GACf,MAAO,CACL,GAAI,GACJ,MAAO,eAAeA,GAAe,eAAe,OAAO,CAAC,kBAC9D,EAGF,IAAIK,EAAMD,EAAI,QAAQ,oBAAqB,EAAE,EAE7C,GAAI,CAGFC,EAAMA,EAAI,QAAQ,kCAAmC,CAACC,EAAGC,IAAQ,CAC/D,IAAMC,EAAY,SAASD,EAAK,EAAE,EAClC,OAAIC,EAAY,GAAKA,EAAY,QAAiB,SAC3C,OAAO,cAAcA,CAAS,CACvC,CAAC,EAEDH,EAAMA,EAAI,QAAQ,WAAY,EAAE,EAGhCA,EAAMA,EAAI,QAAQ,0BAA2B,IAAI,CACnD,MAAQ,CACN,MAAO,CACL,GAAI,GACJ,MAAO,+CACT,CACF,CAEAA,EAAMA,EAAI,QAAQ,KAAM,EAAE,EAAE,QAAQ,KAAM,EAAE,EAE5C,OAAW,CAACI,EAASC,CAAK,IAAKT,GAC7B,GAAIQ,EAAQ,KAAKJ,CAAG,EAClB,MAAO,CAAE,GAAI,GAAO,MAAO,iCAAiCK,CAAK,EAAG,EASxE,IAAMC,EAAa,+BACfC,EACJ,MAAQA,EAAWD,EAAW,KAAKN,CAAG,KAAO,MAAM,CACjD,IAAIQ,EAAWD,EAAS,CAAC,EAAE,KAAK,EAShC,IAPIC,EAAS,WAAW,GAAG,GAAKA,EAAS,WAAW,GAAG,KACrDA,EAAWA,EAAS,MAAM,CAAC,IAEzBA,EAAS,SAAS,GAAG,GAAKA,EAAS,SAAS,GAAG,KACjDA,EAAWA,EAAS,MAAM,EAAG,EAAE,GAEjCA,EAAWA,EAAS,KAAK,EACrBA,GAAY,CAACX,GAAe,KAAKW,CAAQ,EAC3C,MAAO,CACL,GAAI,GACJ,MAAO,sDACT,CAEJ,CAEA,MAAO,CAAE,GAAI,GAAM,IAAAR,CAAI,CACzB,CCvFA,IAAMS,GAAkB,iBAUjB,SAASC,GACdC,EACAC,EACS,CAET,GADI,OAAO,SAAa,KACpB,CAACA,EAAQ,MAAO,GAEpB,IAAMC,EAAYf,GAAkBc,CAAM,EAE1C,GADI,CAACC,EAAU,IACX,CAACA,EAAU,IAAI,KAAK,EAAG,MAAO,GAElC,IAAMC,EAAKL,GAAkBM,GAAWJ,CAAU,EAE9CK,EAAQ,SAAS,eAAeF,CAAE,EACtC,OAAKE,IACHA,EAAQ,SAAS,cAAc,OAAO,EACtCA,EAAM,GAAKF,EACXE,EAAM,aAAa,oBAAqB,YAAY,EACpD,SAAS,KAAK,YAAYA,CAAK,GAK7BA,EAAM,cAAgBH,EAAU,MAClCG,EAAM,YAAcH,EAAU,KAEzB,EACT,CAGA,SAASE,GAAWE,EAAuB,CACzC,IAAIC,EAAO,WACX,QAASC,EAAI,EAAGA,EAAIF,EAAM,OAAQE,IAChCD,GAAQD,EAAM,WAAWE,CAAC,EAC1BD,EAAO,KAAK,KAAKA,EAAM,QAAU,EAEnC,OAAQA,IAAS,GAAG,SAAS,EAAE,CACjC,CCnDO,SAASE,GAAYC,EAAyBC,EAA8B,CACjF,IAAMC,EAAM,OAAOF,GAAW,SAAW,WAAWA,CAAM,EAAIA,EAE9D,GAAI,CACF,OAAO,IAAI,KAAK,aAAa,OAAW,CACtC,MAAO,WACP,SAAUC,CACZ,CAAC,EAAE,OAAOC,CAAG,CACf,MAAQ,CACN,MAAO,GAAGD,CAAY,IAAIC,EAAI,QAAQ,CAAC,CAAC,EAC1C,CACF,CAEO,SAASC,GACdC,EACAC,EACAC,EACQ,CACR,OAAID,IAAiB,aACZ,KAAK,IAAI,EAAGD,GAAS,EAAIE,EAAgB,IAAI,EAE/C,KAAK,IAAI,EAAGF,EAAQE,CAAa,CAC1C,CAEA,IAAMC,GAAuD,CAC3D,GAAI,KACJ,GAAI,KACJ,KAAM,QACN,GAAI,KACJ,IAAK,SACL,EAAG,IACH,IAAK,MACL,GAAI,KACJ,KAAM,OACN,GAAI,KACJ,EAAG,IACH,GAAI,KACJ,EAAG,IACH,GAAI,QACJ,GAAI,QACJ,GAAI,KACJ,GAAI,KACJ,GAAI,KACJ,GAAI,KACJ,GAAI,KACJ,GAAI,KACJ,QAAS,GACT,GAAI,IACN,EAaO,SAASC,GACdC,EACAC,EACAC,EACe,CACf,GAAI,CAACF,GAAa,CAACC,GAAeA,EAAY,gBAAkB,UAC9D,OAAO,KAMT,IAAMV,EAAS,WAAWS,EAAU,MAAM,EAC1C,GAAI,CAAC,OAAO,SAAST,CAAM,EAAG,OAAO,KAKrC,IAAMhB,EAAQuB,GAAWG,EAAY,aAAa,GAAK,GACvD,GAAI,CAAC1B,EAAO,OAAO,KAEnB,IAAM4B,EAAWH,EAAU,cAAgBE,GAAoB,MACzDE,EAAYd,GAAYC,EAAQY,CAAQ,EACxCE,EAAQJ,EAAY,eACpBK,EAAkBD,IAAU,EAAI9B,EAAQ,GAAG8B,CAAK,GAAG9B,CAAK,GAC9D,MAAO,GAAG6B,CAAS,IAAIE,CAAe,EACxC,CIhGA,IAAAC,GAAA,CAAA,EAAAC,GAAAD,GAAA,CAAA,gBAAA,IAAAE,GAAA,oBAAA,IAAAC,GAAA,aAAA,IAAAC,GAAA,UAAA,IAAAC,GAAA,kBAAA,IAAAC,EAAA,CAAA,ECaO,IAAMC,GAAyB,EA8B/B,SAASL,GAAgB,CAC9B,QAAAM,EACA,eAAAC,EACA,cAAAC,EACA,OAAAC,EAASJ,GACT,KAAAK,CACF,EAA+C,CAC7C,IAAMC,EAAaD,EACf,KAAK,IAAIH,EAAgBG,EAAK,MAAM,EACpCH,EACEK,EAAaF,EAAO,KAAK,IAAI,EAAGA,EAAK,GAAG,EAAI,EAC5CG,EAAa,KAAK,IAAI,EAAGF,EAAaL,EAAQ,OAASG,CAAM,EAC7DK,EAAa,KAAK,IAAI,EAAGR,EAAQ,IAAMM,EAAaH,CAAM,EAI1DM,EACJP,GAAiBK,EACb,OACAC,EAAaD,EACX,KACA,OAOFG,EAAY,KAAK,IAAIR,EALTO,IAAc,OAASF,EAAaC,CAKH,EAE7CG,EACJF,IAAc,OACVT,EAAQ,OAASG,EACjBH,EAAQ,IAAMG,EAASO,EAE7B,MAAO,CACL,UAAAD,EACA,UAAAC,EACA,UAAAC,EACA,WAAYX,EAAQ,KACpB,MAAOA,EAAQ,KACjB,CACF,CC1BA,IAAMY,GAA8B,CAClC,KAAM,cACN,eAAgB,EAClB,EAOO,SAAShB,GACdiB,EACQ,CACR,QAASC,EAAI,EAAGA,EAAID,EAAQ,OAAQC,IAClC,GAAI,CAACD,EAAQC,CAAC,EAAE,SAAU,OAAOA,EAEnC,MAAO,EACT,CAEA,SAASC,GAAYF,EAAuD,CAC1E,QAASC,EAAID,EAAQ,OAAS,EAAGC,GAAK,EAAGA,IACvC,GAAI,CAACD,EAAQC,CAAC,EAAE,SAAU,OAAOA,EAEnC,MAAO,EACT,CAEA,SAASE,GACPH,EACAI,EACQ,CACR,GAAIJ,EAAQ,SAAW,EAAG,MAAO,GACjC,QAASK,EAAO,EAAGA,GAAQL,EAAQ,OAAQK,IAAQ,CACjD,IAAMC,GAAOF,EAAOC,GAAQL,EAAQ,OACpC,GAAI,CAACA,EAAQM,CAAG,EAAE,SAAU,OAAOA,CACrC,CACA,OAAOF,CACT,CAEA,SAASG,GACPP,EACAI,EACQ,CACR,GAAIJ,EAAQ,SAAW,EAAG,MAAO,GACjC,QAASK,EAAO,EAAGA,GAAQL,EAAQ,OAAQK,IAAQ,CACjD,IAAMC,GAAOF,EAAOC,EAAOL,EAAQ,QAAUA,EAAQ,OACrD,GAAI,CAACA,EAAQM,CAAG,EAAE,SAAU,OAAOA,CACrC,CACA,OAAOF,CACT,CAEA,SAASI,GAAYC,EAAsB,CACzC,OAAOA,EAAI,SAAW,GAAKA,IAAQ,KAAO,KAAK,KAAKA,CAAG,CACzD,CAEO,SAASzB,GACd0B,EACAC,EACgB,CAEhB,GAAID,EAAM,SAAWA,EAAM,SAAWA,EAAM,OAAQ,OAAOX,GAE3D,GAAM,CAAE,IAAAU,CAAI,EAAIC,EACV,CAAE,OAAAE,EAAQ,YAAAC,EAAa,cAAAC,EAAe,QAAAd,CAAQ,EAAIW,EAGxD,GAAI,CAACC,EACH,OAAQH,EAAK,CACX,IAAK,QACL,IAAK,IACL,IAAK,YACH,MAAO,CACL,KAAM,OACN,YACEK,GAAiB,GAAK,CAACd,EAAQc,CAAa,GAAG,SAC3CA,EACA/B,GAAaiB,CAAO,EAC1B,eAAgB,EAClB,EACF,IAAK,UACH,MAAO,CACL,KAAM,OACN,YACEc,GAAiB,GAAK,CAACd,EAAQc,CAAa,GAAG,SAC3CA,EACAZ,GAAYF,CAAO,EACzB,eAAgB,EAClB,EACF,QACE,OAAIQ,GAAYC,CAAG,EACV,CACL,KAAM,OACN,YACEK,GAAiB,GAAK,CAACd,EAAQc,CAAa,GAAG,SAC3CA,EACA/B,GAAaiB,CAAO,EAC1B,eAAgB,EAClB,EAEKD,EACX,CAIF,OAAQU,EAAK,CACX,IAAK,YACH,MAAO,CACL,KAAM,cACN,YAAaN,GAAYH,EAASa,CAAW,EAC7C,eAAgB,EAClB,EACF,IAAK,UACH,MAAO,CACL,KAAM,cACN,YAAaN,GAAYP,EAASa,CAAW,EAC7C,eAAgB,EAClB,EACF,IAAK,OACH,MAAO,CACL,KAAM,cACN,YAAa9B,GAAaiB,CAAO,EACjC,eAAgB,EAClB,EACF,IAAK,MACH,MAAO,CACL,KAAM,cACN,YAAaE,GAAYF,CAAO,EAChC,eAAgB,EAClB,EACF,IAAK,QACL,IAAK,IACH,OAAIa,GAAe,GAAK,CAACb,EAAQa,CAAW,GAAG,SACtC,CACL,KAAM,SACN,MAAOA,EACP,eAAgB,EAClB,EAMK,CAAE,KAAM,cAAe,eAAgB,EAAK,EACrD,IAAK,SACH,MAAO,CACL,KAAM,QACN,OAAQ,GACR,aAAc,GACd,eAAgB,EAClB,EACF,IAAK,MAKH,MAAO,CACL,KAAM,QACN,OAAQ,GACR,aAAc,GACd,eAAgB,EAClB,EACF,QACE,OAAIL,GAAYC,CAAG,EACV,CAAE,KAAM,aAAc,KAAMA,EAAK,eAAgB,EAAM,EAEzDV,EACX,CACF,CCxNO,IAAMgB,GAAsB,IAY5B,SAASjC,IAAsC,CACpD,MAAO,CAAE,OAAQ,GAAI,SAAU,CAAE,CACnC,CAOO,SAASG,GACd0B,EACAK,EACAC,EACAjB,EACAkB,EAAkBH,GACF,CAChB,GAAIC,EAAK,SAAW,EAClB,MAAO,CAAE,SAAUL,EAAO,aAAc,IAAK,EAI/C,IAAMQ,GADUF,EAAMN,EAAM,SAAWO,EACb,GAAKP,EAAM,QAAUK,EAAK,YAAY,EAC1DI,EAA2B,CAAE,OAAAD,EAAQ,SAAUF,CAAI,EAEzD,QAAShB,EAAI,EAAGA,EAAID,EAAQ,OAAQC,IAAK,CACvC,IAAMoB,EAAMrB,EAAQC,CAAC,EACrB,GAAI,CAAAoB,EAAI,UACJA,EAAI,MAAM,YAAY,EAAE,WAAWF,CAAM,EAC3C,MAAO,CAAE,SAAAC,EAAU,aAAcnB,CAAE,CAEvC,CAEA,MAAO,CAAE,SAAAmB,EAAU,aAAc,IAAK,CACxC,CCtDA,IAAAE,GAAA,CAAA,EAAA1C,GAAA0C,GAAA,CAAA,qBAAA,IAAAC,GAAA,uBAAA,IAAAC,GAAA,gBAAA,IAAAC,EAAA,CAAA,ECqDA,SAASC,GAAiBC,EAA0C,CAClE,OACEA,EAAQ,kBACR,CAACA,EAAQ,qBACTA,EAAQ,mBAAqB,MAC7BA,EAAQ,mBAAqB,CAEjC,CA6BO,SAASC,GACdD,EACAE,EACS,CACT,OAAKF,EAAQ,iBACTA,EAAQ,qBACRA,EAAQ,mBAAqB,MAC7BD,GAAiBC,CAAO,EAAU,GAC/BA,EAAQ,mBAAqBE,EAJE,EAKxC,CAUO,SAASC,GACdH,EACe,CAGf,OAFIA,EAAQ,qBACRA,EAAQ,mBAAqB,MAC7BD,GAAiBC,CAAO,EAAU,KAC/B,KAAK,IAAI,EAAGA,EAAQ,iBAAiB,CAC9C,CDvFO,SAASI,GACdC,EACAC,EACU,CACV,GAAID,EAAS,SAAW,EAAG,OAAO,KAClC,QAAWE,KAAKF,EAAU,CACxB,GAAIE,EAAE,aAAa,SAAWD,EAAa,OAAQ,SACnD,IAAIE,EAAQ,GACZ,QAASC,EAAI,EAAGA,EAAIF,EAAE,aAAa,OAAQE,IACzC,GAAIF,EAAE,aAAaE,CAAC,IAAMH,EAAaG,CAAC,EAAG,CACzCD,EAAQ,GACR,KACF,CAEF,GAAIA,EAAO,OAAOD,CACpB,CACA,OAAO,IACT,CAQO,SAASG,GACdL,EACAM,EACAC,EACAC,EACS,CACT,QAAWN,KAAKF,EAAU,CAExB,GADI,CAACE,EAAE,WACHA,EAAE,aAAaI,CAAW,IAAMC,EAAO,SAC3C,IAAIE,EAAK,GACT,QAASL,EAAI,EAAGA,EAAIF,EAAE,aAAa,OAAQE,IACzC,GAAIA,IAAME,GACNJ,EAAE,aAAaE,CAAC,IAAMI,EAASJ,CAAC,EAAG,CACrCK,EAAK,GACL,KACF,CAEF,GAAIA,EAAI,MAAO,EACjB,CACA,MAAO,EACT,CAYO,SAASC,GAQdC,EAAYC,EAAqC,CACjD,MAAO,CACL,GAAID,EAAQ,GACZ,aAAcA,EAAQ,gBAAgB,IAAKE,GAAMA,EAAE,KAAK,EACxD,UACED,IAAgB,OACZE,GAAcH,EAASC,CAAW,EAClCD,EAAQ,gBAChB,CACF,CEzEO,IAAMI,GAAiB,QAG1BC,GAAmC,KAQhC,SAASC,GAAWC,EAAuB,CAChD,IAAIC,EAAO,WACX,QAASC,EAAI,EAAGA,EAAIF,EAAM,OAAQE,IAChCD,GAAQD,EAAM,WAAWE,CAAC,EAG1BD,IACGA,GAAQ,IAAMA,GAAQ,IAAMA,GAAQ,IAAMA,GAAQ,IAAMA,GAAQ,IACnEA,KAAU,EAEZ,OAAOA,IAAS,CAClB,CAQO,SAASE,IAAuB,CACrC,GAAI,CACF,IAAMC,EAAS,OAAO,aAAa,QAAQP,EAAc,EACzD,GAAIO,EAAQ,OAAOA,EACnB,IAAMC,EAAQC,GAAS,EACvB,cAAO,aAAa,QAAQT,GAAgBQ,CAAK,EAC1CA,CACT,MAAe,CACb,OAAKP,KAAmBA,GAAoBQ,GAAS,GAC9CR,EACT,CACF,CAEA,SAASQ,IAAmB,CAG1B,OACE,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,EAAG,EAAE,EACtC,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,EAAG,EAAE,CAE1C,CA6BO,SAASC,GACdC,EACAC,EACAC,EACG,CACH,IAAMC,EAAUH,EACb,MAAM,EACN,KAAK,CAAC,EAAGI,IAAO,EAAE,GAAKA,EAAE,GAAK,GAAK,EAAE,GAAKA,EAAE,GAAK,EAAI,CAAE,EAEpDC,EAAOd,GAAWU,EAAY,IAAMC,CAAM,EAAI,IAEhDI,EAAM,EACV,QAAWC,KAAUJ,EAEnB,GADAG,GAAO,KAAK,IAAI,EAAGC,EAAO,QAAQ,EAC9BF,EAAOC,EAAK,OAAOC,EAMzB,OAAOJ,EAAQA,EAAQ,OAAS,CAAC,CACnC,CAYO,SAASK,GACdC,EACAR,EACK,CACL,IAAMS,EAAS,IAAI,IACnB,QAAWC,KAAUF,EAAS,CAC5B,GAAI,CAACE,EAAO,SAAU,SACtB,IAAMC,EAAWF,EAAO,IAAIC,EAAO,QAAQ,EACvCC,EAAUA,EAAS,KAAKD,CAAM,EAC7BD,EAAO,IAAIC,EAAO,SAAU,CAACA,CAAM,CAAC,CAC3C,CAGA,GAAID,EAAO,OAAS,EAAG,OAAOD,EAAQ,MAAM,EAE5C,IAAMI,EAAS,IAAI,IACnB,OAAAH,EAAO,QAAQ,CAACV,EAASE,IAAW,CAGlC,GAAIF,EAAQ,SAAW,EAAG,CACxBa,EAAO,IAAIX,EAAQF,EAAQ,CAAC,CAAC,EAC7B,MACF,CACAa,EAAO,IAAIX,EAAQH,GAAWC,EAASC,EAAWC,CAAM,CAAC,CAC3D,CAAC,EAEMO,EAAQ,OACZE,GAAW,CAACA,EAAO,UAAYE,EAAO,IAAIF,EAAO,QAAQ,IAAMA,CAClE,CACF,CCzHO,SAASG,GAAgBC,EAAmC,CACjE,OAAQC,GAAU,CAChB,GAAI,CACF,OAAO,IAAI,KAAK,aAAa,OAAW,CACtC,MAAO,WACP,SAAUD,CACZ,CAAC,EAAE,OAAOC,EAAQ,GAAG,CACvB,MAAQ,CAEN,OAAOD,EAAe,KAAOC,EAAQ,KAAK,QAAQ,CAAC,CACrD,CACF,CACF,CAUO,IAAMC,GAAuB,mBACvBC,GAAwB,oBAkBxBC,GAAwB,oBACxBC,GAAkB,MAExB,SAASC,GACdC,EACAC,EACuC,CACvC,MAAO,CACL,CAAE,IAAKN,GAAsB,MAAOK,CAAU,EAC9C,CAAE,IAAKJ,GAAuB,MAAOK,CAAW,CAClD,CACF,CChFO,SAASC,GAAUC,EAA8B,CACtD,IAAMC,EAAOD,EAAI,MAAMA,EAAI,YAAY,GAAG,EAAI,CAAC,EACzC,EAAI,OAAOC,CAAI,EACrB,OAAO,OAAO,SAAS,CAAC,GAAKA,IAAS,GAAK,EAAID,CACjD,CAGO,SAASE,GAAQC,EAA2C,CACjE,GAAI,CAACA,EAAQ,MAAO,GACpB,IAAMC,EAAI,WAAWD,CAAM,EAC3B,OAAO,OAAO,SAASC,CAAC,EAAI,KAAK,MAAMA,EAAI,GAAG,EAAI,CACpD,CAOA,SAASC,GACPC,EACe,CACf,IAAMC,EAAID,GAAO,MACXE,EAAIF,GAAO,OACjB,OAAI,OAAOC,GAAM,UAAY,OAAOC,GAAM,UAAYD,GAAK,GAAKC,GAAK,EAC5D,KAEFD,EAAIC,CACb,CAYA,SAASC,GACPC,EACAC,EACAC,EACe,CACf,MAAO,CACL,GAAIb,GAAUW,EAAE,EAAE,EAClB,MAAOA,EAAE,MAET,SAAUA,EAAE,iBAAmB,CAAC,GAAG,IAAKG,GAAMA,EAAE,KAAK,EAGrD,UAAWC,GAAqBJ,EAAGC,CAAW,EAC9C,MAAOT,GAAQQ,EAAE,OAAO,MAAM,EAC9B,eAAgBR,GAAQQ,EAAE,gBAAgB,MAAM,EAChD,UAAWK,GAAgBL,EAAE,UAAWA,EAAE,qBAAsBE,CAAgB,EAChF,MAAOF,EAAE,OAAO,KAAO,KAGvB,kBAAmBM,GAAmBN,CAAC,CACzC,CACF,CASO,SAASO,GACdC,EACAC,EACAC,EACe,CACf,IAAMC,EAAUD,EAAK,kBACfE,EAAQJ,EAAQ,SAAS,MAAM,OAClCR,GAAM,CAACW,GAAWA,EAAQ,SAAW,GAAKA,EAAQ,SAASX,EAAE,EAAE,CAClE,EAEMa,EAAWL,EAAQ,WAAW,gBAAgB,aAC9CM,EAAaC,GACjBL,EAAK,WACLF,EAAQ,GACRI,EAAM,CAAC,GAAG,IAAM,EAClB,EAEMI,EAAWJ,EAAM,IAAKZ,GAAM,CAChC,IAAMiB,EAAMF,GAAiBL,EAAK,WAAYF,EAAQ,GAAIR,EAAE,EAAE,EACxDkB,EAAUnB,GAAaC,EAAGiB,EAAKJ,CAAQ,EAI7C,OAAII,IAAQH,IAAYI,EAAQ,SAAWD,GACpCC,CACT,CAAC,EAEKC,EAAUH,EAAS,KAAMhB,GAAMA,EAAE,SAAS,GAAKgB,EAAS,CAAC,EAE/D,MAAO,CACL,UAAW3B,GAAUmB,EAAQ,EAAE,EAC/B,MAAOA,EAAQ,MACf,IAAKA,EAAQ,OAAS,aAAeA,EAAQ,OAAS,KACtD,cAAeA,EAAQ,eAAe,KAAO,KAC7C,mBAAoBb,GAAiBa,EAAQ,aAAa,EAC1D,SAAUM,EACV,kBAAmBK,EAAUA,EAAQ,GAAK,GAG1C,aAAcP,EAAM,CAAC,GAAG,iBAAmB,CAAC,GAAG,IAAKT,GAAMA,EAAE,IAAI,EAChE,SAAAa,CACF,CACF,CAGO,SAASI,GAAcC,EAMV,CAClB,OAAOA,EAAO,SAAS,IAAI,CAACb,EAASc,IACnCf,GAAaC,EAASa,EAAO,GAAI,CAC/B,WAAY,CACV,kBAAmBA,EAAO,kBAC1B,kBAAmBA,EAAO,iBAC5B,EACA,kBAAmBA,EAAO,qBAAqBC,CAAG,GAAK,IACzD,CAAC,CACH,CACF,CC9IO,SAASC,GACdC,EACAC,EACe,CACf,QAASC,EAAI,EAAGA,EAAIF,EAAQ,SAAS,OAAQE,IAC3C,GAAI,OAAOF,EAAQ,SAASE,CAAC,EAAE,EAAE,IAAM,OAAOD,CAAS,EACrD,OAAOD,EAAQ,SAASE,CAAC,EAG7B,OAAOF,EAAQ,SAAS,CAAC,CAC3B,CAcO,SAASG,GACdH,EACsB,CACtB,IAAMI,EAASL,GAAYC,EAASA,EAAQ,iBAAiB,EAC7D,GAAII,GAAUA,EAAO,YAAc,GAAO,OAAOA,EACjD,QAASF,EAAI,EAAGA,EAAIF,EAAQ,SAAS,OAAQE,IAC3C,GAAIF,EAAQ,SAASE,CAAC,EAAE,YAAc,GAAO,OAAOF,EAAQ,SAASE,CAAC,EAExE,OAAO,IACT,CAQO,SAASG,GACdL,EACAM,EACsB,CACtB,GAAI,CAACN,EAAQ,UAAY,CAACM,EAAc,OAAO,KAC/C,QAASJ,EAAI,EAAGA,EAAIF,EAAQ,SAAS,OAAQE,IAAK,CAChD,IAAMK,EAAIP,EAAQ,SAASE,CAAC,EAC5B,GAAI,CAACK,EAAE,SAAWA,EAAE,QAAQ,SAAWD,EAAa,OAAQ,SAC5D,IAAIE,EAAQ,GACZ,QAASC,EAAI,EAAGA,EAAIF,EAAE,QAAQ,OAAQE,IACpC,GAAIF,EAAE,QAAQE,CAAC,IAAMH,EAAaG,CAAC,EAAG,CACpCD,EAAQ,GACR,KACF,CAEF,GAAIA,EAAO,OAAOD,CACpB,CACA,OAAO,IACT,CAWO,SAASG,GACdC,EACAC,EACAC,EACAC,EACS,CACT,QAASZ,EAAI,EAAGA,EAAIS,EAAS,OAAQT,IAAK,CACxC,IAAMK,EAAII,EAAST,CAAC,EAEpB,GADIK,EAAE,YAAc,IAChB,CAACA,EAAE,SAAWA,EAAE,QAAQK,CAAW,IAAMC,EAAO,SACpD,IAAIE,EAAK,GACT,QAASN,EAAI,EAAGA,EAAIF,EAAE,QAAQ,OAAQE,IACpC,GAAIA,IAAMG,GACNL,EAAE,QAAQE,CAAC,IAAMK,EAASL,CAAC,EAAG,CAChCM,EAAK,GACL,KACF,CAEF,GAAIA,EAAI,MAAO,EACjB,CACA,MAAO,EACT,CC1FO,SAASC,GAAeC,EAAuC,CACpE,IAAMC,EAAoB,CAAC,EAC3B,QAASC,EAAI,EAAGA,EAAIF,EAAS,OAAQE,IAAK,CACxC,IAAMC,EAAUC,GAAYJ,EAASE,CAAC,EAAGF,EAASE,CAAC,EAAE,iBAAiB,EAChEG,EAAMF,EAAQ,UAAYH,EAASE,CAAC,EAAE,UAAY,EACxDD,EAAM,KAAK,CACT,UAAW,OAAOD,EAASE,CAAC,EAAE,iBAAiB,EAC/C,SAAUG,EACV,YAAaF,EAAQ,OAAS,GAAKE,CACrC,CAAC,CACH,CACA,OAAOJ,CACT,CCQO,SAASK,GACdC,EACAC,EACAC,EACe,CACf,IAAIC,EAAa,EACbC,EAAY,EAEhB,QAAS,EAAI,EAAG,EAAIJ,EAAS,OAAQ,IAAK,CACxC,IAAMK,EAAUC,GAAYN,EAAS,CAAC,EAAGA,EAAS,CAAC,EAAE,iBAAiB,EAChEO,EAAMF,EAAQ,UAAYL,EAAS,CAAC,EAAE,UAAY,EAClDQ,EAAYH,EAAQ,MAAQE,EAGlC,GAFAJ,GAAcK,EAEVP,IAAiB,aAAc,CACjC,IAAMQ,EAAe,KAAK,MAAOJ,EAAQ,MAAQH,EAAiB,GAAG,EACrEE,IAAcC,EAAQ,MAAQI,GAAgBF,CAChD,MAEEH,GAAaI,CAEjB,CAMA,GAHIP,IAAiB,iBACnBG,EAAY,KAAK,IAAI,EAAGD,EAAa,KAAK,MAAMD,EAAgB,GAAG,CAAC,GAElED,IAAiB,aAAc,CACjC,IAAMS,EAAY,KAAK,MAAMR,EAAgB,GAAG,EAChDE,EAAYM,EAAYP,EAAaO,EAAYP,CACnD,CAEA,MAAO,CAAE,WAAAA,EAAY,UAAAC,EAAW,QAASD,EAAaC,CAAU,CAClE,CAQO,SAASO,GACdC,EACAZ,EACAa,EACM,CAGN,IAAMC,EAAQF,EAAU,WAAW,SAAS,UAAU,EAClDA,EACAA,EAAU,cAA2B,WAAW,EACpD,GAAI,CAACE,EAAO,OAEZ,GAAM,CAAE,WAAAX,EAAY,UAAAC,EAAW,QAAAW,CAAQ,EAAIhB,GACzCC,EACAc,EAAM,aAAa,oBAAoB,GAAK,GAC5C,WAAWA,EAAM,aAAa,qBAAqB,GAAK,EAAE,GAAK,CACjE,EAEME,EAASJ,EAAU,cAA2B,mBAAmB,EACjEK,EAAYL,EAAU,cAA2B,sBAAsB,EACvEM,EAAaN,EAAU,cAA2B,oBAAoB,EACtEO,EACJP,EAAU,cAA2B,uBAAuB,EACxDQ,EAAmBR,EAAU,cACjC,wBACF,EAaA,GAXII,IAAQA,EAAO,YAAcH,EAAYT,CAAS,GAElDa,IACEF,EAAU,GACZE,EAAU,YAAcJ,EAAYV,CAAU,EAC9Cc,EAAU,MAAM,QAAU,IAE1BA,EAAU,MAAM,QAAU,QAI1BC,EACF,GAAIH,EAAU,EAAG,CAEf,GADII,IAAiBA,EAAgB,YAAcN,EAAYE,CAAO,GAClEK,EAAkB,CACpB,IAAMC,EACJlB,EAAa,EAAI,KAAK,MAAOY,EAAU,IAAOZ,CAAU,EAAI,EAC9DiB,EAAiB,YAAc,IAAMC,EAAM,IAC7C,CACAH,EAAW,MAAM,QAAU,EAC7B,MACEA,EAAW,MAAM,QAAU,MAGjC,CCnGO,SAASI,GACdC,EACAC,EACAC,EACM,CACN,IAAMC,EACJH,EAAI,iBAAoC,uBAAuB,EACjE,GAAI,CAACG,EAAc,OAAQ,OAE3B,SAASC,EAAWC,EAAgC,CAClD,OAAO,SAASA,EAAI,aAAa,sBAAsB,GAAK,GAAI,EAAE,CACpE,CAEA,SAASC,GAAgC,CACvC,IAAMC,EAAmB,CAAC,EAC1B,QAASC,EAAI,EAAGA,EAAIL,EAAc,OAAQK,IAAK,CAC7C,IAAMC,EAAML,EAAWD,EAAcK,CAAC,CAAC,EACnC,CAACC,GAAOA,EAAM,IAClBF,EAAOE,EAAM,CAAC,EAAIN,EAAcK,CAAC,EAAE,MACrC,CACA,OAAOD,CACT,CAEA,SAASG,EAAqBC,EAAqC,CACjE,GAAI,GAACA,GAAW,CAACA,EAAQ,SACzB,QAASH,EAAI,EAAGA,EAAIL,EAAc,OAAQK,IAAK,CAC7C,IAAMC,EAAML,EAAWD,EAAcK,CAAC,CAAC,EACvC,GAAI,CAACC,GAAOA,EAAM,EAAG,SACrB,IAAMG,EAAWD,EAAQ,QAAQF,EAAM,CAAC,EACpCN,EAAcK,CAAC,EAAE,QAAUI,IAC7BT,EAAcK,CAAC,EAAE,MAAQI,EAE7B,CACF,CAGA,SAASC,EACPC,EACAC,EACM,CACN,QAASP,EAAI,EAAGA,EAAIL,EAAc,OAAQK,IAAK,CAC7C,IAAMH,EAAMF,EAAcK,CAAC,EACrBC,EAAML,EAAWC,CAAG,EAC1B,GAAI,CAACI,GAAOA,EAAM,EAAG,SACrB,IAAMO,EAAOX,EAAI,QACjB,QAASY,EAAI,EAAGA,EAAID,EAAK,OAAQC,IAC/BD,EAAKC,CAAC,EAAE,SAAW,CAACC,GAClBJ,EAAQ,SACRL,EAAM,EACNO,EAAKC,CAAC,EAAE,MACRF,CACF,CAEJ,CACF,CAEA,IAAMI,EAASlB,EAAW,EAC1B,GAAIkB,EAAQ,CACV,IAAMC,EAAiBC,GAAYF,EAAQA,EAAO,iBAAiB,EAC/DC,GAAkBA,EAAe,SACnCP,EAAuBM,EAAQC,EAAe,OAAO,CAEzD,CAEA,QAAS,EAAI,EAAG,EAAIjB,EAAc,OAAQ,IACxCA,EAAc,CAAC,EAAE,iBAAiB,SAAWmB,GAAM,CAIjDA,EAAE,gBAAgB,EAElB,IAAMR,EAAUb,EAAW,EAC3B,GAAI,CAACa,EAAS,OAEd,IAAMH,EAAUY,GAAqBT,EAASR,EAAoB,CAAC,EACnE,GAAI,CAACK,EAAS,CAIZ,IAAMa,EAAOH,GAAYP,EAASA,EAAQ,iBAAiB,EAC3DJ,EAAqBc,CAAI,EACrBA,GAAQA,EAAK,SAASX,EAAuBC,EAASU,EAAK,OAAO,EACtE,MACF,CAEAV,EAAQ,kBAAoBH,EAAQ,GACpCE,EAAuBC,EAASH,EAAQ,OAAO,EAC/CT,EAAWS,EAASG,CAAO,CAC7B,CAAC,CAEL,CC/FO,SAASW,GACdC,EACAC,EACAC,EACM,CACN,GAAI,CAACD,EAAQ,SAAS,OAAQ,OAK9B,IAAME,EAAQH,EAAI,cAA2B,0BAA0B,EACnEG,GAAS,CAACA,EAAM,cAClBA,EAAM,YAAcD,EAAS,OAG/B,IAAME,EAASJ,EAAI,cACjB,kCACF,EACII,GAAU,CAACA,EAAO,SAAS,QAC7BC,GAAkBD,EAAQH,EAASC,CAAQ,CAE/C,CAGA,SAASG,GACPC,EACAL,EACAC,EACM,CACN,IAAMK,EAAcN,EAAQ,aAAe,CAAC,EAE5C,QAASO,EAAI,EAAGA,EAAID,EAAY,OAAQC,IAAK,CAC3C,IAAMC,EAAQ,SAAS,cAAc,KAAK,EAC1CA,EAAM,UAAY,iCAElB,IAAMC,EAAQ,SAAS,cAAc,MAAM,EAC3CA,EAAM,UAAY,iCAClBA,EAAM,YAAcH,EAAYC,CAAC,EACjCC,EAAM,YAAYC,CAAK,EAEvB,IAAMC,EAAS,SAAS,cAAc,QAAQ,EAC9CA,EAAO,UAAY,2BACnBA,EAAO,aAAa,sBAAuB,EAAE,EAC7CA,EAAO,aAAa,uBAAwB,OAAOH,EAAI,CAAC,CAAC,EACzDG,EAAO,aAAa,kBAAmB,OAAOV,EAAQ,SAAS,CAAC,EAChEU,EAAO,KAAO,cAAgBV,EAAQ,UAAY,KAAOO,EAAI,GAC7DG,EAAO,aAAa,aAAcJ,EAAYC,CAAC,CAAC,EAEhD,IAAMI,EAAgC,OAAO,OAAO,IAAI,EACxD,QAAWC,KAAKZ,EAAQ,SAAU,CAChC,IAAMa,EAAQD,EAAE,SAAWA,EAAE,QAAQL,CAAC,EACtC,GAAI,CAACM,GAASF,EAAKE,CAAK,EAAG,SAC3BF,EAAKE,CAAK,EAAI,GAEd,IAAMC,EAAM,SAAS,cAAc,QAAQ,EAC3CA,EAAI,MAAQD,EACZC,EAAI,YAAcD,EACdZ,EAAS,SAAWA,EAAS,QAAQM,CAAC,IAAMM,IAC9CC,EAAI,SAAW,IAEjBJ,EAAO,YAAYI,CAAG,CACxB,CAEAN,EAAM,YAAYE,CAAM,EACxBL,EAAU,YAAYG,CAAK,CAC7B,CACF,CC1EA,IAAMO,GAAgB,IAAI,QASnB,SAASC,GACdC,EACAC,EACAC,EACM,CACN,IAAMC,EAAQH,EAAI,cAA2B,kBAAkB,EAC/D,GAAI,CAACG,EAAO,OAEZ,IAAMC,EAAMD,EAAM,cAAc,KAAK,EACrC,GAAI,CAACL,GAAc,IAAIE,CAAG,EAAG,CAC3B,IAAMK,EAAOH,IAAiBE,EAAMA,EAAI,IAAM,MAC1CC,GAAMP,GAAc,IAAIE,EAAKK,CAAI,CACvC,CAEA,IAAMC,EAAUL,EAAQ,OAASH,GAAc,IAAIE,CAAG,GAAK,KAC3D,GAAKM,EAEL,GAAIF,EACFA,EAAI,IAAME,EAGVF,EAAI,OAAS,OACR,CAGL,IAAMG,EAAS,SAAS,cAAc,KAAK,EAC3CA,EAAO,IAAMD,EACbC,EAAO,QAAU,OACjBA,EAAO,IAAM,GACbJ,EAAM,YAAc,GACpBA,EAAM,YAAYI,CAAM,CAC1B,CACF,CAQO,SAASC,GAAqBR,EAAkBS,EAAmB,CACxE,IAAMC,EAAKV,EAAI,cAA2B,mBAAmB,EACxDU,IACLA,EAAG,YAAc,OAAMD,EACzB,CAOO,SAASE,GACdX,EACAC,EACAW,EACAC,EACM,CACN,GAAI,CAACZ,EAAS,OAEdF,GAAgBC,EAAKC,EAASW,EAAQ,aAAa,EACnDJ,GAAqBR,EAAKC,EAAQ,UAAYW,EAAQ,UAAY,CAAC,EAEnE,IAAME,EAAUd,EAAI,cAA2B,sBAAsB,EACjEc,GAAWb,EAAQ,OAAS,OAC9Ba,EAAQ,YAAcD,EAAYZ,EAAQ,KAAK,GAGjD,IAAMc,EAAYf,EAAI,cACpB,8BACF,EACA,GAAIe,EAAW,CACb,IAAMC,EAAUf,EAAQ,eACpBe,GAAWA,EAAUf,EAAQ,OAC/Bc,EAAU,YAAcF,EAAYG,CAAO,EAC3CD,EAAU,OAAS,KAEnBA,EAAU,YAAc,GACxBA,EAAU,OAAS,GAEvB,CAEA,IAAME,EAASjB,EAAI,cAA2B,2BAA2B,EACrEiB,IACEhB,EAAQ,WACVgB,EAAO,YAAchB,EAAQ,UAC7BgB,EAAO,OAAS,KAEhBA,EAAO,YAAc,GACrBA,EAAO,OAAS,IAGtB,CC5FA,SAASC,EAAIC,EAAaC,EAAmBC,EAAgC,CAC3E,IAAMC,EAAK,SAAS,cAAcH,CAAG,EACrCG,EAAG,UAAYF,EACf,QAAWG,KAAKF,EAAOC,EAAG,aAAaC,EAAGF,EAAME,CAAC,CAAC,EAClD,OAAOD,CACT,CAgCO,SAASE,GACdC,EACA,EACAC,EACa,CACb,IAAMC,EAAWF,EAAQ,SAAS,OAE5BG,EAAMV,EAAI,MAAO,uBAAuB,EAC1CQ,EAAK,QACPE,EAAI,UAAU,IAAI,4BAA4B,EAC9CA,EAAI,aAAa,gBAAiB,MAAM,GAE1CA,EAAI,aAAa,kBAAmB,OAAOH,EAAQ,SAAS,CAAC,EACzDC,EAAK,UAAUE,EAAI,aAAa,iBAAkBF,EAAK,QAAQ,EAEnE,IAAMG,EAAQX,EAAI,MAAO,sBAAuB,CAAE,iBAAkB,EAAG,CAAC,EAGlEY,EAAQL,EAAQ,mBAClBC,EAAK,iBAAmB,YAAcI,GAAS,MAAQA,EAAQ,GACjED,EAAM,MAAM,YAAY,uBAAwB,OAAOC,CAAK,CAAC,EAE/DF,EAAI,YAAYC,CAAK,EAErB,IAAME,EAAOb,EAAI,MAAO,wBAAwB,EAE1Cc,EAAQ,SAAS,cAAcN,EAAK,KAAO,IAAM,MAAM,EAM7D,GALAM,EAAM,UAAY,yBACdN,EAAK,OAAOM,EAA4B,KAAON,EAAK,MACxDM,EAAM,YAAcP,EAAQ,OAAS,GACrCM,EAAK,YAAYC,CAAK,EAElBN,EAAK,MAAO,CACd,IAAMO,EAAMf,EAAI,OAAQ,qBAAqB,EAC7C,OAAAe,EAAI,YAAc,EAAE,YAAc,eAClCF,EAAK,YAAYE,CAAG,EACpBL,EAAI,YAAYG,CAAI,EACbH,CACT,CAGID,IAAa,GAAKF,EAAQ,SAAS,CAAC,GAAG,QAAU,iBACnDM,EAAK,YAAYb,EAAI,OAAQ,yBAAyB,CAAC,EAGzD,IAAMgB,EAAShB,EAAI,OAAQ,0BAA0B,EAUrD,GATAgB,EAAO,YACLhB,EAAI,OAAQ,kCAAmC,CAC7C,6BAA8B,GAC9B,OAAQ,EACV,CAAC,CACH,EACAgB,EAAO,YACLhB,EAAI,OAAQ,0BAA2B,CAAE,qBAAsB,EAAG,CAAC,CACrE,EACIQ,EAAK,gBAAkB,KAAM,CAC/B,IAAMS,EAAMjB,EAAI,OAAQ,uBAAwB,CAAE,kBAAmB,EAAG,CAAC,EACzEiB,EAAI,YAAc,OAAMT,EAAK,eAC7BQ,EAAO,YAAYC,CAAG,CACxB,CAwBA,GAvBAJ,EAAK,YAAYG,CAAM,EAEvBH,EAAK,YACHb,EAAI,OAAQ,+BAAgC,CAC1C,0BAA2B,EAC7B,CAAC,CACH,EAEIS,EAAW,GACbI,EAAK,YACHb,EAAI,MAAO,kCAAmC,CAG5C,MAAO,2BAA6BO,EAAQ,YAAY,QAAU,EACpE,CAAC,CACH,EAGFG,EAAI,YAAYG,CAAI,EAEhBL,EAAK,gBAAkB,MACzBE,EAAI,YAAYV,EAAI,OAAQ,qBAAsB,CAAE,kBAAmB,EAAG,CAAC,CAAC,EAE1EQ,EAAK,UAAW,CAClB,IAAMU,EAAQlB,EAAI,OAAQ,gBAAgB,EAC1CkB,EAAM,YAAcV,EAAK,UACzBE,EAAI,YAAYQ,CAAK,CACvB,CAEA,OAAOR,CACT,CAiCO,SAASS,GACdC,EACAZ,EACa,CACb,IAAMa,EAAOrB,EAAI,MAAOoB,CAAS,EAE3BE,EAAStB,EAAI,MAAO,kBAAkB,EACtCuB,EAAUvB,EAAI,KAAM,iBAAiB,EAG3C,GAFAuB,EAAQ,YAAcf,EAAK,MAC3Bc,EAAO,YAAYC,CAAO,EACtBf,EAAK,SAAU,CACjB,IAAMgB,EAAMxB,EAAI,IAAK,oBAAoB,EACzCwB,EAAI,YAAchB,EAAK,SACvBc,EAAO,YAAYE,CAAG,CACxB,CAGA,GAFAH,EAAK,YAAYC,CAAM,EAEnBd,EAAK,OAAQ,CACf,IAAMiB,EAAYzB,EAAI,MAAO,sBAAuB,CAClD,iBAAkB,EACpB,CAAC,EACK0B,EAAQ1B,EAAI,OAAQ,2BAA2B,EAErD0B,EAAM,YAAclB,EAAK,gBAAkB,qBAC3CiB,EAAU,YAAYC,CAAK,EAC3BD,EAAU,YACRzB,EAAI,OAAQ,4BAA6B,CACvC,uBAAwB,EAC1B,CAAC,CACH,EACAqB,EAAK,YAAYI,CAAS,CAC5B,CAEA,IAAME,EAAW3B,EAAI,MAAOoB,EAAY,yBAAyB,EACjEC,EAAK,YAAYM,CAAQ,EAEzBN,EAAK,YAAYrB,EAAI,MAAO,mBAAmB,CAAC,EAKhD,IAAM4B,EAAU5B,EAAI,MAAO,oBAAqB,CAC9C,uBAAwB,EAC1B,CAAC,EACK6B,EAAO7B,EAAI,MAAO,yBAAyB,EAC3C0B,EAAQ1B,EAAI,OAAQ,0BAA0B,EAOpD,GANA0B,EAAM,YAAclB,EAAK,cAAgB,eACzCqB,EAAK,YAAYH,CAAK,EAClBlB,EAAK,eAEPkB,EAAM,YAAY1B,EAAI,OAAQ,GAAI,CAAE,kBAAmB,EAAG,CAAC,CAAC,EAE1DQ,EAAK,eAAgB,CACvB,IAAMsB,EAAU9B,EAAI,IAAK,yBAA0B,CACjD,mBAAoB,GACpB,MAAO,cACT,CAAC,EACD8B,EAAQ,YAAY,SAAS,eAAe,OAAO,CAAC,EACpDA,EAAQ,YAAY9B,EAAI,OAAQ,GAAI,CAAE,sBAAuB,EAAG,CAAC,CAAC,EAClE8B,EAAQ,YAAY,SAAS,eAAe,GAAG,CAAC,EAChDA,EAAQ,YAAY9B,EAAI,OAAQ,GAAI,CAAE,uBAAwB,EAAG,CAAC,CAAC,EACnE6B,EAAK,YAAYC,CAAO,CAC1B,CACAF,EAAQ,YAAYC,CAAI,EAExB,IAAMb,EAAShB,EAAI,OAAQ,2BAA2B,EAClDQ,EAAK,kBACPQ,EAAO,YACLhB,EAAI,OAAQ,0BAA2B,CAAE,qBAAsB,EAAG,CAAC,CACrE,EAEFgB,EAAO,YACLhB,EAAI,OAAQ,uBAAwB,CAClC,CAACQ,EAAK,eAAiB,mBAAqB,iBAAiB,EAAG,EAClE,CAAC,CACH,EACAoB,EAAQ,YAAYZ,CAAM,EAC1BK,EAAK,YAAYO,CAAO,EAMxB,IAAMG,EAAM/B,EAAI,SAAU,gBAAiB,CACzC,gBAAiBQ,EAAK,QACtB,KAAM,SACN,kBAAmB,EACrB,CAAC,EACKwB,EAAWhC,EAAI,OAAQ,eAAgB,CAAE,iBAAkB,EAAG,CAAC,EACrE,OAAAgC,EAAS,YAAcxB,EAAK,QAC5BuB,EAAI,YAAYC,CAAQ,EACxBX,EAAK,YAAYU,CAAG,EAEpBV,EAAK,YACHrB,EAAI,IAAK,kBAAmB,CAAE,aAAc,GAAI,YAAa,QAAS,CAAC,CACzE,EACAqB,EAAK,YACHrB,EAAI,OAAQ,qBAAsB,CAChC,cAAe,GACf,YAAa,QACf,CAAC,CACH,EAEO,CAAE,KAAAqB,EAAM,SAAAM,EAAU,IAAAI,CAAI,CAC/B,CAkCO,SAASE,GACdC,EACAC,EACA3B,EAA0B,CAAC,EACd,CACb,IAAM4B,EAAc5B,EAAK,cAAgB,GACnC6B,EAAgB7B,EAAK,gBAAkB,GACvC8B,EAAQtC,EAAI,MAAO,gCAAiC,CACxD,KAAM,aACN,aAAcmC,EACd,kBAAmB,EACrB,CAAC,EAED,OAAAD,EAAM,QAAQ,CAACK,EAAMC,IAAM,CACzB,IAAMpC,EAAKJ,EAAI,MAAO,kBAAmB,CACvC,KAAM,QACN,eAAgBuC,EAAK,SAAW,OAAS,QAEzC,SAAUA,EAAK,SAAW,IAAM,KAChC,kBAAmB,OAAOC,CAAC,EAC3B,gBAAiB,OAAOD,EAAK,QAAQ,EACrC,gBAAiB,OAAOA,EAAK,OAAO,EACpC,gBAAiB,OAAOA,EAAK,WAAW,CAC1C,CAAC,EAEKE,EAAQzC,EAAI,OAAQ,kBAAkB,EAC5CyC,EAAM,YAAYzC,EAAI,OAAQ,sBAAsB,CAAC,EACrDI,EAAG,YAAYqC,CAAK,EAEpB,IAAM5B,EAAOb,EAAI,OAAQ,sBAAsB,EACzC0B,EAAQ1B,EAAI,OAAQ,uBAAuB,EACjD0B,EAAM,YAAca,EAAK,MACzB1B,EAAK,YAAYa,CAAK,EAEtB,IAAMgB,EAAQ1C,EAAI,OAAQ,uBAAuB,EAIjD,GAHIoC,IAAgBG,EAAK,QAAU,GAAKA,EAAK,YAAc,IACzDG,EAAM,YAAY1C,EAAI,OAAQ,yBAAyB,CAAC,EAEtDqC,IACFK,EAAM,YAAY1C,EAAI,OAAQ,GAAI,CAAE,uBAAwB,EAAG,CAAC,CAAC,EAC7DQ,EAAK,WAAW,CAClB,IAAMmC,EAAO3C,EAAI,OAAQ,sBAAsB,EAC/C2C,EAAK,YAAcnC,EAAK,UACxBkC,EAAM,YAAYC,CAAI,CACxB,CAKF,GAHA9B,EAAK,YAAY6B,CAAK,EACtBtC,EAAG,YAAYS,CAAI,EAEf0B,EAAK,YAAcA,EAAK,aAAc,CACxC,IAAMK,EAAQ5C,EAAI,OAAQ,uBAAuB,EACjD,GAAIuC,EAAK,WAAY,CACnB,IAAMrB,EAAQlB,EAAI,OAAQ,uBAAuB,EACjDkB,EAAM,YAAcqB,EAAK,WACzBK,EAAM,YAAY1B,CAAK,CACzB,CACA,GAAIqB,EAAK,aAAc,CACrB,IAAMT,EAAU9B,EAAI,OAAQ,yBAAyB,EACrD8B,EAAQ,YAAcS,EAAK,aAC3BK,EAAM,YAAYd,CAAO,CAC3B,CACA1B,EAAG,YAAYwC,CAAK,CACtB,CAEAN,EAAM,YAAYlC,CAAE,CACtB,CAAC,EAEMkC,CACT,CASO,SAASO,GAAiBC,EAAmC,CAClE,IAAMC,EAAO/C,EAAI,MAAO,yBAA0B,CAAE,gBAAiB,EAAG,CAAC,EAEnEgD,EAAWhD,EAAI,MAAO,kCAAmC,CAC7D,KAAM,cACN,gBAAiB,IACjB,gBAAiB,IACjB,gBAAiB,OAAO8C,CAAY,CACtC,CAAC,EACD,QAASN,EAAI,EAAGA,EAAIM,EAAcN,IAChCQ,EAAS,YACPhD,EAAI,OAAQ,iCAAkC,CAC5C,wBAAyB,EAC3B,CAAC,CACH,EAEF+C,EAAK,YAAYC,CAAQ,EAEzB,IAAMC,EAASjD,EAAI,MAAO,+BAA+B,EACzD,OAAAiD,EAAO,YACLjD,EAAI,OAAQ,+BAAgC,CAC1C,sBAAuB,GACvB,YAAa,QACf,CAAC,CACH,EACAiD,EAAO,YACLjD,EAAI,OAAQ,mCAAoC,CAC9C,0BAA2B,GAC3B,YAAa,QACf,CAAC,CACH,EACA+C,EAAK,YAAYE,CAAM,EAEhBF,CACT,CCpaA,SAASG,GAAIC,EAAmB,CAC9B,OAAOA,EAAI,GAAK,IAAMA,EAAI,OAAOA,CAAC,CACpC,CAcO,SAASC,GAAcC,EAA8B,CACtDA,EAAU,uBACZ,cAAcA,EAAU,oBAAoB,EAC5CA,EAAU,qBAAuB,MAGnC,IAAMC,EAAYD,EAAU,cAA2B,kBAAkB,EACzE,GAAI,CAACC,EAAW,OAEhB,IAAMC,EAASF,EAAU,QAAqB,mBAAmB,EAC3DG,EAASD,GAAUA,EAAO,aAAa,cAAc,EAC3D,GAAI,CAACC,EAAQ,CACXF,EAAU,MAAM,QAAU,OAC1B,MACF,CAEA,IAAMG,EAAU,IAAI,KAAKD,CAAM,EAAE,QAAQ,EACzC,GAAI,MAAMC,CAAO,EAAG,CAClBH,EAAU,MAAM,QAAU,OAC1B,MACF,CAEA,IAAMI,EAAUJ,EAAU,cAA2B,wBAAwB,EAC7E,GAAI,CAACI,EAAS,OAEd,IAAIC,EAEJ,SAASC,GAAe,CACtB,IAAMC,EAAYJ,EAAU,KAAK,IAAI,EACrC,GAAII,GAAa,EAAG,CACdN,IAAQA,EAAO,MAAM,QAAU,QAC/BI,GAAU,cAAcA,CAAQ,EACpC,MACF,CACA,IAAMG,EAAO,KAAK,MAAMD,EAAY,KAAQ,EACtCE,EAAQ,KAAK,MAAOF,EAAY,MAAY,IAAO,EACnDG,EAAO,KAAK,MAAOH,EAAY,KAAW,GAAK,EAC/CI,EAAO,KAAK,MAAOJ,EAAY,IAAS,GAAI,EAClDH,EAAS,YACPI,EAAO,EACHA,EAAO,KAAOZ,GAAIa,CAAK,EAAI,KAAOb,GAAIc,CAAI,EAAI,KAAOd,GAAIe,CAAI,EAAI,IACjEf,GAAIa,CAAK,EAAI,KAAOb,GAAIc,CAAI,EAAI,KAAOd,GAAIe,CAAI,EAAI,GAC3D,CAEAL,EAAO,EACHH,EAAU,KAAK,IAAI,EAAI,IACzBE,EAAW,YAAYC,EAAQ,GAAI,EACnCP,EAAU,qBAAuBM,EAErC,CC9BO,IAAMO,GAAiC,CAC5C,OAAQ,KACR,QAAS,MACT,UAAW,QACX,WAAY,SACZ,SAAU,OACV,SAAU,WACV,QAAS,WACT,WAAY,eACZ,gBAAiB,CACf,IAAK,8BACL,MAAO,8BACT,EACA,SAAU,WACV,iBAAkB,oBAClB,iBAAkB,oBAClB,SAAU,MACV,cAAe,iBACf,UAAW,cACX,YAAa,eACb,KAAM,OACN,SAAU,WACV,WAAY,+BACZ,SAAU,uBACV,eAAgB,2BAChB,eAAgB,0BAChB,iBAAkB,+CAClB,mBAAoB,gDACpB,OAAQ,6BACR,eAAgB,yCAChB,WAAY,uBACZ,aAAc,oBACd,eAAgB,CACd,IAAK,uBACL,MAAO,uBACT,EACA,cAAe,CACb,IAAK,uBACL,MAAO,uBACT,EACA,KAAM,QACN,OAAQ,gBACR,UAAW,CACT,IAAK,iBACL,MAAO,iBACT,EACA,WAAY,CACV,IAAK,uBACL,MAAO,uBACT,CACF,EAOO,SAASC,GACdC,EACAC,EACQ,CACR,IAAMC,EAAMF,EAAE,gBACVG,EACJ,GAAID,GAAO,OAAOA,GAAQ,SAAU,CAClC,IAAME,EAAO,IAAI,KAAK,YAAYJ,EAAE,QAAU,IAAI,EAAE,OAAOC,CAAK,EAChEE,EAAMD,EAAIE,CAAI,GAAKF,EAAI,KACzB,MACEC,EAAMD,EAER,OAAQC,GAAO,gCACZ,MAAM,WAAW,EACjB,KAAK,OAAOF,CAAK,CAAC,CACvB,CC3GO,SAASI,GAAuBC,EAAqC,CAC1E,IAAMC,EAAMD,EAAG,cACTE,EAAMD,GAAOA,EAAI,YACvB,GAAI,CAACC,EAAK,OAAO,KAEjB,IAAIC,EAAMH,EAAG,cACb,KAAOG,GAAOA,IAAQF,EAAI,MAAM,CAC9B,IAAMG,EAAYF,EAAI,iBAAiBC,CAAG,EAAE,UAC5C,GAAIC,IAAc,QAAUA,IAAc,UAAYA,IAAc,SAClE,OAAOD,EAETA,EAAMA,EAAI,aACZ,CACA,OAAO,IACT,CCPA,IAAME,GAAoC,CAAC,EACvCC,GAA4B,GAUhC,SAASC,GAAoBC,EAAcC,EAAiC,CAC1E,GAAID,EAAM,aAAc,CACtB,IAAME,EAAOF,EAAM,aAAa,EAChC,QAASG,EAAI,EAAGA,EAAID,EAAK,OAAQC,IAC/B,GAAID,EAAKC,CAAC,IAAMF,EAAK,OAASC,EAAKC,CAAC,IAAMF,EAAK,QAAS,MAAO,GAEjE,MAAO,EACT,CACA,IAAMG,EAASJ,EAAM,OACrB,OAAOC,EAAK,MAAM,SAASG,CAAM,GAAKH,EAAK,QAAQ,SAASG,CAAM,CACpE,CAEA,SAASC,GAAgBL,EAAoB,CAC3C,QAASM,EAAIT,GAAc,OAAS,EAAGS,GAAK,EAAGA,IACxCP,GAAoBC,EAAOH,GAAcS,CAAC,CAAC,GAAGT,GAAcS,CAAC,EAAE,MAAM,CAE9E,CAEA,SAASC,IAAuB,CAC9B,QAASD,EAAIT,GAAc,OAAS,EAAGS,GAAK,EAAGA,IAAKT,GAAcS,CAAC,EAAE,MAAM,CAC7E,CAEA,SAASE,IAA2B,CAC9BV,KACJ,SAAS,iBAAiB,cAAeO,GAAiB,EAAI,EAC9D,OAAO,iBAAiB,SAAUE,EAAc,EAIhD,OAAO,iBAAiB,SAAUF,GAAiB,EAAI,EACvDP,GAA4B,GAC9B,CAEA,SAASW,IAA2B,CAC7BX,KACL,SAAS,oBAAoB,cAAeO,GAAiB,EAAI,EACjE,OAAO,oBAAoB,SAAUE,EAAc,EACnD,OAAO,oBAAoB,SAAUF,GAAiB,EAAI,EAC1DP,GAA4B,GAC9B,CAGO,SAASY,GAAaC,EAAkC,CAC7Dd,GAAc,KAAKc,CAAQ,EACvBd,GAAc,SAAW,GAAGW,GAAmB,CACrD,CAGO,SAASI,GAAeD,EAAkC,CAC/D,IAAME,EAAMhB,GAAc,QAAQc,CAAQ,EACtCE,GAAO,GAAGhB,GAAc,OAAOgB,EAAK,CAAC,EACrChB,GAAc,SAAW,GAAGY,GAAmB,CACrD,CAGO,SAASK,GAAYC,EAA4C,CACtE,QAAST,EAAIT,GAAc,OAAS,EAAGS,GAAK,EAAGA,IACzCT,GAAcS,CAAC,IAAMS,GAAQlB,GAAcS,CAAC,EAAE,MAAM,CAE5D,CClEA,IAAMU,GAAc,GACdC,GAAiB,EACjBC,GAAoB,EAGpBC,GAAY,IAAI,QAEf,SAASC,GACdC,EAC8B,CAC9B,OAAOF,GAAU,IAAIE,CAAM,CAC7B,CAeO,SAASC,GACdC,EACyB,CACzB,GAAIA,EAAS,UAAU,SAAS,mBAAmB,EAAG,OAAO,KAE7D,IAAMC,EAAM,SACNC,EAAYF,EAAS,aAAa,YAAY,GAAK,GACnDG,EAAS,SAAW,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,EAAG,CAAC,EAE/DH,EAAS,UAAU,IAAI,mBAAmB,EAC1CA,EAAS,aAAa,cAAe,MAAM,EAC3CA,EAAS,aAAa,WAAY,IAAI,EAEtC,IAAMI,EAAQH,EAAI,cAAc,KAAK,EACrCG,EAAM,UAAY,cAClBA,EAAM,aAAa,mBAAoB,EAAE,EAEzC,IAAMC,EAAUJ,EAAI,cAAc,QAAQ,EAC1CI,EAAQ,KAAO,SACfA,EAAQ,UAAY,sBACpBA,EAAQ,aAAa,OAAQ,UAAU,EACvCA,EAAQ,aAAa,gBAAiB,SAAS,EAC/CA,EAAQ,aAAa,gBAAiB,OAAO,EAE7C,IAAMC,EAAYH,EAAS,WAC3BE,EAAQ,aAAa,gBAAiBC,CAAS,EAC3CJ,GAAWG,EAAQ,aAAa,aAAcH,CAAS,EAE3D,IAAMK,EAAeN,EAAI,cAAc,MAAM,EAC7CM,EAAa,UAAY,4BAEzB,IAAMC,EAAUP,EAAI,cAAc,MAAM,EACxCO,EAAQ,UAAY,sBACpBA,EAAQ,aAAa,cAAe,MAAM,EAE1CH,EAAQ,YAAYE,CAAY,EAChCF,EAAQ,YAAYG,CAAO,EAE3B,IAAMC,EAAUR,EAAI,cAAc,IAAI,EACtCQ,EAAQ,GAAKH,EACbG,EAAQ,UAAY,sBACpBA,EAAQ,aAAa,OAAQ,SAAS,EAClCP,GAAWO,EAAQ,aAAa,aAAcP,CAAS,EAC3DO,EAAQ,OAAS,GAEjBL,EAAM,YAAYC,CAAO,EACzBL,EAAS,YAAY,aAAaI,EAAOJ,EAAS,WAAW,EAQ7D,IAAMU,EAAeV,EAAS,QAAqB,sBAAsB,EACrEU,GACFA,EAAa,YAAYD,CAAO,EAChCA,EAAQ,aAAa,0BAA2B,EAAE,GAElDL,EAAM,YAAYK,CAAO,EAG3B,IAAIE,EAAS,GACTC,EAAc,GACdC,EAAqCC,GAAS,oBAAoB,EAClEC,EAA6B,CAAC,EAElC,SAASC,GAAkC,CACzC,IAAMC,EAAqB,CAAC,EAC5B,QAASC,EAAI,EAAGA,EAAIlB,EAAS,QAAQ,OAAQkB,IAAK,CAChD,IAAMC,EAAInB,EAAS,QAAQkB,CAAC,EAC5BD,EAAI,KAAK,CAAE,SAAUE,EAAE,SAAU,MAAOA,EAAE,aAAeA,EAAE,KAAM,CAAC,CACpE,CACA,OAAOF,CACT,CAEA,SAASG,GAAuB,CAK9Bf,EAAQ,SAAWL,EAAS,SAE5B,IAAMiB,EAAMD,EAAiB,EACvBK,EAAMrB,EAAS,cAKrB,IAJAO,EAAa,YAAcc,GAAO,GAAKJ,EAAII,CAAG,EAAIJ,EAAII,CAAG,EAAE,MAAQ,GAI5DZ,EAAQ,YAAYA,EAAQ,YAAYA,EAAQ,UAAU,EACjEM,EAAY,CAAC,EAEb,QAASG,EAAI,EAAGA,EAAID,EAAI,OAAQC,IAAK,CACnC,IAAMI,EAAKrB,EAAI,cAAc,IAAI,EACjCqB,EAAG,GAAKnB,EAAS,QAAUe,EAC3BI,EAAG,UAAY,qBACfA,EAAG,aAAa,OAAQ,QAAQ,EAChCA,EAAG,aAAa,gBAAiBJ,IAAMG,EAAM,OAAS,OAAO,EACzDJ,EAAIC,CAAC,EAAE,UAAUI,EAAG,aAAa,gBAAiB,MAAM,EAC5DA,EAAG,aAAa,aAActB,EAAS,QAAQkB,CAAC,EAAE,KAAK,EACvDI,EAAG,aAAa,aAAc,OAAOJ,CAAC,CAAC,EACvCI,EAAG,YAAcL,EAAIC,CAAC,EAAE,MACxBT,EAAQ,YAAYa,CAAE,EACtBP,EAAU,KAAKO,CAAE,CACnB,CACF,CAEA,SAASC,EAAaC,EAAwB,CAM5C,GALIZ,GAAe,GAAKG,EAAUH,CAAW,GAC3CG,EAAUH,CAAW,EAAE,UAAU,OAAO,WAAW,EAErDA,EAAcY,EAEVA,EAAW,GAAK,CAACT,EAAUS,CAAQ,EAAG,CACxCnB,EAAQ,aAAa,wBAAyB,EAAE,EAChD,MACF,CAEA,IAAMiB,EAAKP,EAAUS,CAAQ,EAC7BF,EAAG,UAAU,IAAI,WAAW,EAC5BjB,EAAQ,aAAa,wBAAyBiB,EAAG,EAAE,EAMnD,IAAMG,EAAQH,EAAG,UACXI,EAAWD,EAAQH,EAAG,aACtBK,EAASlB,EAAQ,UACjBmB,EAAYD,EAASlB,EAAQ,aAC/BgB,EAAQE,EACVlB,EAAQ,UAAYgB,EACXC,EAAWE,IACpBnB,EAAQ,UAAYiB,EAAWjB,EAAQ,aAE3C,CAGA,SAASoB,GAAoB,CAC3B,IAAMC,EAAOzB,EAAQ,sBAAsB,EAC3C,GAAIyB,EAAK,QAAU,EAAG,MAAO,GAG7B,IAAMC,EADe,KAAK,IAAIhB,EAAU,QAAU,EAAGpB,EAAiB,EACjCF,GAAcC,GAQ7CsC,EAAaC,GAAuB5B,CAAO,EAC3C6B,EAAWF,EACb,CACE,IAAKA,EAAW,sBAAsB,EAAE,IACxC,OAAQA,EAAW,sBAAsB,EAAE,MAC7C,EACA,OAEEG,EAAMrB,GAAS,gBAAgB,CACnC,QAAS,CACP,IAAKgB,EAAK,IACV,OAAQA,EAAK,OACb,KAAMA,EAAK,KACX,MAAOA,EAAK,KACd,EACA,eAAgB,OAAO,YACvB,cAAAC,EACA,KAAMG,CACR,CAAC,EAED,OAAAzB,EAAQ,aAAa,iBAAkB0B,EAAI,SAAS,EACpD1B,EAAQ,MAAM,UAAY0B,EAAI,UAAY,KAItC1B,EAAQ,aAAa,yBAAyB,IAChDA,EAAQ,MAAM,IAAM0B,EAAI,UAAY,KACpC1B,EAAQ,MAAM,KAAO0B,EAAI,WAAa,KACtC1B,EAAQ,MAAM,MAAQ0B,EAAI,MAAQ,MAE7B,EACT,CAEA,SAASC,GAAa,CACpB,GAAIzB,EAAQ,OACZ0B,GAAYC,CAAQ,EAEpB3B,EAAS,GACTF,EAAQ,OAAS,GACjBJ,EAAQ,aAAa,gBAAiB,MAAM,EAEvCwB,EAAS,GACZ,sBAAsB,IAAM,CAC1BA,EAAS,CACX,CAAC,EAGH,IAAMZ,EAAMD,EAAiB,EACvBuB,EAASvC,EAAS,cACpBuC,GAAU,GAAKtB,EAAIsB,CAAM,GAAK,CAACtB,EAAIsB,CAAM,EAAE,SAC7ChB,EAAagB,CAAM,EAEnBhB,EAAaT,GAAS,aAAaG,CAAG,CAAC,EAGzCuB,GAAaF,CAAQ,CACvB,CAEA,SAASG,EAAMC,EAA6B,CACrC/B,IACLA,EAAS,GACTF,EAAQ,OAAS,GACjBJ,EAAQ,aAAa,gBAAiB,OAAO,EAC7CA,EAAQ,aAAa,wBAAyB,EAAE,EAC5CO,GAAe,GAAKG,EAAUH,CAAW,GAC3CG,EAAUH,CAAW,EAAE,UAAU,OAAO,WAAW,EAErDA,EAAc,GAEd+B,GAAeL,CAAQ,EACnBI,GAAcrC,EAAQ,MAAM,EAClC,CAEA,SAASuC,EAAOC,EAAqB,CACnC,IAAMC,EAAM9C,EAAS,QAAQ6C,CAAK,EAC9B,CAACC,GAAOA,EAAI,WACZ9C,EAAS,QAAU8C,EAAI,QACzB9C,EAAS,MAAQ8C,EAAI,MACrB9C,EAAS,cAAc,IAAI,MAAM,SAAU,CAAE,QAAS,EAAK,CAAC,CAAC,GAE/DoB,EAAe,EACfqB,EAAM,EAAI,EACZ,CAEA,SAASM,EAAUC,EAA4B,CAC7C,IAAMC,EAAYjC,EAAiB,EAC7BkC,EAASpC,GAAS,UACtB,CACE,IAAKkC,EAAM,IACX,QAASA,EAAM,QACf,QAASA,EAAM,QACf,OAAQA,EAAM,OACd,SAAUA,EAAM,QAClB,EACA,CACE,OAAArC,EACA,YAAAC,EACA,cAAeZ,EAAS,cACxB,QAASiD,CACX,CACF,EAIA,OAFIC,EAAO,gBAAgBF,EAAM,eAAe,EAExCE,EAAO,KAAM,CACnB,IAAK,OACHd,EAAK,EACDc,EAAO,aAAe,GAAG3B,EAAa2B,EAAO,WAAW,EAC5D,MACF,IAAK,QACHT,EAAMS,EAAO,YAAY,EACzB,MACF,IAAK,cACH3B,EAAa2B,EAAO,WAAW,EAC/B,MACF,IAAK,SACHN,EAAOM,EAAO,KAAK,EACnB,MACF,IAAK,aAAc,CACjB,IAAMC,EAAIrC,GAAS,kBACjBD,EACAqC,EAAO,KACP,KAAK,IAAI,EACTD,CACF,EACApC,EAAYsC,EAAE,SACVA,EAAE,eAAiB,OAChBxC,GAAQyB,EAAK,EAClBb,EAAa4B,EAAE,YAAY,GAE7B,KACF,CACF,CACF,CAEA,SAASC,EAAeJ,EAAyB,CAC/CA,EAAM,eAAe,EACjBrC,EAAQ8B,EAAM,EAAK,EAClBL,EAAK,CACZ,CAGA,SAASiB,EAAgBL,EAA6B,CACpD,IAAIM,EAASN,EAAM,OACnB,KAAOM,GAAUA,IAAW7C,GAAS,CACnC,GAAI6C,EAAO,WAAaA,EAAO,UAAU,SAAS,oBAAoB,EAAG,CACvE,IAAMjC,EAAM,SAASiC,EAAO,aAAa,YAAY,GAAK,GAAI,EAAE,EAChE,OAAO,MAAMjC,CAAG,EAAI,KAAOA,CAC7B,CACAiC,EAASA,EAAO,UAClB,CACA,OAAO,IACT,CAEA,SAASC,EAAeP,EAAyB,CAC/C,IAAM3B,EAAMgC,EAAgBL,CAAK,EAC7B3B,IAAQ,MAAMuB,EAAOvB,CAAG,CAC9B,CAEA,SAASmC,EAAmBR,EAAyB,CACnD,IAAIM,EAASN,EAAM,OACnB,KAAOM,GAAUA,IAAW7C,GAAS,CACnC,GAAI6C,EAAO,WAAaA,EAAO,UAAU,SAAS,oBAAoB,EAAG,CACvE,GAAIA,EAAO,aAAa,eAAe,IAAM,OAAQ,OACrD,IAAMjC,EAAM,SAASiC,EAAO,aAAa,YAAY,GAAK,GAAI,EAAE,EAC5D,CAAC,MAAMjC,CAAG,GAAKA,IAAQT,GAAaW,EAAaF,CAAG,EACxD,MACF,CACAiC,EAASA,EAAO,UAClB,CACF,CAEA,SAASG,GAAwB,CAE/B,WAAW,IAAM,CACV9C,IACAP,EAAM,SAAS,SAAS,aAAa,GAAGqC,EAAM,EAAK,EAC1D,EAAG,CAAC,CACN,CAGA,SAASiB,GAAuB,CAC9BtC,EAAe,CACjB,CAGA,IAAMuC,EAAW,IAAI,iBAAiB,IAAM,CAC1CvC,EAAe,CACjB,CAAC,EACDuC,EAAS,QAAQ3D,EAAU,CACzB,UAAW,GACX,QAAS,GACT,WAAY,GACZ,gBAAiB,CAAC,WAAY,QAAS,UAAU,CACnD,CAAC,EAUD,SAAS4D,EAAmBZ,EAAyB,CACnDA,EAAM,eAAe,CACvB,CAEA3C,EAAQ,iBAAiB,QAAS+C,CAAc,EAChD/C,EAAQ,iBAAiB,UAAW0C,CAAS,EAC7C3C,EAAM,iBAAiB,WAAYqD,CAAe,EAClDhD,EAAQ,iBAAiB,YAAamD,CAAkB,EACxDnD,EAAQ,iBAAiB,QAAS8C,CAAc,EAChD9C,EAAQ,iBAAiB,YAAa+C,CAAkB,EACxDxD,EAAS,iBAAiB,SAAU0D,CAAc,EAElD,SAASG,GAAgB,CACnBlD,GAAQ8B,EAAM,EAAK,EACvBkB,EAAS,WAAW,EACpBtD,EAAQ,oBAAoB,QAAS+C,CAAc,EACnD/C,EAAQ,oBAAoB,UAAW0C,CAAS,EAChD3C,EAAM,oBAAoB,WAAYqD,CAAe,EACrDhD,EAAQ,oBAAoB,YAAamD,CAAkB,EAC3DnD,EAAQ,oBAAoB,QAAS8C,CAAc,EACnD9C,EAAQ,oBAAoB,YAAa+C,CAAkB,EAC3DxD,EAAS,oBAAoB,SAAU0D,CAAc,EACrDtD,EAAM,YAAY,YAAYA,CAAK,EACnCK,EAAQ,YAAY,YAAYA,CAAO,EACvCT,EAAS,UAAU,OAAO,mBAAmB,EAC7CA,EAAS,gBAAgB,aAAa,EACtCA,EAAS,gBAAgB,UAAU,EACnCJ,GAAU,OAAOI,CAAQ,CAC3B,CAIA,IAAMsC,EAA6B,CACjC,MAAAlC,EACA,QAAAK,EACA,OAAQT,EACR,MAAO,IAAMyC,EAAM,EAAK,EACxB,QAAAoB,CACF,EACA,OAAAjE,GAAU,IAAII,EAAUsC,CAAQ,EAEhClB,EAAe,EACRkB,CACT,CClbO,IAAMwB,GACX,uHAMK,SAASC,GAAQC,EAAgC,CAEtD,IAAMC,GADQD,GAAQ,UACA,iBAAoCE,EAAa,EACvE,QAASC,EAAI,EAAGA,EAAIF,EAAQ,OAAQE,IAAKC,GAAaH,EAAQE,CAAC,CAAC,CAClE,CAEO,SAASE,GAAUL,EAAgC,CAExD,IAAMM,GADQN,GAAQ,UACF,iBAClB,0BACF,EACA,QAASG,EAAI,EAAGA,EAAIG,EAAM,OAAQH,IAChCI,GAAYD,EAAMH,CAAC,CAAC,GAAG,QAAQ,CAEnC,CClBO,SAASK,GAAcC,EAA+B,CAC3D,OAAAC,GAAQD,CAAI,EACL,IAAME,GAAUF,CAAI,CAC7B,CCYO,SAASG,GACdC,EACAC,EACAC,EACAC,EACM,CACN,IAAMC,EAAKH,EAAO,aACZI,EAAWC,GAAcL,CAAM,EACrC,GAAI,CAACI,EAAS,OAAQ,OAItB,IAAME,EAAcF,EAAS,OAAQG,GAAM,CAACA,EAAE,SAAS,KAAMC,GAAMA,EAAE,SAAS,CAAC,EAEzEC,EACJT,EAAO,SAAS,CAAC,GAAG,WAAW,gBAAgB,cAAgB,MAC3DU,EAAcC,GAAgBF,CAAQ,EACtCG,EAAIC,GAEJC,EAAQC,GAAiB,WAAY,CACzC,MAAOf,EAAO,MACd,SAAUA,EAAO,YACjB,eAAgBG,EAAG,YAAY,UAAY,GAC3C,iBAAkBA,EAAG,SAAS,qBAAuB,GACrD,QAASA,EAAG,KAAK,SAAWS,EAAE,WAAa,cAC3C,OAAQT,EAAG,WAAW,gBAAkB,GAAQ,KAAOH,EAAO,MAChE,CAAC,EACDc,EAAM,KAAK,aAAa,qBAAsBd,EAAO,eAAe,YAAY,EAChFc,EAAM,KAAK,aACT,sBACA,OAAOd,EAAO,eAAe,aAAa,CAC5C,EAEA,QAAWgB,KAAWZ,EAAU,CAC9B,IAAMa,EAAQ,CAACD,EAAQ,SAAS,KAAMR,GAAMA,EAAE,SAAS,EACjDU,EAAMC,GAAiBH,EAASJ,EAAG,CACvC,MAAAK,EACA,KAAMD,EAAQ,IACd,eAAgBb,EAAG,aAAa,cAClC,CAAC,EAGD,GAFAW,EAAM,SAAS,YAAYI,CAAG,EAE1BD,EAAO,SAKX,IAAMG,EAAWC,GAAuBL,CAAO,EAC3CI,IAAUJ,EAAQ,kBAAoBI,EAAS,IACnD,IAAME,EAAWF,GAAYG,GAAYP,EAASA,EAAQ,iBAAiB,EAC3EQ,GAAmBN,EAAKF,EAASM,CAAQ,EACzCG,GAAaP,EAAKI,EAAUN,EAASN,CAAW,EAEhDgB,GACER,EACA,IAAMF,EACN,CAACW,EAASpB,IAAM,CACdkB,GAAaP,EAAKS,EAASpB,EAAGG,CAAW,EACzCkB,GAAcd,EAAM,KAAMV,EAAUM,CAAW,CACjD,CACF,CACF,CAIA,GAAIJ,EAAY,OAAS,EAAG,CAC1BQ,EAAM,IAAI,SAAW,GACrB,IAAMe,EAAQf,EAAM,IAAI,cAAc,kBAAkB,EACpDe,IACFA,EAAM,YAAcC,GAAsBlB,EAAGN,EAAY,MAAM,EAEnE,MACEQ,EAAM,IAAI,iBAAiB,QAAS,IAAM,CACxCb,EACE8B,GAAe3B,CAAQ,EAAE,IAAK4B,IAAU,CACtC,cAAe,gCAAkCA,EAAK,UACtD,SAAUA,EAAK,SACf,WAAYC,GAAqBjC,EAAO,GAAIA,EAAO,UAAU,CAC/D,EAAE,CACJ,CACF,CAAC,EAGHD,EAAU,YAAYe,EAAM,IAAI,EAChCoB,GAAcpB,EAAM,IAAI,EACxBc,GAAcd,EAAM,KAAMV,EAAUM,CAAW,EAE/CR,IAAYiC,GAAcrB,EAAM,IAAI,CAAC,CACvC,CC/GO,SAASsB,EACdC,EACAC,EACQ,CACR,IAAIC,EAAMF,EACV,QAAWG,KAAOF,EAChBC,EAAMA,EAAI,MAAM,KAAOC,EAAM,IAAI,EAAE,KAAK,OAAOF,EAAOE,CAAG,CAAC,CAAC,EAE7D,OAAOD,CACT,CAOO,SAASE,GACdC,EACA,EACQ,CACR,IAAMC,EAAM,EAAE,WACd,GAAI,CAACA,EAAK,MAAO,SAAWD,EAAM,SAClC,IAAME,EAAO,IAAI,KAAK,YAAY,EAAE,QAAU,IAAI,EAAE,OAAOF,CAAG,EAC9D,OAAON,EAAKO,EAAIC,CAAI,GAAKD,EAAI,OAAS,YAAa,CAAE,MAAOD,CAAI,CAAC,CACnE,CCtBO,SAASG,GACdC,EACAC,EACAC,EACQ,CACR,OAAOC,EAAKD,EAAE,YAAc,+BAAgC,CAC1D,MAAOF,EACP,MAAOC,CACT,CAAC,CACH,CAEO,SAASG,GACdC,EACA,EACQ,CACR,OAAOA,EAAY,EACfF,EAAK,EAAE,UAAY,uBAAwB,CAAE,MAAOE,CAAU,CAAC,EAC/D,EAAE,UAAY,UACpB,CAMO,SAASC,GACdD,EACAE,EACAC,EACAN,EACQ,CACR,GAAIG,GAAa,EAAG,OAAOH,EAAE,gBAAkB,0BAE/C,IAAMO,EACJF,IAAiB,cAAgBC,EAC7B,KAAK,MAAMA,CAAa,EACxB,EAEN,OAAOC,EAAM,EACTN,EAAKD,EAAE,kBAAoB,+CAAgD,CACzE,MAAOG,EACP,IAAKI,CACP,CAAC,EACDN,EACED,EAAE,oBAAsB,gDACxB,CAAE,MAAOG,CAAU,CACrB,CACN,CCnCO,IAAMK,GAA4B,CAAE,IAAK,EAAG,IAAK,EAAG,EAwCpD,SAASC,GACdC,EACAC,EACAC,EACa,CACb,GAAIA,GAAa,KAAM,CACrB,IAAMC,EAAQF,EAAS,gCAAkCC,CAAS,EAClE,GAAIC,GAAS,OAAOA,EAAM,KAAQ,UAAY,OAAOA,EAAM,KAAQ,SACjE,OAAOA,CAEX,CACA,IAAMC,EAAOH,EAAS,yBAA2BD,CAAS,EAC1D,MAAI,CAACI,GAAQ,OAAOA,EAAK,KAAQ,UAAY,OAAOA,EAAK,KAAQ,SACxDN,GAEFM,CACT,CAWO,SAASC,GACdH,EACAD,EACyB,CACzB,GAAIC,GAAa,KAAM,OACvB,IAAME,EAAOH,EAAS,gCAAkCC,CAAS,EACjE,GAAI,GAACE,GAAQ,OAAOA,EAAK,KAAQ,UAAY,OAAOA,EAAK,KAAQ,UAGjE,OAAOA,CACT,CASO,SAASE,GACdC,EACAC,EACU,CACV,IAAMC,EAAWF,GAAgB,CAAC,EAClC,GAAI,CAACC,EAAc,OAAOC,EAC1B,IAAMC,EAAO,OAAO,KAAKF,CAAY,EACrC,GAAIE,EAAK,SAAW,EAAG,OAAOD,EAC9B,IAAME,EAAmB,CAAE,GAAGF,CAAS,EACvC,QAAWG,KAAOF,EAAMC,EAAOC,CAAG,EAAIJ,EAAaI,CAAG,EACtD,OAAOD,CACT,CAYO,SAASE,GACdb,EACAC,EACAC,EACQ,CACR,OAAOH,GAAQC,EAAWC,EAAUC,CAAS,EAAE,GACjD,CAGO,SAASY,GAAWC,EAA+B,CACxD,IAAIC,EAAM,EACV,QAASC,EAAI,EAAGA,EAAIF,EAAM,OAAQE,IAAKD,GAAOD,EAAME,CAAC,EAAE,UAAY,EACnE,OAAOD,CACT,CAGO,SAASE,GACdH,EACAb,EACQ,CACR,QAASe,EAAI,EAAGA,EAAIF,EAAM,OAAQE,IAChC,GAAIF,EAAME,CAAC,GAAKF,EAAME,CAAC,EAAE,YAAcf,EAAW,OAAOe,EAE3D,MAAO,EACT,CAGO,SAASE,GACdJ,EACAb,EACQ,CACR,IAAMkB,EAAMF,GAAuBH,EAAOb,CAAS,EACnD,OAAOkB,IAAQ,GAAK,EAAIL,EAAMK,CAAG,EAAE,UAAY,CACjD,CAQO,SAASC,GACdjB,EACAkB,EACS,CACT,OAAOlB,EAAK,KAAOkB,CACrB,CAgBO,SAASC,GACdnB,EACAkB,EACAE,EACAC,EAAS,EACTC,EAAoB,EACpBC,EACQ,CACR,IAAIC,EAAU,KAAK,IAAIxB,EAAK,IAAMsB,EAAmBD,EAASH,CAAc,EAC5E,OAAI,OAAOK,GAAe,WAAUC,EAAU,KAAK,IAAIA,EAASD,CAAU,GACnE,OAAOH,GAAU,SAAW,KAAK,IAAII,EAASJ,CAAK,EAAII,CAChE,CC7LO,IAAMC,GAAe,CAAC,IAAK,IAAK,GAAG,EAC7BC,GAAgB,CAAC,IAAK,IAAK,IAAK,IAAI,EACpCC,GAAc,OACdC,GAAe,iCAGrB,SAASC,GAASC,EAAaC,EAAmB,CACvD,OAAKD,IACE,gBAAgB,KAAKA,CAAG,EAC3BA,EAAI,QAAQ,kBAAmB,WAAaC,CAAC,EAC7CD,GAAOA,EAAI,QAAQ,GAAG,IAAM,GAAK,IAAM,KAAO,SAAWC,EAC/D,CAEO,SAASC,GAAOF,EAAaG,EAA0B,CAC5D,OAAKH,EACEG,EAAO,IAAKF,GAAMF,GAASC,EAAKC,CAAC,EAAI,IAAMA,EAAI,GAAG,EAAE,KAAK,IAAI,EADnD,EAEnB,CCxBO,SAASG,GACdC,EACAC,EACsB,CACtB,IAAIC,EAA8C,KAClD,OAAO,YAA4BC,EAAS,CACtCD,GAAO,aAAaA,CAAK,EAC7BA,EAAQ,WAAW,IAAMF,EAAG,MAAM,KAAMG,CAAI,EAAGF,CAAK,CACtD,CACF,CAUO,SAASG,GAAcC,EAAwC,CACpE,OAAKA,EACEA,EACJ,UAAU,KAAK,EACf,QAAQ,mBAAoB,EAAE,EAC9B,YAAY,EAJE,EAKnB,CCsBA,IAAMC,GAAoB,IA+BnB,SAASC,GACdC,EACAC,EACAC,EACW,CACX,GAAM,CAAE,EAAAC,EAAG,SAAAC,EAAU,SAAAC,EAAU,YAAAC,CAAY,EAAIJ,EACzCK,EAAQC,GAAQR,EAAE,GAAII,CAAQ,EAE9BK,EAAM,SAAS,cAAc,KAAK,EACxCA,EAAI,UAAY,8BACXT,EAAE,YAAWS,EAAI,WAAa,0CACnCA,EAAI,aAAa,oBAAqB,EAAE,EACxCA,EAAI,aAAa,aAAcC,GAAcV,EAAE,KAAK,CAAC,EACrDS,EAAI,aAAa,YAAaT,EAAE,MAAQ,EAAE,EAK1C,IAAIW,EAAc,GAGdC,EAAkB,GAIhBC,EAAW,SAAS,cAAc,KAAK,EAC7CA,EAAS,UAAY,oCAGrB,IAAMC,EAAa,SAAS,cAAc,MAAM,EAChDA,EAAW,UAAY,kCACvBA,EAAW,YAAcX,EAAE,WAAa,QACxCW,EAAW,OAAS,GACpBD,EAAS,YAAYC,CAAU,EAE/B,IAAMC,EAAiBf,EAAE,SAAS,KAAMgB,GAAMA,EAAE,SAAS,GAAK,KACxDC,EACHF,GAAkBA,EAAe,OAAUf,EAAE,cAChD,GAAIiB,EAAiB,CACnB,IAAMC,EAAM,SAAS,cAAc,KAAK,EACxCA,EAAI,IAAMC,GAASF,EAAiBnB,EAAiB,EACrDoB,EAAI,OAASE,GAAOH,EAAiBI,EAAa,EAClDH,EAAI,MAAQI,GACZJ,EAAI,IAAMlB,EAAE,MACZkB,EAAI,QAAU,OACdL,EAAS,YAAYK,CAAG,CAC1B,CACAT,EAAI,YAAYI,CAAQ,EAIxB,IAAMU,EAAU,SAAS,cAAc,KAAK,EAC5CA,EAAQ,UAAY,mCAEpB,IAAMC,EAAU,SAAS,cAAc,GAAG,EAE1C,GADAA,EAAQ,UAAY,oCAChBxB,EAAE,IAAK,CAGT,IAAMyB,EAAY,SAAS,cAAc,GAAG,EAC5CA,EAAU,KAAOzB,EAAE,IACnByB,EAAU,OAAS,SACnBA,EAAU,IAAM,sBAChBA,EAAU,YAAczB,EAAE,MAC1BwB,EAAQ,YAAYC,CAAS,CAC/B,MACED,EAAQ,YAAcxB,EAAE,MAE1BuB,EAAQ,YAAYC,CAAO,EAI3B,IAAIE,EACFX,IAAmBf,EAAE,SAAS,OAAS,EAAIA,EAAE,SAAS,CAAC,EAAI,MAUvD2B,EAAkBX,GACtBR,GAAQR,EAAE,GAAII,EAAUY,EAAIA,EAAE,GAAK,IAAI,EAEnCY,EAAWF,EAAiBC,EAAeD,CAAc,EAAE,IAAM,EAEjEG,EAAU,SAAS,cAAc,GAAG,EAC1CA,EAAQ,UAAY,oCACpBA,EAAQ,aAAa,iBAAkB,EAAE,EACzC,IAAMC,EAAc,SAAS,cAAc,MAAM,EACjDA,EAAY,aAAa,sBAAuB,EAAE,EAC9CJ,IACFI,EAAY,YAAcxB,EAAYoB,EAAe,MAAQE,CAAQ,GAEvEC,EAAQ,YAAYC,CAAW,EAC/B,IAAMC,EAAiB,SAAS,cAAc,GAAG,EACjDA,EAAe,UAAY,sCAC3BA,EAAe,aAAa,mBAAoB,EAAE,EAClDF,EAAQ,YAAYE,CAAc,EAClCR,EAAQ,YAAYM,CAAO,EAG3B,SAASG,EACPC,EACAC,EACAC,EACM,CACN,IAAMC,EAAMF,GAAWA,EAAQ,eAAiBA,EAAQ,eAAiBC,EAAM,EACzEE,EAAQH,EAAUA,EAAQ,MAAQC,EAAM,EAC1CC,EAAMC,GACRJ,EAAG,YAAc3B,EAAY8B,CAAG,EAChCH,EAAG,OAAS,KAEZA,EAAG,YAAc,GACjBA,EAAG,OAAS,GAEhB,CACAD,EAAgBD,EAAgBL,EAAgBE,CAAQ,EAExD,IAAMU,EAAc,SAAS,cAAc,GAAG,EAC9CA,EAAY,UACV,sEACFA,EAAY,aAAa,sBAAuB,EAAE,EAClD,IAAMC,EAAcb,EAAiBA,EAAe,UAAY,KAC5Da,EAAaD,EAAY,YAAcC,EACtCD,EAAY,OAAS,GAC1Bf,EAAQ,YAAYe,CAAW,EAI/B,IAAIE,EAA+B,KAC/BC,EAAiC,KACjCC,EAAUf,EAAeD,CAAc,EAAE,IACzCiB,EAAehB,EAAeD,CAAc,EAAE,IAC9CkB,EAA6B,KAC7BC,EAEO,KAEX,GAAI7C,EAAE,WAAaE,EAAI,gBAAiB,CAGtCsC,EAAW,SAAS,cAAc,KAAK,EACvCA,EAAS,UACP,iEAEF,IAAMM,EAAc,SAAS,cAAc,KAAK,EAChDA,EAAY,UAAY,4BACxBA,EAAY,aAAa,OAAQ,OAAO,EACxCA,EAAY,aAAa,aAAc3C,EAAE,UAAY,UAAU,EAE/D,IAAM4C,EAAc,SAAS,cAAc,QAAQ,EACnDA,EAAY,KAAO,SACnBA,EAAY,UACV,2EACFA,EAAY,aACV,aACA5C,EAAE,kBAAoB,mBACxB,EACA4C,EAAY,YAAc,SAE1BN,EAAa,SAAS,cAAc,MAAM,EAC1CA,EAAW,UAAY,kCACvBA,EAAW,aAAa,eAAgB,EAAE,EAC1CA,EAAW,aAAa,YAAa,QAAQ,EAC7CA,EAAW,YAAc,OAAOd,EAAeD,CAAc,EAAE,GAAG,EAElE,IAAMsB,EAAa,SAAS,cAAc,QAAQ,EAClDA,EAAW,KAAO,SAClBA,EAAW,UACT,0EACFA,EAAW,aACT,aACA7C,EAAE,kBAAoB,mBACxB,EACA6C,EAAW,YAAc,IAEzBF,EAAY,YAAYC,CAAW,EACnCD,EAAY,YAAYL,CAAU,EAClCK,EAAY,YAAYE,CAAU,EAClCR,EAAS,YAAYM,CAAW,EAEhC,IAAMG,EAAUR,EAChBG,EAAQ,IAAM,CACZK,EAAQ,YAAc,OAAOP,CAAO,EACpCO,EAAQ,aAAa,WAAY,OAAOP,CAAO,CAAC,EAChDK,EAAY,SACVnC,GAAmB8B,GAAWf,EAAeD,CAAc,EAAE,IAC/DsB,EAAW,SAAWpC,GAAmB8B,GAAWC,CACtD,EAEAE,EAAsB,CAACX,EAASgB,EAAkB,IAAM,CACtD,IAAMC,EAAYjB,EAAUA,EAAQ,GAAK,KACnCkB,EACJD,IAAc,KAAO,EAAI9C,EAAS,OAAO8C,CAAS,EAC9CE,EACJF,IAAc,KACV,EACA9C,EAAS,sBAAsBL,EAAE,GAAImD,CAAS,EAI9CG,EACJH,IAAc,KACV,EACA9C,EAAS,UAAUL,EAAE,GAAImD,CAAS,EAElCI,EAAQrB,EAAUA,EAAQ,kBAAoB,KAG9CsB,EAAiB,KAAK,IAAI,EAAGN,EAAkBE,CAAM,EACrDK,GACJ,OAAOF,GAAU,SACb,KAAK,IAAI,EAAGA,EAAQC,CAAc,EAClC,OAKAE,GAAOC,GAAeR,EAAW/C,CAAQ,GAAG,IAC5CwD,GAAOjC,EAAeO,CAAO,EAAE,IAErCS,EACEW,EAAY,EACR,OAAOG,IAAa,SAClB,KAAK,IAAIH,EAAWG,EAAQ,EAC5BH,EACFO,GACEtD,EACAF,EAAS,eAAe,EACxBoD,GACAL,EACAC,EACAK,EACF,EAEFf,EAAeiB,GAGjBlB,EAAUkB,IAENlB,EAAUC,IAAcD,EAAUC,GAClCD,EAAUkB,KAAMlB,EAAUkB,KAEhChB,EAAO,CACT,EAQA,IAAMkB,EAAoB,IAAY,CAC/BpC,GACDrB,EAAS,UAAUqB,EAAe,GAAIgB,CAAO,GAC/CxC,EAAI,sBAAsB,CAE9B,EAEA6C,EAAY,iBAAiB,QAAS,IAAM,CACtCL,EAAUf,EAAeD,CAAc,EAAE,MAAKgB,GAAW,GAC7DE,EAAO,EACPkB,EAAkB,CACpB,CAAC,EACDd,EAAW,iBAAiB,QAAS,IAAM,CACrCN,EAAUC,IAAcD,GAAW,GACvCE,EAAO,EACPkB,EAAkB,CACpB,CAAC,EAEGpC,EAAgBmB,EAAoBnB,CAAc,EACjDkB,EAAM,CACb,CAOA,IAAMmB,EAAgB/D,EAAE,SAAS,MAAM,EACjCgE,EAAqC,CAAC,EAE5C,GAAID,EAAc,OAAS,EAAG,CAC5B,IAAME,EAAcjE,EAAE,aAAe,CAAC,EAGtC0B,EACEqC,EAAc,KAAM/C,GAAMA,EAAE,YAAc,EAAK,GAAK+C,EAAc,CAAC,EAErE,IAAMG,EAAsB,IAA8B,CACxD,IAAMC,EAASH,EAAc,IAAKI,GAAMA,EAAE,KAAK,EAC/C,QAAW,KAAKL,EACd,GAAI,GAAC,EAAE,SAAW,EAAE,QAAQ,SAAWI,EAAO,SAC1C,EAAE,QAAQ,MAAM,CAACE,EAAGC,IAAMD,IAAMF,EAAOG,CAAC,CAAC,EAAG,OAAO,EAEzD,OAAO,IACT,EAEMC,EAAwBvD,GAAoC,CAChE,GAAI,GAACA,GAAK,CAACA,EAAE,SACb,QAASwD,EAAI,EAAGA,EAAIR,EAAc,QAAUQ,EAAIxD,EAAE,QAAQ,OAAQwD,IAC5DR,EAAcQ,CAAC,EAAE,QAAUxD,EAAE,QAAQwD,CAAC,IACxCR,EAAcQ,CAAC,EAAE,MAAQxD,EAAE,QAAQwD,CAAC,EAG1C,EAEMC,EAAmB,CACvBC,EACAC,EACAC,IACY,CACZ,QAAW5D,KAAK+C,EAAe,CAE7B,GADI/C,EAAE,YAAc,IAChB,CAACA,EAAE,SAAWA,EAAE,QAAQ0D,CAAW,IAAMC,EAAO,SACpD,IAAIE,EAAK,GACT,QAASP,EAAI,EAAGA,EAAItD,EAAE,QAAQ,OAAQsD,IACpC,GAAIA,IAAMI,GACN1D,EAAE,QAAQsD,CAAC,IAAMM,EAASN,CAAC,EAAG,CAChCO,EAAK,GACL,KACF,CAEF,GAAIA,EAAI,MAAO,EACjB,CACA,MAAO,EACT,EAEMC,EAAqBF,GAA6B,CACtD,QAASJ,EAAI,EAAGA,EAAIR,EAAc,OAAQQ,IAAK,CAC7C,IAAMO,EAAMf,EAAcQ,CAAC,EAC3B,QAASH,EAAI,EAAGA,EAAIU,EAAI,QAAQ,OAAQV,IACtCU,EAAI,QAAQV,CAAC,EAAE,SAAW,CAACI,EACzBD,EACAO,EAAI,QAAQV,CAAC,EAAE,MACfO,CACF,CAEJ,CACF,EAEMI,EAAiB,IAAY,CACjC,IAAMhE,EAAIkD,EAAoB,EAC9B,GAAI,CAAClD,EAAG,CAGNuD,EAAqB7C,CAAc,EAC/BA,GAAgB,SAASoD,EAAkBpD,EAAe,OAAO,EACrE,MACF,CAEAA,EAAiBV,EACjBP,EAAI,aAAa,2BAA4B,OAAOO,EAAE,EAAE,CAAC,EAEzD,IAAMiE,EAActD,EAAeX,CAAC,EAAE,IACtCc,EAAY,YAAcxB,EAAYU,EAAE,MAAQiE,CAAW,EAC3DjD,EAAgBD,EAAgBf,EAAGiE,CAAW,EAE1CjE,EAAE,WACJsB,EAAY,YAActB,EAAE,UAC5BsB,EAAY,OAAS,KAErBA,EAAY,YAAc,GAC1BA,EAAY,OAAS,IAGvB,IAAM4C,EAASrE,EAAS,cAAc,KAAK,EACrCsE,EAASnE,EAAE,OAAShB,EAAE,cACxBkF,GAAUC,IACZD,EAAO,IAAM/D,GAASgE,EAAQrF,EAAiB,EAC/CoF,EAAO,OAAS9D,GAAO+D,EAAQ9D,EAAa,GAK9CwB,IAAsB7B,EAAGX,EAAS,sBAAsBW,EAAE,EAAE,CAAC,EACzDA,EAAE,SAAS8D,EAAkB9D,EAAE,OAAO,EAC1CoE,EAAQ,CACV,EAEMC,EAAkB,SAAS,cAAc,KAAK,EACpDA,EAAgB,UAAY,kCAE5B,QAASC,EAAK,EAAGA,EAAKrB,EAAY,OAAQqB,IAAM,CAC9C,IAAMC,EAAUtB,EAAYqB,CAAE,EACxBE,EAAU,SAAS,cAAc,KAAK,EAC5CA,EAAQ,UAAY,iCAEpB,IAAMC,EAAU,SAAS,cAAc,MAAM,EAC7CA,EAAQ,UAAY,iCACpBA,EAAQ,YAAcF,EACtBC,EAAQ,YAAYC,CAAO,EAE3B,IAAMV,EAAM,SAAS,cAAc,QAAQ,EAC3CA,EAAI,UAAY,+BAChBA,EAAI,aAAa,sBAAuB,EAAE,EAC1CA,EAAI,aAAa,uBAAwB,OAAOO,EAAK,CAAC,CAAC,EACvDP,EAAI,KAAO,cAAgB/E,EAAE,GAAK,KAAOsF,EAAK,GAC9CP,EAAI,aAAa,aAAcQ,CAAO,EAEtC,IAAMG,EAAsC,OAAO,OAAO,IAAI,EACxDC,GAAajE,GAAgB,UAAU4D,CAAE,EAC/C,QAAWM,MAAM7B,EAAe,CAC9B,IAAM8B,GAAMD,GAAG,SAAWA,GAAG,QAAQN,CAAE,EACvC,GAAI,CAACO,IAAOH,EAAWG,EAAG,EAAG,SAC7BH,EAAWG,EAAG,EAAI,GAClB,IAAMC,GAAM,SAAS,cAAc,QAAQ,EAC3CA,GAAI,MAAQD,GACZC,GAAI,YAAcD,GACdA,KAAQF,KAAYG,GAAI,SAAW,IACvCf,EAAI,YAAYe,EAAG,CACrB,CAEAf,EAAI,iBAAiB,SAAUC,CAAc,EAC7ChB,EAAc,KAAKe,CAAG,EACtBS,EAAQ,YAAYT,CAAG,EACvBM,EAAgB,YAAYG,CAAO,CACrC,CAEAjE,EAAQ,YAAY8D,CAAe,EAC/B3D,IACFjB,EAAI,aAAa,2BAA4B,OAAOiB,EAAe,EAAE,CAAC,EAClEA,EAAe,SAASoD,EAAkBpD,EAAe,OAAO,EAExE,SACEqC,EAAc,SAAW,GACzBA,EAAc,CAAC,EAAE,QAAU,gBAC3B,CACA,IAAMgC,EAAW,SAAS,cAAc,MAAM,EAC9CA,EAAS,UAAY,+BACrBA,EAAS,YAAchC,EAAc,CAAC,EAAE,MACxCxC,EAAQ,YAAYwE,CAAQ,CAC9B,CAIA,IAAIC,EAAmC,KACnChG,EAAE,YACJgG,EAAe,SAAS,cAAc,MAAM,EAC5CA,EAAa,UAAY,kCACzBA,EAAa,OAAS,GACtBzE,EAAQ,YAAYyE,CAAY,GAGlC,IAAIC,EAAmC,KACvC,GAAIjG,EAAE,UAAW,CACf,IAAMkG,EAAa,SAAS,cAAc,KAAK,EAC/CA,EAAW,UAAY,sCACnB1D,GAAU0D,EAAW,YAAY1D,CAAQ,EAE7CyD,EAAS,SAAS,cAAc,QAAQ,EACxCA,EAAO,KAAO,SACdA,EAAO,UAAY,0BACnBA,EAAO,YAAc9F,EAAE,SAAW,MAGlC8F,EAAO,aAAa,cAAe9F,EAAE,SAAW,OAAS,IAAMH,EAAE,KAAK,EACtEiG,EAAO,aAAa,mBAAoB,OAAOhG,CAAQ,CAAC,EACxDiG,EAAW,YAAYD,CAAM,EAC7B1E,EAAQ,YAAY2E,CAAU,CAChC,KAAO,CACL,IAAMC,EAAe,SAAS,cAAc,MAAM,EAClDA,EAAa,UAAY,qCACzBA,EAAa,YAAchG,EAAE,SAAW,WACxCM,EAAI,aAAa,gBAAiB,MAAM,EACxCc,EAAQ,YAAY4E,CAAY,CAClC,CAEA1F,EAAI,YAAYc,CAAO,EAIvB,IAAI6E,EAA+B1E,EAAiBA,EAAe,GAAK,KAExE,SAAS0D,GAAgB,CACvB,GAAI,CAAC1D,EAAgB,OACrB,IAAMQ,EAAUR,EAEV6B,EAAQrB,EAAQ,kBAChBmE,EACJ,OAAO9C,GAAU,SACblD,EAAS,sBAAsB6B,EAAQ,EAAE,EACzC,EACAoE,EAAWjG,EAAS,WAAW6B,EAAQ,EAAE,EAE/C,GAAIW,EAAqB,CAIvB,IAAM0D,EAAYlG,EAAS,OAAO6B,EAAQ,EAAE,EACxCqE,EAAY,EAAG7D,EAAU6D,EACpBH,IAAkBlE,EAAQ,KACjCQ,EAAUf,EAAeO,CAAO,EAAE,KACpCkE,EAAgBlE,EAAQ,GACxBW,EAAoBX,EAASmE,CAAO,CACtC,CAIA,IAAMG,EAAenG,EAAS,UAAUL,EAAE,GAAIkC,EAAQ,EAAE,EAClDuE,EAAQ9E,EAAeO,CAAO,EAC9BwE,EAAcF,GAAgBC,EAAM,IAEpCE,EAAYtG,EAAS,eAAe,EACpCuG,EACJ,CAACN,GAAY,CAACI,GAAe,CAACG,GAAaJ,EAAOE,CAAS,EAa7D,GAXIX,IAGEY,GAAkBD,EAAY,GAChCX,EAAa,YAAcc,GAAiBL,EAAM,IAAKtG,CAAC,EACxD6F,EAAa,OAAS,IAEtBA,EAAa,OAAS,IAItBC,EACF,GAAIK,EACFL,EAAO,SAAW,GACb5F,EAAS,UAAU6B,EAAQ,EAAE,GAWhC+D,EAAO,YAAc9F,EAAE,YAAc,SACrC8F,EAAO,aACL,cACC9F,EAAE,YAAc,UAAY,IAAMH,EAAE,KACvC,EACAiG,EAAO,UAAU,OAAO,mCAAmC,IAb3DA,EAAO,SAAW,GAClBA,EAAO,YAAc9F,EAAE,UAAY,WACnC8F,EAAO,aACL,aACAjG,EAAE,MAAQ,oCACZ,EACAiG,EAAO,UAAU,IAAI,mCAAmC,GAS1DA,EAAO,UAAU,IAAI,gCAAgC,EACrDxF,EAAI,UAAU,IAAI,wCAAwC,EAC1DK,EAAW,OAAS,OACf,CACL,IAAMiG,EAAY5G,EAAE,UAAY,OAC1B6G,EAAQN,EAAcK,EAAY5G,EAAE,SAAW,MACrD8F,EAAO,YAAce,EACrBf,EAAO,aAAa,aAAce,EAAQ,IAAMhH,EAAE,KAAK,EACvDiG,EAAO,UAAU,OAAO,gCAAgC,EACxDA,EAAO,UAAU,OAAO,mCAAmC,EAC3DxF,EAAI,UAAU,OAAO,wCAAwC,EAC7DK,EAAW,OAAS,GAEpB,IAAMmG,EACJ,OAAO1D,GAAU,SACb,KAAK,IAAI,EAAGA,EAAQ8C,CAAO,EAAII,EAAM,IACrC,GAEN,GAAIC,EAEFT,EAAO,SAAWgB,MACb,CAGL,IAAMC,EACJ3G,EAAM,IAAMF,EAAS,sBAAsBL,EAAE,GAAIkC,EAAQ,EAAE,EAC3DuE,EAAM,IACRR,EAAO,SACLgB,GACAC,GACAN,GACAvG,EAAS,OAAO,CACpB,CACF,CAMFM,EAAc,CAACX,EAAE,UACjBY,EAAkB,CAACZ,EAAE,WAAc,CAACsG,GAAY,CAAC,CAACL,GAAQ,SAC1DrD,IAAQ,EACR,QAAWmC,KAAOf,EAAee,EAAI,SAAWpE,CAClD,CAEA,MAAO,CAAE,GAAIF,EAAK,QAAA2E,CAAQ,CAC5B,CC3pBA,IAAM+B,GACJ,uIAWK,SAASC,GAAaC,EAAuC,CAClE,IAAMC,EAAQD,EAAU,iBAA8BF,EAAS,EACzDI,EAAwB,CAAC,EAC/B,QAASC,EAAI,EAAGA,EAAIF,EAAM,OAAQE,IAC5BF,EAAME,CAAC,EAAE,eAAiB,MAAMD,EAAO,KAAKD,EAAME,CAAC,CAAC,EAE1D,OAAOD,CACT,CCVA,IAAIE,GAAS,EACTC,GAAY,EAET,SAASC,IAAa,CAE3B,GADAD,KACIA,GAAY,EAAG,OAEnBD,GAAS,OAAO,aAAe,SAAS,gBAAgB,UACxD,IAAMG,EAAO,SAAS,KACtBA,EAAK,MAAM,SAAW,QACtBA,EAAK,MAAM,IAAM,IAAMH,GAAS,KAChCG,EAAK,MAAM,KAAO,IAClBA,EAAK,MAAM,MAAQ,IACnBA,EAAK,MAAM,SAAW,QACxB,CAEO,SAASC,IAAe,CAG7B,GAFIH,IAAa,IACjBA,KACIA,GAAY,GAAG,OAEnB,IAAME,EAAO,SAAS,KACtBA,EAAK,MAAM,SAAW,GACtBA,EAAK,MAAM,IAAM,GACjBA,EAAK,MAAM,KAAO,GAClBA,EAAK,MAAM,MAAQ,GACnBA,EAAK,MAAM,SAAW,GACtB,OAAO,SAAS,EAAGH,EAAM,CAC3B,CCpBO,SAASK,GACdC,EACAC,EACAC,EACM,CACN,IAAMC,EAAuB,CAAC,EACxBC,EAAsB,CAAC,EAC7B,QAAS,EAAI,EAAG,EAAIH,EAAO,OAAQ,KAChCC,EAAQ,CAAC,EAAIE,EAAOD,GAAO,KAAKF,EAAO,CAAC,CAAC,EAE5C,QAAWI,KAAMF,EAAOH,EAAK,YAAYK,CAAE,EAC3C,QAAWA,KAAMD,EAAMJ,EAAK,YAAYK,CAAE,CAC5C,CCLA,IAAMC,GAAY,UAEZC,GAAsB,IACtBC,GAAqB,IAuCpB,SAASC,GAAkBC,EAA8B,CAC9D,GAAM,CAAE,UAAAC,EAAW,KAAAC,EAAM,EAAAC,EAAG,iBAAAC,EAAkB,MAAAC,CAAM,EAAIL,EAElDM,EAAUL,EAAU,cAA2B,sBAAsB,EAMvEK,GAAWN,EAAK,cAAcA,EAAK,aAAa,YAAYM,CAAO,EAEvE,IAAMC,EAA4BC,GAChCF,EAAUA,EAAQ,cAAiBE,CAAG,EAAI,KAEtCC,EAAcF,EAAe,iBAAiB,EAC9CG,EAAcH,EAAoB,qBAAqB,EACvDI,EAAcJ,EAAe,2BAA2B,EACxDK,EAAYL,EAAe,mBAAmB,EAC9CM,EAAaN,EAAe,oBAAoB,EAChDO,EAAYP,EAAe,mBAAmB,EAC9CQ,EAAWR,EAAe,oBAAoB,EAC9CS,EAAUT,EAAe,mBAAmB,EAC5CU,EAAgBV,EAAe,2BAA2B,EAC1DW,EAAaX,EAAe,uBAAuB,EACnDY,EAAmBZ,EAAe,sBAAsB,EAExDa,EAAkBlB,EAAK,kBAAoB,GAC7CmB,EAAazB,GACb0B,EAAmB,GACnBC,EAAe,GACfC,EAAY,GACZC,EAAoB,CAAC,EAEzB,SAASC,GAAgC,CACvC,QAAWC,KAAKF,EAAME,EAAE,QAAQ,CAClC,CAEA,SAASC,GAAwB,CAG/B,IAAMC,EAAQ,KAAK,IAAIC,GAAWzB,CAAK,EAAGL,EAAK,WAAW,EACpD+B,EAAY,KAAK,IAAI,EAAG/B,EAAK,YAAc6B,CAAK,EAElDZ,IACFA,EAAc,YAAce,GAAiBH,EAAO7B,EAAK,YAAaG,CAAC,GAErEe,IACFA,EAAW,YAAce,GACvBF,EACA7B,EAAK,aACLA,EAAK,cACLC,CACF,EAEJ,CAEA,SAAS+B,GAAgB,CACvBR,EAAwB,EACxBE,EAAgB,CAClB,CAIA,SAASO,GAAyB,CAChC,GAAKvB,EAIL,CAAAZ,EAAK,UAAU,UAAUY,CAAS,EAClCA,EAAU,UAAY,GACtBa,EAAO,CAAC,EAER,QAASW,EAAI,EAAGA,EAAIhC,EAAiB,OAAQgC,IAAK,CAChD,IAAMC,EAAMC,GAAelC,EAAiBgC,CAAC,EAAGA,EAAG,CACjD,EAAAjC,EACA,SAAUH,EAAK,SACf,gBAAiBA,EAAK,gBACtB,SAAUA,EAAK,YACf,YAAaA,EAAK,YAClB,sBAAuB,IAAM,CAC3BA,EAAK,mBAAmB,EACxBkC,EAAQ,CACV,CACF,CAAC,EACDT,EAAK,KAAKY,CAAG,EACbzB,EAAU,YAAYyB,EAAI,EAAE,CAC9B,CAIArC,EAAK,UAAU,QAAQY,CAAS,EAEhCA,EAAU,iBAAiB,QAAS2B,CAAW,EACjD,CAEA,SAASA,EAAYC,EAAqB,CACxC,IAAMC,EAAUD,EAAE,OAAuB,QACvC,oBACF,EACA,GAAI,CAACC,GAAUA,EAAO,SAAU,OAEhC,IAAMC,EAAY,SAChBD,EAAO,aAAa,kBAAkB,GAAK,GAC3C,EACF,EACME,EAAOvC,EAAiBsC,CAAS,EACvC,GAAI,CAACC,EAAM,OAIX,IAAMN,EAAMI,EAAO,QAAqB,8BAA8B,EAChEG,EAAaP,GAAOA,EAAI,aAAa,0BAA0B,EACjEQ,GACDD,EACGD,EAAK,SAAS,KAAM,GAAM,EAAE,KAAO,SAASC,EAAY,EAAE,CAAC,EAC3D,OAAS,KAIf,GAHKC,IACHA,EAAkBF,EAAK,SAAS,KAAM,GAAM,EAAE,SAAS,GAAK,MAE1D,CAACE,EAAiB,OAItB,GAAI7C,EAAK,WAAW6C,EAAgB,EAAE,EAAG,CACvC7C,EAAK,cAAc6C,EAAgB,EAAE,EACrCX,EAAQ,EACR,MACF,CAIA,IAAMY,EAAWT,GAAK,cAA2B,gBAAgB,EAC3DU,EAAYD,EACd,KAAK,IACH,EACA,SACEA,EAAS,aAAa,UAAU,GAAKA,EAAS,aAAe,GAC7D,EACF,GAAK,CACP,EACAE,GAAoBL,EAAK,GAAI3C,EAAK,SAAU6C,EAAgB,EAAE,EAE5DI,EAAwB,CAC5B,UAAWN,EAAK,GAChB,UAAWE,EAAgB,GAC3B,MAAOF,EAAK,MACZ,IAAKA,EAAK,KAAO,KACjB,aAAcE,EAAgB,MAC9B,cAAeA,EAAgB,OAASF,EAAK,eAAiB,KAC9D,MAAOE,EAAgB,MACvB,eAAgBA,EAAgB,gBAAkB,KAClD,UAAWA,EAAgB,WAAa,KACxC,SAAUE,CACZ,EAIMG,EAAYlD,EAAK,aAAa2C,EAAK,GAAIE,EAAgB,EAAE,EAC/D,GAAIK,EAAY,EAAG,CACjBlD,EAAK,YAAYiD,EAAS,KAAK,IAAIF,EAAWG,CAAS,CAAC,EACxDhB,EAAQ,EACR,MACF,CAEIlC,EAAK,WAAW,IAEpBA,EAAK,aAAaiD,CAAO,EACzBf,EAAQ,EACV,CAIA,SAASiB,GAAqB,CAC5B,GAAI,CAAChC,GAAoB,CAACC,EAAiB,OAE3C,IAAMgC,EAAkB,CAAC,EACnBC,EAAgC,OAAO,OAAO,IAAI,EACxD,QAAWC,KAAKlD,EAAkB,CAChC,IAAMmD,GAAMD,EAAE,MAAQ,IAAI,KAAK,EAC3BC,GAAM,CAACF,EAAKE,CAAE,IAChBF,EAAKE,CAAE,EAAI,GACXH,EAAM,KAAKG,CAAE,EAEjB,CAGA,GAAIH,EAAM,OAAS,EAAG,CACpBjC,EAAiB,MAAM,QAAU,OACjCV,GAAa,UAAU,IAAI,qCAAqC,EAChE,MACF,CAEAU,EAAiB,UAAY,GAC7B,QAAWqC,IAAS,CAAC5D,GAAW,GAAGwD,CAAK,EAAG,CACzC,IAAMK,EAAO,SAAS,cAAc,QAAQ,EAC5CA,EAAK,KAAO,SACZA,EAAK,UAAY,uBACjBA,EAAK,aAAa,cAAeD,CAAK,EACtCC,EAAK,aAAa,eAAgBD,IAAUnC,EAAa,OAAS,OAAO,EACrEmC,IAAUnC,GAAYoC,EAAK,UAAU,IAAI,8BAA8B,EAC3EA,EAAK,YAAcD,IAAU5D,GAAYO,EAAE,UAAY,MAAQqD,EAC/DC,EAAK,iBAAiB,QAAS,IAAM,CACnCpC,EAAamC,EACbE,EAAqB,EACrBC,EAAejD,EAAcA,EAAY,MAAQ,EAAE,CACrD,CAAC,EACDS,EAAiB,YAAYsC,CAAI,CACnC,CACF,CAEA,SAASC,GAA6B,CACpC,GAAI,CAACvC,EAAkB,OACvB,IAAMyC,EAAQzC,EAAiB,iBAA8B,eAAe,EAC5E,QAASiB,EAAI,EAAGA,EAAIwB,EAAM,OAAQxB,IAAK,CACrC,IAAMyB,EAAKD,EAAMxB,CAAC,EAAE,aAAa,aAAa,IAAMf,EACpDuC,EAAMxB,CAAC,EAAE,UAAU,OAAO,+BAAgCyB,CAAE,EAC5DD,EAAMxB,CAAC,EAAE,aAAa,eAAgByB,EAAK,OAAS,OAAO,CAC7D,CACF,CAIA,SAASF,EAAeG,EAAqB,CAC3C,GAAI,CAAClD,EAAW,OAChB,IAAMmD,EAAkBC,GAAcF,CAAK,EACrCG,EAAYrD,EAAU,iBAC1B,qBACF,EACIsD,EAAe,EAEnB,QAAS9B,EAAI,EAAGA,EAAI6B,EAAU,OAAQ7B,IAAK,CACzC,IAAM+B,EAAQF,EAAU7B,CAAC,EAAE,aAAa,YAAY,GAAK,GACnDgC,EAAOH,EAAU7B,CAAC,EAAE,aAAa,WAAW,GAAK,GACjDiC,GACH,CAACN,GAAmBI,EAAM,QAAQJ,CAAe,IAAM,MACvD1C,IAAezB,IAAawE,IAAS/C,GACxC4C,EAAU7B,CAAC,EAAE,UAAU,OAAO,YAAa,CAACiC,CAAO,EAC/CA,GAASH,GACf,CAEIrD,IACFA,EAAW,MAAM,QACfqD,IAAiB,GAAKH,EAAgB,OAAS,EAAI,GAAK,QAExDpD,IACFA,EAAY,MAAM,QAAUmD,EAAM,OAAS,EAAI,GAAK,QAElDhD,IACFA,EAAU,YAAcwD,EACtBnE,EAAE,gBAAkB,2BACpB,CAAE,MAAO+D,CAAa,CACxB,EAEJ,CAEA,GAAIxD,EAAa,CACf,IAAM6D,EAAeC,GACnB,IAAMb,EAAejD,EAAY,KAAK,EACtCZ,EACF,EACAY,EAAY,iBAAiB,QAAS6D,CAAY,CACpD,CACI5D,GACFA,EAAY,iBAAiB,QAAS,IAAM,CACrCD,IACLA,EAAY,MAAQ,GACpBiD,EAAe,EAAE,EACjBjD,EAAY,MAAM,EACpB,CAAC,EAKH,SAAS+D,GAAa,CACpB,GAAI,GAACnE,GAAWkB,GAsBhB,IArBAA,EAAY,GAEPF,IACHa,EAAiB,EACjBb,EAAmB,IAEhBC,IACH4B,EAAa,EACb5B,EAAe,IAGjBW,EAAQ,EAERb,EAAazB,GACb8D,EAAqB,EACjBhD,IAAaA,EAAY,MAAQ,IACrCiD,EAAejD,EAAcA,EAAY,MAAQ,EAAE,EAK/CE,EAAW,CACb,IAAM8D,EAA2C,OAAO,OAAO,IAAI,EACnE,QAAWC,KAAMtE,EAAOqE,EAAmB,OAAOC,EAAG,SAAS,CAAC,EAAI,GACnEC,GACEhE,EACAa,EAAK,IAAKE,GAAMA,EAAE,EAAE,EACnBS,GAAMsC,EAAmB,OAAOtE,EAAiBgC,CAAC,EAAE,EAAE,CAAC,IAAM,EAChE,EACAxB,EAAU,UAAY,CACxB,CAEAN,EAAQ,MAAM,QAAU,GAEnBA,EAAQ,aACbA,EAAQ,UAAU,IAAI,mCAAmC,EAEzDuE,GAAK,EAIL,WAAW,IAAM,CACX9D,EAAUA,EAAS,MAAM,EACxBN,GAAa,MAAM,CAC1B,EAAG,EAAE,EACP,CAEA,SAASqE,GAAc,CACrB,GAAI,CAACxE,GAAW,CAACkB,EAAW,OAC5BA,EAAY,GAEZlB,EAAQ,UAAU,OAAO,mCAAmC,EAC5DyE,GAAO,EAEP,WAAW,IAAM,CACVvD,IAAWlB,EAAQ,MAAM,QAAU,OAC1C,EAAGT,EAAmB,EAEtB,IAAMmF,EAAYhF,EAAK,cACnBgF,GAAa,OAAOA,EAAU,OAAU,YAC1C,WAAW,IAAMA,EAAU,MAAM,EAAG,CAAC,CAEzC,CAOA,GALAjE,GAAU,iBAAiB,QAAS+D,CAAK,EACzC9D,GAAS,iBAAiB,QAAS8D,CAAK,EAIpCxE,EAAS,CACX,IAAI2E,EAAsC,KAC1C3E,EAAQ,iBAAiB,YAAckC,GAAM,CAC3CyC,EAAkBzC,EAAE,MACtB,CAAC,EACDlC,EAAQ,iBAAiB,UAAYkC,GAAM,CACrCA,EAAE,SAAWlC,GAAW2E,IAAoB3E,GAASwE,EAAM,EAC/DG,EAAkB,IACpB,CAAC,CACH,CAEA,gBAAS,iBAAiB,UAAYzC,GAAM,CAC1C,GAAKhB,EAEL,IAAIgB,EAAE,MAAQ,SAAU,CACtBA,EAAE,eAAe,EACjBsC,EAAM,EACN,MACF,CAEA,GAAItC,EAAE,MAAQ,OAAS/B,EAAa,CAClC,IAAMyE,EAAYC,GAAa1E,CAAW,EAC1C,GAAIyE,EAAU,SAAW,EAAG,CAC1B1C,EAAE,eAAe,EACjB,MACF,CACA,IAAM4C,EAAQF,EAAU,CAAC,EACnBG,EAAOH,EAAUA,EAAU,OAAS,CAAC,EACvC1C,EAAE,UAAY,SAAS,gBAAkB4C,GAC3C5C,EAAE,eAAe,EACjB6C,EAAK,MAAM,GACF,CAAC7C,EAAE,UAAY,SAAS,gBAAkB6C,IACnD7C,EAAE,eAAe,EACjB4C,EAAM,MAAM,EAEhB,EACF,CAAC,EAEM,CAAE,KAAAX,EAAM,MAAAK,EAAO,OAAQ,IAAMtD,EAAW,QAAAU,EAAS,QAAA5B,CAAQ,CAClE,CC3aO,SAASgF,GACdC,EACAC,EACQ,CACR,OAAO,KAAK,IAAI,EAAGA,EAAcC,GAAWF,CAAK,CAAC,CACpD,CAEO,SAASG,GAAWH,EAAuBC,EAA8B,CAC9E,OAAOC,GAAWF,CAAK,GAAKC,CAC9B,CASO,SAASG,GACdJ,EACAK,EACQ,CACR,IAAIC,EAAM,EACV,QAASC,EAAI,EAAGA,EAAIP,EAAM,OAAQO,IAC5BP,EAAMO,CAAC,EAAE,YAAcF,IAAWC,GAAON,EAAMO,CAAC,EAAE,UAAY,GAEpE,OAAOD,CACT,CAQO,SAASE,GACdR,EACAS,EACAC,EACQ,CACR,IAAIJ,EAAM,EACV,QAASC,EAAI,EAAGA,EAAIP,EAAM,OAAQO,IAAK,CACrC,IAAMI,EAAKX,EAAMO,CAAC,EACdI,GAAMA,EAAG,YAAcF,GAAaE,EAAG,YAAcD,IACvDJ,GAAOK,EAAG,UAAY,EAE1B,CACA,OAAOL,CACT,CAeO,SAASM,GACdZ,EACAa,EACAC,EACS,CACT,GAAIC,GAAeF,EAAK,UAAWC,CAAQ,GAAG,SAAU,MAAO,GAE/D,IAAME,EAAOC,GAAQJ,EAAK,UAAWC,CAAQ,EAC7C,GAAI,CAACE,EAAK,SAAU,MAAO,GAE3B,IAAIE,EAAiB,EACrB,QAASC,EAAK,EAAGA,EAAKnB,EAAM,OAAQmB,IAAM,CACxC,IAAMC,EAAKpB,EAAMmB,CAAE,EACfC,IAAOP,GAAQO,EAAG,YAAcP,EAAK,YACvCK,GAAkBE,EAAG,UAAY,EAErC,CACA,OAAOF,GAAkBF,EAAK,GAChC,CAYO,SAASK,GACdrB,EACAS,EACAJ,EACAS,EACQ,CACR,IAAME,EAAOC,GAAQR,EAAWK,CAAQ,EAExC,MADI,CAACE,EAAK,UAAYA,EAAK,MAAQA,EAAK,KACpCM,GAAuBtB,EAAOK,CAAS,IAAM,GAAW,EACrDG,GAAyBR,EAAOS,EAAWJ,CAAS,CAC7D,CC1HA,SAASkB,IAAyB,CAChC,IAAMC,EAAK,6BACLC,EAAM,SAAS,gBAAgBD,EAAI,KAAK,EAC9CC,EAAI,aAAa,QAAS,IAAI,EAC9BA,EAAI,aAAa,SAAU,IAAI,EAC/BA,EAAI,aAAa,UAAW,WAAW,EACvCA,EAAI,aAAa,OAAQ,MAAM,EAC/B,OAAW,CAACC,EAAIC,EAAIC,EAAIC,CAAE,GAAK,CAC7B,CAAC,IAAK,IAAK,KAAM,IAAI,EACrB,CAAC,KAAM,IAAK,IAAK,IAAI,CACvB,EAAG,CACD,IAAMC,EAAO,SAAS,gBAAgBN,EAAI,MAAM,EAChDM,EAAK,aAAa,KAAMJ,CAAE,EAC1BI,EAAK,aAAa,KAAMH,CAAE,EAC1BG,EAAK,aAAa,KAAMF,CAAE,EAC1BE,EAAK,aAAa,KAAMD,CAAE,EAC1BC,EAAK,aAAa,SAAU,cAAc,EAC1CA,EAAK,aAAa,eAAgB,GAAG,EACrCA,EAAK,aAAa,iBAAkB,OAAO,EAC3CL,EAAI,YAAYK,CAAI,CACtB,CACA,OAAOL,CACT,CAQO,SAASM,GACdC,EACAC,EACAC,EAEAC,EACAC,EACAC,EACa,CACb,IAAMC,EAAO,SAAS,cAAc,KAAK,EACzCA,EAAK,UAAY,gDAEjB,IAAMC,EAAQ,SAAS,cAAc,KAAK,EAE1C,GADAA,EAAM,UAAY,sBACdP,EAAK,cAAe,CACtB,IAAMQ,EAAM,SAAS,cAAc,KAAK,EACxCA,EAAI,IAAMC,GAAST,EAAK,cAAe,GAAG,EAC1CQ,EAAI,OAASE,GAAOV,EAAK,cAAeW,EAAY,EACpDH,EAAI,MAAQI,GACZJ,EAAI,IAAMR,EAAK,MACfQ,EAAI,QAAU,OACdD,EAAM,YAAYC,CAAG,CACvB,CACAF,EAAK,YAAYC,CAAK,EAEtB,IAAMM,EAAO,SAAS,cAAc,KAAK,EACzCA,EAAK,UAAY,4BAIjB,IAAMC,EAAQ,SAAS,cAAc,QAAQ,EAU7C,GATAA,EAAM,KAAO,SACbA,EAAM,UAAY,6BAClBA,EAAM,YAAcd,EAAK,MACzBc,EAAM,aACJ,cACCb,EAAM,eAAiB,kBAAoB,KAAOD,EAAK,KAC1D,EACAa,EAAK,YAAYC,CAAK,EAElBd,EAAK,cAAgBA,EAAK,eAAiB,gBAAiB,CAC9D,IAAMe,EAAU,SAAS,cAAc,MAAM,EAC7CA,EAAQ,UAAY,+BACpBA,EAAQ,YAAcf,EAAK,aAC3Ba,EAAK,YAAYE,CAAO,CAC1B,CAEA,IAAMC,EAAW,SAAS,cAAc,MAAM,EAC9CA,EAAS,UAAY,6BACrB,IAAMC,EAAcjB,EAAK,gBAAkB,EAC3C,GAAIiB,EAAcjB,EAAK,MAAO,CAC5B,IAAMkB,EAAW,SAAS,cAAc,GAAG,EAC3CA,EAAS,UAAY,+BACrBA,EAAS,YAAchB,EAAYe,CAAW,EAC9CD,EAAS,YAAYE,CAAQ,CAC/B,CACA,IAAMC,EAAS,SAAS,cAAc,MAAM,EAC5CA,EAAO,UAAY,0BACnBA,EAAO,YAAcjB,EAAYF,EAAK,KAAK,EAC3CgB,EAAS,YAAYG,CAAM,EAG3B,IAAMC,EAAY,SAAS,cAAc,MAAM,EAM/C,GALAA,EAAU,UAAY,uBACtBA,EAAU,YAAc,QAAOpB,EAAK,UAAY,GAChDgB,EAAS,YAAYI,CAAS,EAC9BP,EAAK,YAAYG,CAAQ,EAErBhB,EAAK,UAAW,CAClB,IAAMqB,EAAc,SAAS,cAAc,MAAM,EACjDA,EAAY,UAAY,+BACxBA,EAAY,YAAcrB,EAAK,UAC/Ba,EAAK,YAAYQ,CAAW,CAC9B,CAGA,GAFAf,EAAK,YAAYO,CAAI,EAEjBV,EAAW,CACb,IAAMmB,EAAY,SAAS,cAAc,QAAQ,EACjDA,EAAU,KAAO,SACjBA,EAAU,UAAY,4BACtBA,EAAU,aACR,cACCrB,EAAM,YAAc,UAAY,IAAMD,EAAK,KAC9C,EACAsB,EAAU,YAAY/B,GAAW,CAAC,EAClC+B,EAAU,iBAAiB,QAAUC,GAAM,CACzCA,EAAE,gBAAgB,EAClBnB,EAAS,CACX,CAAC,EACDE,EAAK,YAAYgB,CAAS,CAC5B,KAAO,CAGL,IAAME,EAAe,SAAS,cAAc,MAAM,EAClDA,EAAa,UAAY,8BACzBA,EAAa,YAAcvB,EAAM,UAAY,WAC7CK,EAAK,YAAYkB,CAAY,CAC/B,CAIA,OAAAlB,EAAK,iBAAiB,QAASD,CAAM,EAC9BC,CACT,CCvHO,SAASmB,GACdC,EACAC,EACAC,EACAC,EACAC,EACM,CACN,IAAMC,EAAQF,EAAE,cAAgB,CAAC,EAC3BG,EAAYN,EAAK,cAA2B,wBAAwB,EAE1E,GAAIM,EAAW,CACbA,EAAU,UAAY,GACtB,QAASC,EAAI,EAAGA,EAAIN,EAAM,OAAQM,IAAK,CACrC,IAAMC,EAAQD,EACdD,EAAU,YACRG,GACER,EAAMO,CAAK,EACXH,EACAD,EAAK,YACLM,GAAcT,EAAOA,EAAMO,CAAK,EAAGJ,EAAK,QAAQ,EAChD,IAAMA,EAAK,SAASI,CAAK,EACzBJ,EAAK,MACP,CACF,CACF,CACF,CAEA,IAAMO,EAAUX,EAAK,cAA2B,4BAA4B,EACxEW,IACFA,EAAQ,MAAM,QAAUC,GAAWX,CAAK,GAAKC,EAAS,OAAS,IAMjE,IAAMW,EAASb,EAAK,cAAiC,mBAAmB,EACxE,GAAIa,EAAQ,CACVA,EAAO,SAAWD,GAAWX,CAAK,EAAIC,EAItC,IAAMY,EAAWD,EAAO,cAA2B,kBAAkB,EACjEC,IACFA,EAAS,YACPD,EAAO,aAAa,eAAe,GAAKR,EAAM,WAAa,cAEjE,CACF,CAEO,SAASU,GACdf,EACAC,EACAC,EACAC,EACAa,EACAC,EACM,CACN,IAAMC,EAAiBlB,EAAK,cAC1B,wBACF,EAEA,GAAIY,GAAWX,CAAK,EAAIC,EAAQ,CAE1BgB,IAAgBA,EAAe,MAAM,QAAU,QACnD,MACF,CAEIA,IAAgBA,EAAe,MAAM,QAAU,IAEnD,IAAIC,EAAa,EACjB,QAASZ,EAAI,EAAGA,EAAIN,EAAM,OAAQM,IAChCY,GAAclB,EAAMM,CAAC,EAAE,OAASN,EAAMM,CAAC,EAAE,UAAY,GAGnDY,EAAa,GAAKhB,EAAE,cACtBc,EACEjB,EACAgB,EAAaG,EAAYhB,EAAE,aAAcA,EAAE,eAAiB,CAAC,EAC7DgB,CACF,CAEJ,CAQO,SAASC,GACdpB,EACAqB,EACApB,EACAC,EACAG,EACM,CACN,IAAMiB,EAAQ,KAAK,IAAIV,GAAWX,CAAK,EAAGC,CAAM,EAC1CqB,EAAY,KAAK,IAAI,EAAGrB,EAASoB,CAAK,EAEtCE,EAAuB,CAACxB,CAAI,EAC9BqB,GAASG,EAAM,KAAKH,CAAO,EAE/B,QAAWI,KAAQD,EAAO,CACxB,IAAME,EAAWD,EAAK,iBACpB,yBACF,EACA,QAASE,EAAI,EAAGA,EAAID,EAAS,OAAQC,IACnCD,EAASC,CAAC,EAAE,UAAU,OACpB,yCACAA,EAAIL,CACN,EAGF,IAAMM,EAAgBH,EAAK,cACzB,uBACF,EACIG,IACFA,EAAc,YAAcC,GAAiBP,EAAOpB,EAAQG,CAAK,GAGnE,IAAMyB,EAAoBL,EAAK,cAC7B,2BACF,EACIK,IACFA,EAAkB,YAAcC,GAAeR,EAAWlB,CAAK,GAGjE,IAAM2B,EAAUP,EAAK,cACnB,kCACF,EACIO,GAASA,EAAQ,aAAa,gBAAiB,OAAOV,CAAK,CAAC,CAClE,CACF,CC7IO,SAASW,GAAcC,EAA0B,CACtD,MAAO,CAACC,EAAwBC,EAAmBC,IAA+B,CAChF,IAAMC,EAASH,EAAU,cAA2B,mBAAmB,EACjEI,EAAYJ,EAAU,cAA2B,sBAAsB,EACvEK,EAAaL,EAAU,cAA2B,oBAAoB,EACtEM,EAAkBN,EAAU,cAA2B,uBAAuB,EAC9EO,EAAmBP,EAAU,cAA2B,wBAAwB,EAChFQ,EAAUN,EAAeD,EAW/B,GATIE,IAAQA,EAAO,YAAcJ,EAAYE,CAAS,GAClDG,IACEI,EAAU,GACZJ,EAAU,YAAcL,EAAYG,CAAY,EAChDE,EAAU,MAAM,QAAU,IAE1BA,EAAU,MAAM,QAAU,QAG1BC,EACF,GAAIG,EAAU,EAAG,CAEf,GADIF,IAAiBA,EAAgB,YAAcP,EAAYS,CAAO,GAClED,EAAkB,CACpB,IAAME,EAAMP,EAAe,EAAI,KAAK,MAAOM,EAAU,IAAON,CAAY,EAAI,EAC5EK,EAAiB,YAAc,IAAME,EAAM,IAC7C,CACAJ,EAAW,MAAM,QAAU,EAC7B,MACEA,EAAW,MAAM,QAAU,MAGjC,CACF,CCTO,SAASK,GACdC,EACAC,EACAC,EACAC,EACgB,CAChB,IAAMC,EAAkBC,GACtBA,EAAE,SAAS,KAAMC,GAAMA,EAAE,SAAS,GAAK,KAEnCC,EAAO,CACXF,EACAC,EACAE,KACkB,CAClB,UAAWH,EAAE,GACb,UAAWC,EAAE,GACb,MAAOD,EAAE,MACT,IAAKA,EAAE,KAAO,KACd,aAAcC,EAAE,MAChB,cAAeD,EAAE,eAAiB,KAClC,MAAOC,EAAE,MACT,eAAgBA,EAAE,gBAAkB,KACpC,UAAWA,EAAE,WAAa,KAC1B,SAAAE,CACF,GAEMC,EAA2B,CAAC,EAClC,QAAWJ,KAAKL,EAAkB,CAChC,GAAI,CAACK,EAAE,WAAa,CAACA,EAAE,SAAU,SAKjC,IAAIK,EAAgB,GACpB,QAAWJ,KAAKD,EAAE,SAAU,CAC1B,IAAMM,EAAQC,GAAeN,EAAE,GAAIL,CAAQ,EACvC,CAACU,GAAO,UAAY,CAACL,EAAE,YAC3BG,EAAS,KAAKF,EAAKF,EAAGC,EAAG,KAAK,IAAI,EAAGK,EAAM,GAAG,CAAC,CAAC,EAChDD,EAAgB,GAClB,CACA,GAAIA,EAAe,SAEnB,IAAMG,EAAOC,GAAQT,EAAE,GAAIJ,CAAQ,EACnC,GAAI,CAACY,EAAK,SAAU,SACpB,IAAMP,EAAIF,EAAeC,CAAC,EACrBC,GACLG,EAAS,KAAKF,EAAKF,EAAGC,EAAG,KAAK,IAAI,EAAGO,EAAK,GAAG,CAAC,CAAC,CACjD,CACA,GAAIJ,EAAS,QAAU,CAACN,GAAU,QAAS,OAAOM,EAElD,IAAMM,EAAaV,GACjBA,EAAE,WAAaS,GAAQT,EAAE,GAAIJ,CAAQ,EAAE,KAAOC,EAE1Cc,GACHb,EAAS,kBAAoB,KAC1BH,EAAiB,KACdK,GAAMA,EAAE,KAAOF,EAAS,kBAAoBY,EAAUV,CAAC,CAC1D,EACA,OAASL,EAAiB,KAAKe,CAAS,EAC9C,GAAI,CAACC,EAAM,MAAO,CAAC,EAEnB,IAAMV,EAAIF,EAAeY,CAAI,EAC7B,OAAOV,EAAI,CAACC,EAAKS,EAAMV,EAAGQ,GAAQE,EAAK,GAAIf,CAAQ,EAAE,GAAG,CAAC,EAAI,CAAC,CAChE,CCnFA,SAASgB,GAAKC,EAA4D,CACxE,IAAMC,EAAK,6BACLC,EAAM,SAAS,gBAAgBD,EAAI,KAAK,EAC9CC,EAAI,aAAa,QAAS,IAAI,EAC9BA,EAAI,aAAa,SAAU,IAAI,EAC/BA,EAAI,aAAa,UAAW,WAAW,EACvCA,EAAI,aAAa,OAAQ,MAAM,EAC/B,OAAW,CAACC,EAAIC,EAAIC,EAAIC,CAAE,IAAKN,EAAO,CACpC,IAAMO,EAAO,SAAS,gBAAgBN,EAAI,MAAM,EAChDM,EAAK,aAAa,KAAMJ,CAAE,EAC1BI,EAAK,aAAa,KAAMH,CAAE,EAC1BG,EAAK,aAAa,KAAMF,CAAE,EAC1BE,EAAK,aAAa,KAAMD,CAAE,EAC1BC,EAAK,aAAa,SAAU,cAAc,EAC1CA,EAAK,aAAa,eAAgB,GAAG,EACrCA,EAAK,aAAa,iBAAkB,OAAO,EAC3CL,EAAI,YAAYK,CAAI,CACtB,CACA,OAAOL,CACT,CAEA,SAASM,EAAGC,EAAaC,EAAmBC,EAAgC,CAAC,EAAG,CAC9E,IAAMC,EAAO,SAAS,cAAcH,CAAG,EACnCC,IAAWE,EAAK,UAAYF,GAChC,QAAWG,KAAKF,EAAOC,EAAK,aAAaC,EAAGF,EAAME,CAAC,CAAC,EACpD,OAAOD,CACT,CAkBO,SAASE,GAAiBC,EAAuC,CACtE,IAAMC,EAAU,kBAAoBD,EAAK,MAEnCE,EAAUT,EAAG,MAAO,8BAA+B,CACvD,qBAAsB,GACtB,kBAAmBO,EAAK,UACxB,MAAO,gBACT,CAAC,EAEKG,EAASV,EACb,MACA,uBACGO,EAAK,gBAAkB,GAAK,wCAC/B,CACE,KAAM,SACN,aAAc,OACd,kBAAmBC,EACnB,SAAU,IACZ,CACF,EAIMG,EAASX,EAAG,MAAO,4BAA4B,EAC/CY,EAAMZ,EAAG,MAAO,gCAAgC,EAEhDa,EAAUb,EAAG,MAAO,6BAA6B,EACjDc,EAAQd,EAAG,KAAM,4BAA6B,CAAE,GAAIQ,CAAQ,CAAC,EAG/DD,EAAK,QAAQO,EAAM,aAAa,WAAY,IAAI,EACpDA,EAAM,YAAc,qBACpBD,EAAQ,YAAYC,CAAK,EACzBD,EAAQ,YACNb,EAAG,IAAK,+BAAgC,CAAE,sBAAuB,EAAG,CAAC,CACvE,EACAY,EAAI,YAAYC,CAAO,EAEvB,IAAME,EAAQf,EAAG,SAAU,4BAA6B,CACtD,KAAM,SACN,mBAAoB,GACpB,aAAc,OAChB,CAAC,EACDe,EAAM,YACJxB,GAAK,CACH,CAAC,IAAK,IAAK,KAAM,IAAI,EACrB,CAAC,KAAM,IAAK,IAAK,IAAI,CACvB,CAAC,CACH,EACAqB,EAAI,YAAYG,CAAK,EACrBJ,EAAO,YAAYC,CAAG,EAEtB,IAAMI,EAAWhB,EACf,MACA,sDACA,CAAE,gBAAiB,EAAG,CACxB,EACMiB,EAAWjB,EAAG,MAAO,kCAAmC,CAC5D,KAAM,cACN,gBAAiB,IACjB,gBAAiB,IACjB,gBAAiB,OAAOO,EAAK,cAAgB,CAAC,CAChD,CAAC,EACD,GAAIA,EAAK,OACPU,EAAS,aAAa,2BAA4B,EAAE,MAEpD,SAASC,EAAI,EAAGA,GAAKX,EAAK,cAAgB,GAAIW,IAC5CD,EAAS,YACPjB,EAAG,OAAQ,iCAAkC,CAC3C,wBAAyB,EAC3B,CAAC,CACH,EASJ,GANAgB,EAAS,YAAYC,CAAQ,EAC7BN,EAAO,YAAYK,CAAQ,EAC3BN,EAAO,YAAYC,CAAM,EAIrBJ,EAAK,WAAY,CACnB,IAAMY,EAASnB,EAAG,MAAO,4BAA4B,EAC/CoB,EAAQpB,EAAG,QAAS,mCAAoC,CAC5D,KAAM,OACN,oBAAqB,GACrB,KAAM,YACN,aAAc,kBACd,YAAa,kBACb,aAAc,KAChB,CAAC,EACDmB,EAAO,YAAYC,CAAK,EAExB,IAAMC,EAAQrB,EAAG,SAAU,mCAAoC,CAC7D,KAAM,SACN,0BAA2B,GAC3B,aAAc,eACd,MAAO,gBACT,CAAC,EACDqB,EAAM,YACJ9B,GAAK,CACH,CAAC,IAAK,IAAK,KAAM,IAAI,EACrB,CAAC,KAAM,IAAK,IAAK,IAAI,CACvB,CAAC,CACH,EACA4B,EAAO,YAAYE,CAAK,EACxBX,EAAO,YAAYS,CAAM,CAC3B,CAEIZ,EAAK,iBACPG,EAAO,YACLV,EAAG,MAAO,wBAAyB,CACjC,qBAAsB,GACtB,KAAM,QACN,aAAc,gBAChB,CAAC,CACH,EAKFU,EAAO,YACLV,EAAG,MAAO,2BAA4B,CAAE,kBAAmB,EAAG,CAAC,CACjE,EAEA,IAAMsB,EAAQtB,EAAG,MAAO,4BAA6B,CACnD,mBAAoB,GACpB,MAAO,gBACT,CAAC,EACKuB,EAAY,SAAS,cAAc,GAAG,EAC5CA,EAAU,YAAc,gCACxBD,EAAM,YAAYC,CAAS,EAC3Bb,EAAO,YAAYY,CAAK,EAExBZ,EAAO,YACLV,EAAG,OAAQ,qBAAsB,CAC/B,kBAAmB,GACnB,YAAa,QACf,CAAC,CACH,EAIA,IAAMwB,EAASxB,EACb,MACAO,EAAK,OACD,yDACA,4BACN,EAEA,GAAIA,EAAK,OAAQ,CACf,IAAMkB,EAAOzB,EAAG,SAAU,4BAA6B,CACrD,KAAM,SACN,kBAAmB,GACnB,MAAO,gBACT,CAAC,EACDyB,EAAK,YAAc,OACnBD,EAAO,YAAYC,CAAI,CACzB,CASA,GAPAD,EAAO,YACLxB,EAAG,OAAQ,mCAAoC,CAC7C,0BAA2B,GAC3B,YAAa,QACf,CAAC,CACH,EAEIO,EAAK,OAAQ,CACf,IAAMmB,EAAO1B,EAAG,SAAU,2BAA4B,CACpD,KAAM,SACN,kBAAmB,GACnB,MAAO,gBACT,CAAC,EACD0B,EAAK,YAAc,OACnBF,EAAO,YAAYE,CAAI,CACzB,CAEA,IAAMC,EAAO3B,EAAG,SAAU,2BAA4B,CACpD,KAAM,SACN,kBAAmB,GACnB,GAAIO,EAAK,OAAS,CAAE,MAAO,gBAAiB,EAAI,CAAC,CACnD,CAAC,EACD,OAAAoB,EAAK,YAAc,OACnBH,EAAO,YAAYG,CAAI,EAEvBjB,EAAO,YAAYc,CAAM,EACzBf,EAAQ,YAAYC,CAAM,EAEnBD,CACT,CCtMO,SAASmB,GACdC,EACAC,EACAC,EACAC,EACAC,EACM,CACN,IAAMC,EAAKJ,EAAO,aACZK,EAAIC,GACJC,EAAcP,EAAO,aAAe,EACpCQ,EAAWC,GAAcT,EAAO,aAAcA,EAAO,YAAY,EAOjEU,EADUC,GAAcX,CAAM,EACgB,IAAI,CAACY,EAAGC,KAAO,CACjE,GAAI,OAAOD,EAAE,SAAS,EACtB,MAAOA,EAAE,OAAS,GAClB,IAAKA,EAAE,KAAO,KACd,KAAMZ,EAAO,SAASa,CAAC,GAAG,aAAe,GACzC,cAAeD,EAAE,eAAiB,KAClC,UAAWA,EAAE,SAAS,KAAME,GAAMA,EAAE,SAAS,EAC7C,YAAaF,EAAE,YACf,SAAUA,EAAE,SAAS,IAAKE,IAAO,CAC/B,GAAI,OAAOA,EAAE,EAAE,EACf,MAAOA,EAAE,MACT,QAASA,EAAE,QACX,UAAWA,EAAE,UACb,MAAOA,EAAE,MACT,eAAgBA,EAAE,eAClB,UAAWA,EAAE,UACb,MAAOA,EAAE,MACT,kBAAmBA,EAAE,mBAAqB,IAC5C,EAAE,CACJ,EAAE,EAMF,GALI,CAACJ,EAAiB,QAIJA,EAAiB,OAAQE,GAAMA,EAAE,SAAS,EAAE,SAC5C,EAAG,OAErB,IAAMG,EAAcC,GAClBhB,EAAO,SAAS,CAAC,GAAG,WAAW,gBAAgB,cAAgB,KACjE,EAEMiB,EAAQC,GAAiB,eAAgB,CAC7C,MAAOlB,EAAO,MACd,SAAUA,EAAO,YACjB,eAAgBI,EAAG,YAAY,UAAY,GAC3C,iBAAkBA,EAAG,SAAS,qBAAuB,GACrD,QAASA,EAAG,KAAK,SAAWC,EAAE,WAAa,cAC3C,OAAQD,EAAG,WAAW,gBAAkB,GAAQ,KAAOJ,EAAO,MAChE,CAAC,EACDiB,EAAM,KAAK,aAAa,yBAA0B,OAAOV,CAAW,CAAC,EAIrEU,EAAM,SAAS,OAAOE,GAAiBZ,CAAW,CAAC,EAEnD,IAAMa,EAAQ,SAAS,cAAc,KAAK,EAC1CA,EAAM,UAAY,mCAClBA,EAAM,aAAa,uBAAwB,EAAE,EAC7CH,EAAM,SAAS,YAAYG,CAAK,EAEhC,IAAMC,EAAa,SAAS,cAAc,QAAQ,EAClDA,EAAW,KAAO,SAClBA,EAAW,UAAY,4BACvBA,EAAW,aAAa,2BAA4B,EAAE,EACtDA,EAAW,aAAchB,EAAE,QAAU,iBACrCe,EAAM,MAAMC,CAAU,EAItB,IAAMC,EAAUC,GAAiB,CAC/B,UAAWvB,EAAO,GAClB,MAAOA,EAAO,GAAG,QAAQ,MAAO,EAAE,EAClC,WAAYI,EAAG,aAAe,GAC9B,gBAAiBA,EAAG,0BAA4B,GAChD,aAAcG,EACd,OAAQ,EACV,CAAC,EACDU,EAAM,KAAK,YAAYK,CAAO,EAO9B,IAAME,EAAarB,EACfH,EAAO,SAAS,UAAWY,GAAMA,EAAE,SAAWT,CAAoB,EAClE,GACEsB,EAAgCC,GACpChB,EACAF,EACAD,EACA,CACE,QAAS,CAAC,CAACJ,EACX,iBACEqB,IAAe,GAAKd,EAAiBc,CAAU,GAAG,IAAM,KAAO,IACnE,CACF,EAEMG,EAA2B,CAC/B,eAAgB,IAAMC,GAAeH,EAAelB,CAAW,EAC/D,OAASO,GAAMe,GAAuBJ,EAAeX,CAAC,EACtD,sBAAwBA,GAAMgB,GAA0BL,EAAeX,CAAC,EACxE,sBAAuB,CAACF,EAAGE,IAAMiB,GAAyBN,EAAeb,EAAGE,CAAC,EAC7E,UAAW,CAACF,EAAGE,IAAMkB,GAAaP,EAAeb,EAAGE,EAAGN,CAAQ,EAC/D,WAAaM,GAAMmB,GAAuBR,EAAeX,CAAC,IAAM,GAChE,UAAYA,GAAM,CAChB,IAAMD,EAAIoB,GAAuBR,EAAeX,CAAC,EACjD,OAAOD,IAAM,IAAMqB,GAAcT,EAAeA,EAAcZ,CAAC,EAAGL,CAAQ,CAC5E,EACA,OAAQ,IAAM2B,GAAWV,EAAelB,CAAW,EACnD,UAAW,CAACO,EAAGsB,IAAQ,CACrB,IAAMvB,EAAIoB,GAAuBR,EAAeX,CAAC,EACjD,OAAID,IAAM,IAAMY,EAAcZ,CAAC,EAAE,WAAauB,EAAY,IAC1DX,EAAcZ,CAAC,EAAE,SAAWuB,EACrB,GACT,CACF,EAEA,SAASC,GAAkB,CACzBC,GAAarB,EAAM,KAAMQ,EAAelB,EAAaP,EAAiB,CACpE,YAAAe,EACA,SAAAP,EACA,SAAWK,GAAM,CACVqB,GAAcT,EAAeA,EAAcZ,CAAC,EAAGL,CAAQ,IAC5DiB,EAAc,OAAOZ,EAAG,CAAC,EACzBwB,EAAU,EACNE,EAAM,OAAO,GAAGA,EAAM,QAAQ,EACpC,EACA,OAAQ,IAAMA,EAAM,KAAK,CAC3B,CAAC,EACDC,GACEvB,EAAM,KACNQ,EACAlB,EAIA,CACE,aAAcP,EAAO,eAAe,aACpC,cAAeA,EAAO,eAAe,aACvC,EACA,CAACyC,EAAOC,EAAMC,IACZC,GAAkBH,EAAOC,EAAuCC,CAAK,EACvEH,GAAazB,CAAW,CAC1B,EACA8B,GAAe5B,EAAM,KAAMsB,EAAM,QAASd,EAAelB,EAAaF,CAAC,CACzE,CAEA,IAAMkC,EAAQO,GAAkB,CAC9B,UAAW7B,EAAM,KACjB,KAAM,CAAE,aAAcZ,EAAG,aAAcL,EAAO,eAAe,aAAc,cAAeA,EAAO,eAAe,aAAc,EAC9H,EAAAK,EACA,SAAAG,EACA,iBAAAE,EACA,YAAAH,EACA,gBAAiBH,EAAG,+BAAiC,GACrD,MAAOqB,EACP,YAAAV,EACA,YAAAY,EACA,SAAU,OAEV,aAAc,KACd,cAAeN,EACf,mBAAoBgB,EACpB,aAAeU,GAAS,CACtB,GAAIC,GAAWvB,CAAa,GAAKsB,EAAK,UAAY,GAAKxC,EAAa,OAGpE,IAAM0C,EAAOC,GAAeH,EAAK,UAAWvC,CAAQ,GAAG,IACnD2C,EACFC,GAAQL,EAAK,UAAWvC,CAAQ,EAAE,IAClCuB,GAAyBN,EAAesB,EAAK,UAAWA,EAAK,SAAS,EACpE,OAAOE,GAAS,WAAUE,EAAW,KAAK,IAAIA,EAAUF,CAAI,GAC3D,GAAAF,EAAK,UAAY,GAAKI,KAC3B1B,EAAc,KAAKsB,CAAI,EACvBV,EAAU,EACZ,EACA,cAAgBvB,GAAM,CACpB,IAAMD,EAAIoB,GAAuBR,EAAeX,CAAC,EAC7CD,IAAM,IAAM,CAACqB,GAAcT,EAAeA,EAAcZ,CAAC,EAAGL,CAAQ,IACxEiB,EAAc,OAAOZ,EAAG,CAAC,EACzBwB,EAAU,EACZ,EACA,YAAa,CAACU,EAAMX,IAAQ,CAC1B,IAAIiB,EAAQ,EACZ,QAASxC,EAAIY,EAAc,OAAS,EAAGZ,GAAK,GAAKwC,EAAQjB,EAAKvB,IAAK,CACjE,IAAMyC,EAAK7B,EAAcZ,CAAC,EAC1B,GAAIyC,EAAG,YAAcP,EAAK,WAAaO,EAAG,YAAcP,EAAK,UAAW,SACxE,IAAMQ,EAAO,KAAK,IAAID,EAAG,UAAY,EAAGlB,EAAMiB,CAAK,EAC/CE,IAASD,EAAG,UAAY,GAAI7B,EAAc,OAAOZ,EAAG,CAAC,EACpDyC,EAAG,UAAYA,EAAG,UAAY,GAAKC,EACxCF,GAASE,CACX,CACIF,EAAQ,IACVN,EAAK,SAAWM,EAChB5B,EAAc,KAAKsB,CAAI,EACvBV,EAAU,EAEd,EACA,aAAc,CAACzB,EAAGE,IAAMkB,GAAaP,EAAeb,EAAGE,EAAGN,CAAQ,EAClE,WAAaM,GAAMmB,GAAuBR,EAAeX,CAAC,IAAM,GAChE,WAAY,IAAMqB,GAAWV,EAAelB,CAAW,CACzD,CAAC,EAEDc,EAAW,iBAAiB,QAAS,IAAM,CACpCkB,EAAM,OAAO,GAAGA,EAAM,KAAK,CAClC,CAAC,EAEDtB,EAAM,IAAI,iBAAiB,QAAS,IAAM,CACpC+B,GAAWvB,CAAa,EAAIlB,GAChCN,EACEwB,EAAc,IAAKsB,IAAU,CAC3B,cAAe,gCAAkCA,EAAK,UACtD,SAAUA,EAAK,UAAY,EAC3B,WAAYS,GAAqBxD,EAAO,GAAIA,EAAO,UAAU,CAC/D,EAAE,CACJ,CACF,CAAC,EAEDD,EAAU,YAAYkB,EAAM,IAAI,EAChCwC,GAAcxC,EAAM,IAAI,EACxBoB,EAAU,EAEVnC,IAAYwD,GAAczC,EAAM,IAAI,CAAC,CACvC,CCrQA,IAAM0C,GAAiB,uBAEhB,SAASC,GAAQC,EAAyB,CAC/C,OAAO,SAASA,EAAG,aAAa,eAAe,GAAK,GAAI,EAAE,GAAK,CACjE,CAEO,SAASC,GAAgBD,EAA0B,CACxD,OAAOA,EAAG,aAAa,eAAe,IAAM,MAC9C,CAGO,SAASE,GACdC,EACAC,EACM,CACN,QAASC,EAAI,EAAGA,EAAIF,EAAQ,OAAQE,IAAK,CACvC,IAAML,EAAKG,EAAQE,CAAC,EACAD,IAAQ,MAAQL,GAAQC,CAAE,EAAII,GAEhDJ,EAAG,UAAU,IAAIF,EAAc,EAC/BE,EAAG,aAAa,gBAAiB,MAAM,IAEvCA,EAAG,UAAU,OAAOF,EAAc,EAClCE,EAAG,gBAAgB,eAAe,EAEtC,CACF,CAOO,SAASM,GACdH,EACAI,EACQ,CACR,IAAMC,EAAIL,EAAQI,CAAS,EAC3B,GAAIC,GAAK,CAACP,GAAgBO,CAAC,EAAG,OAAOD,EACrC,QAASF,EAAI,EAAGA,EAAIF,EAAQ,OAAQE,IAClC,GAAI,CAACJ,GAAgBE,EAAQE,CAAC,CAAC,EAAG,OAAOA,EAE3C,MAAO,EACT,CAGO,SAASI,GACdN,EACAO,EACAC,EACQ,CACR,IAAIN,EAAIK,EAAOC,EACf,KAAON,GAAK,GAAKA,EAAIF,EAAQ,QAAQ,CACnC,GAAI,CAACF,GAAgBE,EAAQE,CAAC,CAAC,EAAG,OAAOA,EACzCA,GAAKM,CACP,CACA,OAAOD,CACT,CCnCO,SAASE,GACdC,EACAC,EACAC,EACQ,CACR,GAAID,IAAiB,eAAgB,CACnC,IAAME,EAAM,SAASD,EAAO,aAAa,eAAe,GAAK,GAAI,EAAE,GAAK,EACxE,OAAO,KAAK,IAAI,EAAGF,EAAYG,CAAG,CACpC,CACA,IAAMC,EAAM,SAASF,EAAO,aAAa,eAAe,GAAK,GAAI,EAAE,GAAK,EACxE,OAAO,KAAK,IAAI,EAAGF,EAAY,KAAK,MAAOA,EAAYI,EAAO,GAAG,CAAC,CACpE,CAWO,SAASC,GACdL,EACAC,EACAC,EACY,CACZ,IAAMI,EAAM,SAASJ,EAAO,aAAa,eAAe,GAAK,GAAI,EAAE,EAC7DK,EAAYR,GAAcC,EAAWC,EAAcC,CAAM,EACzDM,EAAaD,EAAYD,EACzBG,EAAoBT,EAAYM,EACtC,MAAO,CACL,IAAAA,EACA,UAAAC,EACA,WAAAC,EACA,kBAAAC,EACA,aAAcA,EAAoBD,CACpC,CACF,CASO,SAASE,GACdJ,EACAK,EACe,CACf,GAAI,CAACA,EAAa,UAAW,OAAO,KACpC,IAAMC,EAAO,IAAI,KAAK,YAAYD,EAAa,QAAU,IAAI,EAAE,OAAOL,CAAG,EAGzE,MAAO,MADLK,EAAa,UAAUC,CAAI,GAAKD,EAAa,UAAU,OAAS,aAChD,MAAM,WAAW,EAAE,KAAK,OAAOL,CAAG,CAAC,EAAI,GAC3D,CAGO,SAASO,GACdC,EACAd,EACAC,EACAc,EACM,CACN,QAASC,EAAI,EAAGA,EAAIF,EAAS,OAAQE,IAAK,CACxC,IAAMd,EAASY,EAASE,CAAC,EACnBC,EAAcf,EAAO,cACzB,wBACF,EACIe,IACFA,EAAY,YAAcF,EACxBhB,GAAcC,EAAWC,EAAcC,CAAM,CAC/C,GAEF,IAAMgB,EAAYhB,EAAO,cACvB,0BACF,EACIgB,IAAWA,EAAU,YAAcH,EAAYf,CAAS,EAC9D,CACF,CAGO,SAASmB,GACdC,EACAN,EACAd,EACAC,EACAoB,EACAV,EACAI,EACM,CACN,QAASC,EAAI,EAAGA,EAAIF,EAAS,OAAQE,IACnCF,EAASE,CAAC,EAAE,aAAa,eAAgBA,IAAMK,EAAQ,OAAS,OAAO,EAGvEP,EAASE,CAAC,EAAE,aAAa,WAAYA,IAAMK,EAAQ,IAAM,IAAI,EAG/D,GAAM,CAAE,IAAAf,EAAK,WAAAE,EAAY,kBAAAC,EAAmB,aAAAa,CAAa,EACvDjB,GAAkBL,EAAWC,EAAca,EAASO,CAAK,CAAC,EAEtDE,EAAcH,EAAU,cAA2B,mBAAmB,EAC5E,GAAIG,EAAa,CACf,IAAMC,EAAQd,GAAgBJ,EAAKK,CAAY,EAC3Ca,IAAU,OAAMD,EAAY,YAAcC,EAChD,CAEA,IAAMC,EAAeL,EAAU,cAA2B,oBAAoB,EAC1EK,IAAcA,EAAa,YAAcV,EAAYP,CAAU,GAEnE,IAAMU,EAAYE,EAAU,cAA2B,sBAAsB,EACzEF,IACEI,EAAe,GACjBJ,EAAU,YAAcH,EAAYN,CAAiB,EACrDS,EAAU,MAAM,QAAU,IAE1BA,EAAU,MAAM,QAAU,QAI9B,IAAMQ,EAAaN,EAAU,cAA2B,oBAAoB,EACtEO,EACJP,EAAU,cAA2B,uBAAuB,EACxDQ,EAAmBR,EAAU,cACjC,wBACF,EACA,GAAIM,EACF,GAAIJ,EAAe,EAAG,CAIpB,GAHIK,IACFA,EAAgB,YAAcZ,EAAYO,CAAY,GAEpDM,EAAkB,CACpB,IAAMC,EACJpB,EAAoB,EAChB,KAAK,MAAOa,EAAe,IAAOb,CAAiB,EACnD,EACNmB,EAAiB,YAAc,IAAMC,EAAa,IACpD,CACAH,EAAW,MAAM,QAAU,EAC7B,MACEA,EAAW,MAAM,QAAU,MAGjC,CCnJO,SAASI,GACdC,EACAC,EACAC,EACAC,EACM,CACN,IAAMC,EAAKH,EAAO,aAIZI,EAAUC,GAAsBL,EAAO,SAAUE,CAAc,EACrE,GAAI,CAACE,EAAS,OAKd,IAAME,EAAWF,EAAQ,SAAS,MAC5BG,EAAa,KAAK,IACtB,GAAGP,EAAO,YAAY,IAAKQ,GAASA,EAAK,WAAW,CACtD,EACMC,EACJH,EAAS,KAAMI,GAAMC,GAAqBD,EAAGH,CAAU,CAAC,GACxDD,EAAS,KAAMI,GAAMA,EAAE,gBAAgB,GACvCJ,EAAS,CAAC,EACZ,GAAI,CAACG,EAAgB,OAErB,IAAMG,EAAYC,GAAQJ,EAAe,MAAM,MAAM,EAC/CK,EAAed,EAAO,eAAe,aACrCe,EAAcC,GAAgBZ,EAAQ,WAAW,gBAAgB,YAAY,EAC7Ea,EAAIC,GAKJC,EAAUC,GAAmBpB,EAAO,YAAac,CAAY,EAC7DO,EAAaC,GACjBnB,EAAG,YACHH,EAAO,YAAY,OACnBmB,CACF,EACMI,EAAaC,GACjBrB,EAAG,cAAc,UACjBA,EAAG,cAAc,UAAY,GAC7BgB,CACF,EAEMM,EAAoBzB,EAAO,YAAY,IAAI,CAACQ,EAAMkB,IAAM,CAC5D,IAAMC,EAAUb,IAAiB,eAAiB,EAAI,KAAK,MAAMN,EAAK,YAAc,CAAC,EAC/EoB,EAAcd,IAAiB,eAAiB,KAAK,OAAON,EAAK,QAAU,GAAK,GAAG,EAAI,EACvFqB,EACJf,IAAiB,eACb,KAAK,IAAI,EAAGF,EAAYgB,CAAW,EACnC,KAAK,IAAI,EAAGhB,EAAY,KAAK,MAAOA,EAAYe,EAAW,GAAG,CAAC,EAC/DG,EACJlB,EAAY,EAAI,KAAK,OAAQA,EAAYiB,GAAa,IAAOjB,CAAS,EAAI,EAE5E,MAAO,CACL,SAAUJ,EAAK,YACf,QAAAmB,EACA,YAAAC,EACA,MAAOG,EAAKd,EAAE,QAAU,gBAAiB,CAAE,MAAOT,EAAK,WAAY,CAAC,EACpE,aAAcsB,EAAW,EAAI,QAAUA,EAAW,IAAM,KACxD,WAAYJ,IAAMH,EAAapB,EAAG,cAAc,MAAQ,eAAiB,KACzE,SAAUuB,IAAML,CAClB,CACF,CAAC,EACD,GAAI,CAACI,EAAM,OAAQ,OAEnB,IAAMO,EAAQC,GAAiB,YAAa,CAC1C,MAAOjC,EAAO,MACd,SAAUA,EAAO,YACjB,aAAc,QACd,cAAeG,EAAG,SAAS,gBAAkB,GAC7C,eAAgB,GAChB,eAAgBA,EAAG,YAAY,UAAY,GAC3C,iBAAkBA,EAAG,SAAS,qBAAuB,GACrD,QAASA,EAAG,KAAK,SAAWc,EAAE,WAAa,cAC3C,OAAQd,EAAG,WAAW,gBAAkB,GAAQ,KAAOH,EAAO,MAChE,CAAC,EAEKkC,EAAQC,GAAiBV,EAAO,kBAAmB,CACvD,YAAatB,EAAG,SAAS,mBAAqB,GAC9C,cAAeA,EAAG,SAAS,mBAAqB,GAChD,WAAYc,EAAE,MAAQ,QAAQ,KAAK,CACrC,CAAC,EACDe,EAAM,SAAS,YAAYE,CAAK,EAEhC,IAAME,EAAUF,EAAM,iBAA8B,mBAAmB,EAGvEG,GAAoBD,EAASxB,EAAWE,EAAcC,CAAW,EAIjEuB,GAAoBF,EAASG,GAAmB9B,CAAc,CAAC,EAC/D,IAAI+B,EAAgBC,GAA0BL,EAASf,CAAU,EAC3DqB,EAAkBF,IAAkB,GACtCE,IAAiBF,EAAgBnB,GAErC,IAAMsB,EAAQ,IACZC,GAAWZ,EAAM,KAAMI,EAASxB,EAAWE,EAAc0B,EAAevB,EAAGF,CAAW,EAyBxF,GAvBAmB,EAAM,iBAAiB,QAAUW,GAAM,CACrC,IAAMC,EAAMD,EAAE,OAAuB,QAAqB,mBAAmB,EACzE,CAACC,GAAMC,GAAgBD,CAAE,IAC7BN,EAAgB,SAASM,EAAG,aAAa,iBAAiB,GAAK,GAAI,EAAE,GAAK,EAC1EH,EAAM,EACR,CAAC,EAIDT,EAAM,iBAAiB,UAAYW,GAAM,CACnCA,EAAE,MAAQ,aAAeA,EAAE,MAAQ,cACrCA,EAAE,eAAe,EACjBL,EAAgBQ,GAAoBZ,EAASI,EAAe,CAAC,EAC7DG,EAAM,EACNP,EAAQI,CAAa,EAAE,MAAM,IACpBK,EAAE,MAAQ,WAAaA,EAAE,MAAQ,eAC1CA,EAAE,eAAe,EACjBL,EAAgBQ,GAAoBZ,EAASI,EAAe,EAAE,EAC9DG,EAAM,EACNP,EAAQI,CAAa,EAAE,MAAM,EAEjC,CAAC,EAEGE,EAAiB,CACnBV,EAAM,IAAI,SAAW,GACrB,IAAMiB,EAAQjB,EAAM,IAAI,cAAc,kBAAkB,EACpDiB,IAAOA,EAAM,YAAchC,EAAE,YAAc,eACjD,MACEe,EAAM,IAAI,iBAAiB,QAAS,IAAM,CACxC,IAAMkB,EAAM,SACVd,EAAQI,CAAa,EAAE,aAAa,eAAe,GAAK,GACxD,EACF,EACAvC,EAAY,CACV,CACE,cAAeQ,EAAe,GAC9B,SAAUyC,EACV,WAAYC,GAAqBnD,EAAO,GAAIA,EAAO,UAAU,CAC/D,CACF,CAAC,CACH,CAAC,EAGHD,EAAU,YAAYiC,EAAM,IAAI,EAChCoB,GAAcpB,EAAM,IAAI,EACxBW,EAAM,CACR,CC3KO,SAASU,GAAeC,EAAoBC,EAAyB,CAC1E,OAAOD,EAAa,KAAK,MAAOA,EAAaC,EAAW,GAAG,CAC7D,CAcO,SAASC,GACdC,EACAF,EACAG,EACAC,EACY,CACZ,IAAMC,EAAaC,GAAYJ,EAAM,CAAC,EAAGA,EAAM,CAAC,EAAE,iBAAiB,EAC7DK,EAAaD,GAAYJ,EAAM,CAAC,EAAGA,EAAM,CAAC,EAAE,iBAAiB,EAE7DM,EAAaH,EAAW,MAAQF,EAASI,EAAW,MAAQH,EAC5DK,EACJJ,EAAW,MAAQF,EACnBL,GAAeS,EAAW,MAAOP,CAAO,EAAII,EAE9C,MAAO,CAAE,WAAAI,EAAY,UAAAC,EAAW,QAASD,EAAaC,CAAU,CAClE,CASO,SAASC,GACdC,EACAC,EACAZ,EACAa,EACAC,EACM,CACN,IAAMC,EAAUJ,EAAI,cAA2B,sBAAsB,EAC/DK,EAAYL,EAAI,cACpB,8BACF,EACA,GAAKI,EAEL,IAAIF,GAASb,EAAU,EAAG,CACxBe,EAAQ,YAAcD,EAAYhB,GAAec,EAAQ,MAAOZ,CAAO,CAAC,EACpEgB,IACFA,EAAU,YAAcF,EAAYF,EAAQ,KAAK,EACjDI,EAAU,OAAS,IAErB,MACF,CAGA,GADAD,EAAQ,YAAcD,EAAYF,EAAQ,KAAK,EAC3CI,EAAW,CACb,IAAMC,EAAUL,EAAQ,eACpBK,GAAWA,EAAUL,EAAQ,OAC/BI,EAAU,YAAcF,EAAYG,CAAO,EAC3CD,EAAU,OAAS,KAEnBA,EAAU,YAAc,GACxBA,EAAU,OAAS,GAEvB,EACF,CAGO,SAASE,GACdC,EACAjB,EACAF,EACAG,EACAC,EACAU,EACM,CACN,GAAM,CAAE,WAAAN,EAAY,UAAAC,EAAW,QAAAW,CAAQ,EAAInB,GACzCC,EACAF,EACAG,EACAC,CACF,EAEMiB,EAASF,EAAU,cAA2B,mBAAmB,EACjEH,EAAYG,EAAU,cAA2B,sBAAsB,EACvEG,EAAaH,EAAU,cAA2B,oBAAoB,EACtEI,EACJJ,EAAU,cAA2B,uBAAuB,EACxDK,EAAmBL,EAAU,cACjC,wBACF,EAaA,GAXIE,IAAQA,EAAO,YAAcP,EAAYL,CAAS,GAElDO,IACEI,EAAU,GACZJ,EAAU,YAAcF,EAAYN,CAAU,EAC9CQ,EAAU,MAAM,QAAU,IAE1BA,EAAU,MAAM,QAAU,QAI1BM,EACF,GAAIF,EAAU,EAAG,CAEf,GADIG,IAAiBA,EAAgB,YAAcT,EAAYM,CAAO,GAClEI,EAAkB,CACpB,IAAMC,EACJjB,EAAa,EAAI,KAAK,MAAOY,EAAU,IAAOZ,CAAU,EAAI,EAC9DgB,EAAiB,YAAc,IAAMC,EAAM,IAC7C,CACAH,EAAW,MAAM,QAAU,EAC7B,MACEA,EAAW,MAAM,QAAU,MAGjC,CC/GO,SAASI,GACdC,EACAC,EACAC,EACAC,EACY,CACZ,IAAMC,EAAaC,GAAYL,EAAM,CAAC,EAAGA,EAAM,CAAC,EAAE,iBAAiB,EAC7DM,EAAaD,GAAYL,EAAM,CAAC,EAAGA,EAAM,CAAC,EAAE,iBAAiB,EAE7DO,EAAoB,CACxB,UAAW,OAAOP,EAAM,CAAC,EAAE,iBAAiB,EAC5C,SAAUE,EACV,YAAaE,EAAW,OAAS,GAAKF,CACxC,EACMM,EAAgBC,GAAeH,EAAW,OAAS,EAAGL,CAAO,EAAIE,EAEvE,OAAI,OAAOH,EAAM,CAAC,EAAE,iBAAiB,IAAM,OAAOA,EAAM,CAAC,EAAE,iBAAiB,EACnE,CACL,CACE,UAAWO,EAAQ,UACnB,SAAUL,EAASC,EACnB,YAAaI,EAAQ,YAAc,GAAKC,CAC1C,CACF,EAGK,CACLD,EACA,CACE,UAAW,OAAOP,EAAM,CAAC,EAAE,iBAAiB,EAC5C,SAAUG,EACV,WAAYK,EACZ,WAAY,CAAC,CAAE,IAAKE,GAAuB,MAAOC,EAAgB,CAAC,CACrE,CACF,CACF,CC1CA,SAASC,IAA6B,CACpC,IAAMC,EAAO,SAAS,cAAc,KAAK,EACzC,OAAAA,EAAK,UAAY,gBACjBA,EAAK,aAAa,cAAe,MAAM,EACvCA,EAAK,UACH,+TAIKA,CACT,CAaO,SAASC,GACdC,EACAC,EACAC,EACAC,EACM,CACN,IAAMC,EAAKH,EAAO,aACZI,EAAaJ,EAAO,SAAS,KAAMK,GAAMA,EAAE,KAAOL,EAAO,YAAY,EACrEM,EAAaN,EAAO,SAAS,KAAMK,GAAMA,EAAE,KAAOL,EAAO,YAAY,EAC3E,GAAI,CAACI,GAAc,CAACE,EAAY,OAEhC,IAAMC,EAASP,EAAO,YAChBQ,EAASR,EAAO,YAChBS,EAAUT,EAAO,eAAe,cAEhCU,EAAa,CACjB,kBAAmBV,EAAO,kBAC1B,kBAAmBA,EAAO,iBAC5B,EAKMW,EAAaX,EAAO,eAAe,OAASA,EAAO,cAAgB,KACnEY,EAAaZ,EAAO,eAAe,OAASA,EAAO,cAAgB,KACnEa,EAAoB,CACxB,CACE,GAAGC,GAAaV,EAAYJ,EAAO,GAAI,CACrC,WAAAU,EACA,kBAAmBC,CACrB,CAAC,EACD,KAAM,KACR,EACA,CACE,GAAGG,GAAaR,EAAYN,EAAO,GAAI,CACrC,WAAAU,EACA,kBAAmBE,CACrB,CAAC,EACD,KAAM,KACR,CACF,EAIMG,EAAWF,EAAM,OAAQG,GAAM,CAACA,EAAE,SAAS,KAAMC,GAAMA,EAAE,SAAS,CAAC,EAAE,OAErEC,EAAcC,GAClBf,EAAW,WAAW,gBAAgB,YACxC,EACMgB,EAAIC,GAEJC,EAAQC,GAAiB,UAAW,CACxC,MAAOvB,EAAO,MACd,SAAUA,EAAO,YACjB,eAAgBG,EAAG,YAAY,UAAY,GAC3C,iBAAkBA,EAAG,SAAS,qBAAuB,GACrD,QAASA,EAAG,KAAK,SAAWiB,EAAE,WAAa,cAC3C,OAAQjB,EAAG,WAAW,gBAAkB,GAAQ,KAAOH,EAAO,MAChE,CAAC,EACDsB,EAAM,KAAK,aAAa,sBAAuB,OAAOb,CAAO,CAAC,EAC9Da,EAAM,KAAK,aAAa,eAAgB,OAAOf,CAAM,CAAC,EACtDe,EAAM,KAAK,aAAa,eAAgB,OAAOd,CAAM,CAAC,EAEtD,IAAMgB,EAAYf,GAAW,IAAM,OAASA,EAAU,QAmCtD,GAjCAI,EAAM,QAAQ,CAACY,EAAMC,IAAM,CACzB,IAAMC,EAAQD,IAAM,EAGhBC,GAAOL,EAAM,SAAS,YAAY1B,GAAc,CAAC,EACrD,IAAMgC,EAAQ,CAACH,EAAK,SAAS,KAAMR,GAAMA,EAAE,SAAS,EAC9CY,EAAMC,GAAiBL,EAAML,EAAG,CACpC,MAAAQ,EACA,KAAMH,EAAK,IACX,SAAUA,EAAK,KACf,eAAgBE,EAAQnB,EAASD,EACjC,UAAWoB,GAAS,CAACC,EAAQJ,EAAY,KACzC,eAAgBrB,EAAG,aAAa,cAClC,CAAC,EAED,GADAmB,EAAM,SAAS,YAAYO,CAAG,EAC1BD,EAAO,OAEX,IAAMG,EAAUC,GAAYP,EAAMA,EAAK,iBAAiB,EACxDQ,GAAmBJ,EAAKJ,EAAMM,CAAO,EACrCG,GAAgBL,EAAKE,EAASN,EAAK,aAAa,EAChDU,GAAWN,EAAKE,EAAStB,EAASkB,EAAOT,CAAW,EAEpDkB,GACEP,EACA,IAAMJ,EACLY,GAAY,CACXH,GAAgBL,EAAKQ,EAASZ,EAAK,aAAa,EAChDU,GAAWN,EAAKQ,EAAS5B,EAASkB,EAAOT,CAAW,EACpDoB,GAAchB,EAAM,KAAMT,EAAOJ,EAASF,EAAQC,EAAQU,CAAW,CACvE,CACF,CACF,CAAC,EAEGH,EAAW,EAAG,CAChBO,EAAM,IAAI,SAAW,GACrB,IAAMiB,EAAQjB,EAAM,IAAI,cAAc,kBAAkB,EACpDiB,IACFA,EAAM,YAAcC,GAAsBpB,EAAGL,CAAQ,EAEzD,MACEO,EAAM,IAAI,iBAAiB,QAAS,IAAM,CACxCrB,EACEwC,GAAmB5B,EAAOJ,EAASF,EAAQC,CAAM,EAAE,IAAKkC,IAAU,CAChE,cAAe,gCAAkCA,EAAK,UACtD,SAAUA,EAAK,SAGf,WAAY,CACV,GAAGC,GAAqB3C,EAAO,GAAIA,EAAO,UAAU,EACpD,GAAI0C,EAAK,YAAc,CAAC,CAC1B,CACF,EAAE,CACJ,CACF,CAAC,EAGH3C,EAAU,YAAYuB,EAAM,IAAI,EAChCsB,GAActB,EAAM,IAAI,EACxBgB,GAAchB,EAAM,KAAMT,EAAOJ,EAASF,EAAQC,EAAQU,CAAW,EAErEhB,IAAY2C,GAAcvB,EAAM,IAAI,CAAC,CACvC,CChKO,SAASwB,GACdC,EACAC,EACAC,EACQ,CACR,OAAOC,EAAKD,EAAE,QAAU,6BAA8B,CACpD,KAAMF,EAAI,EACV,MAAOC,CACT,CAAC,CACH,CAEO,SAASG,GACdC,EACAJ,EACAC,EACQ,CACR,OAAOC,EAAKD,EAAE,YAAc,+BAAgC,CAC1D,MAAOG,EACP,MAAOJ,CACT,CAAC,CACH,CAQO,SAASK,GACdC,EACAN,EACAC,EACQ,CACR,OAAOC,EAAKD,EAAE,gBAAkB,yCAA0C,CACxE,MAAOK,EACP,MAAON,CACT,CAAC,CACH,CAEO,SAASO,GACdC,EACA,EACQ,CACR,OAAOA,EAAY,EACfN,EAAK,EAAE,UAAY,uBAAwB,CAAE,MAAOM,CAAU,CAAC,EAC/D,EAAE,UAAY,UACpB,CAGA,SAASC,GACPC,EACAN,EACAO,EACQ,CACR,IAAMC,EAAO,IAAI,KAAK,YAAYD,GAAU,IAAI,EAAE,OAAOP,CAAK,EAC9D,OAAOF,EAAKQ,EAAIE,CAAI,GAAKF,EAAI,OAAS,YAAa,CAAE,MAAON,CAAM,CAAC,CACrE,CAGO,SAASS,GACdC,EACA,EACQ,CACR,OAAOA,EACH,EAAE,cAAgB,oBAClB,EAAE,YAAc,sBACtB,CAMO,SAASC,GACdD,EACAN,EACAR,EACAC,EACQ,CACR,GAAI,CAACa,EAAU,CACb,IAAME,EAAQf,EAAE,eAChB,OAAI,OAAOe,GAAU,SAAiBd,EAAKc,EAAO,CAAE,MAAOhB,CAAM,CAAC,EAC3DS,GACLO,GAAS,CAAE,IAAK,uBAAwB,MAAO,uBAAwB,EACvEhB,EACAC,EAAE,MACJ,CACF,CACA,OAAOQ,GACLR,EAAE,eAAiB,CACjB,IAAK,uBACL,MAAO,uBACT,EACAO,EACAP,EAAE,MACJ,CACF,CC3EO,SAASgB,GAAYC,EAA4BC,EAAmB,CACzE,IAAIC,EAAM,EACV,QAASC,EAAI,EAAGA,EAAIH,EAAWC,CAAC,EAAE,OAAQE,IACxCD,GAAOF,EAAWC,CAAC,EAAEE,CAAC,EAAE,UAAY,EAEtC,OAAOD,CACT,CAEO,SAASE,GACdJ,EACAK,EACAJ,EACS,CACT,OAAOF,GAAYC,EAAYC,CAAC,IAAMI,EAAMJ,CAAC,EAAE,aAAe,EAChE,CAGO,SAASK,GACdN,EACAK,EACAJ,EACQ,CACR,IAAMM,EAAMF,EAAMJ,CAAC,EAAE,YACrB,OAAIM,GAAO,KAAa,IACjB,KAAK,IAAI,EAAGA,EAAMR,GAAYC,EAAYC,CAAC,CAAC,CACrD,CAEO,SAASO,GACdR,EACAK,EACS,CACT,QAASJ,EAAI,EAAGA,EAAII,EAAM,OAAQJ,IAChC,GAAI,CAACG,GAAcJ,EAAYK,EAAOJ,CAAC,EAAG,MAAO,GAEnD,MAAO,EACT,CAwDO,SAASQ,GACdC,EACAC,EACAC,EACS,CACT,GAAIC,GAAeF,EAAK,UAAWC,CAAQ,GAAG,SAAU,MAAO,GAE/D,IAAME,EAAOC,GAAQJ,EAAK,UAAWC,CAAQ,EAC7C,GAAI,CAACE,EAAK,SAAU,MAAO,GAE3B,IAAIE,EAAiB,EACrB,QAAWC,KAAQP,EACjB,QAAWQ,KAAQD,EACbC,IAASP,GAAQO,EAAK,YAAcP,EAAK,YAC3CK,GAAkBE,EAAK,UAAY,GAIzC,OAAOF,GAAkBF,EAAK,GAChC,CASO,SAASK,GACdT,EACAU,EACAC,EACAC,EACAV,EACQ,CACR,IAAME,EAAOC,GAAQM,EAAWT,CAAQ,EAExC,GADI,CAACE,EAAK,UAAYA,EAAK,MAAQA,EAAK,KACpCS,GAAqBb,EAAYU,EAAGE,CAAS,IAAM,GAAI,MAAO,GAElE,IAAIE,EAAM,EACV,QAAWC,KAAMf,EAAWU,CAAC,EACvBK,EAAG,YAAcJ,GAAaI,EAAG,YAAcH,IACjDE,GAAOC,EAAG,UAAY,GAG1B,OAAOD,CACT,CAEO,SAASD,GACdb,EACAU,EACAE,EACQ,CACR,QAASI,EAAI,EAAGA,EAAIhB,EAAWU,CAAC,EAAE,OAAQM,IACxC,GAAIhB,EAAWU,CAAC,EAAEM,CAAC,EAAE,YAAcJ,EAAW,OAAOI,EAEvD,MAAO,EACT,CCtIO,SAASC,GAAiBC,EAAsB,CACrD,GAAM,CAAE,UAAAC,EAAW,MAAAC,EAAO,WAAAC,EAAY,EAAAC,CAAE,EAAIJ,EAE5C,QAAS,EAAI,EAAG,EAAIE,EAAM,OAAQ,IAAK,CACrC,IAAMG,EAAMH,EAAM,CAAC,EAAE,aAAe,EAE9BI,EAAUL,EAAU,cACxB,qBAAuB,EAAI,IAC7B,EACIK,GACFA,EAAQ,aACN,iBACAH,EAAW,CAAC,EAAE,OAAS,EAAI,OAAS,KACtC,EAGF,IAAMI,EAAaC,GACjB,KAAK,IAAIC,GAAYN,EAAY,CAAC,EAAGE,CAAG,EACxCA,EACAD,CACF,EACMM,EAAMC,GAAcR,EAAYD,EAAO,CAAC,EACxCU,EAAWX,EAAU,iBACzB,qBAAuB,EAAI,IAC7B,EACA,QAASY,EAAI,EAAGA,EAAID,EAAS,OAAQC,IACnCD,EAASC,CAAC,EAAE,YAAcN,EAC1BK,EAASC,CAAC,EAAE,UAAU,OAAO,kCAAmCH,CAAG,EAGrE,IAAMI,EAAYb,EAAU,cAC1B,qBAAuB,EAAI,IAC7B,EACA,GAAKa,EAEL,CAAAA,EAAU,UAAY,GACtB,QAASC,EAAI,EAAGA,EAAIZ,EAAW,CAAC,EAAE,OAAQY,IAAK,CAC7C,IAAMC,EAAU,EACVC,EAAUF,EACVG,EAAOf,EAAWa,CAAO,EAAEC,CAAO,EACxCH,EAAU,YACRK,GACED,EACAd,EACAJ,EAAK,YACLoB,GAAcjB,EAAYe,EAAMlB,EAAK,QAAQ,EAC7C,IAAMA,EAAK,SAASgB,EAASC,CAAO,EACpC,IAAMjB,EAAK,OAAOgB,CAAO,CAC3B,CACF,CACF,EACF,CACF,CAYO,SAASK,GAAWrB,EAAsB,CAC/C,GAAM,CAAE,UAAAC,EAAW,MAAAC,EAAO,WAAAC,EAAY,EAAAC,CAAE,EAAIJ,EACtCsB,EAAOrB,EAAU,cAA2B,kBAAkB,EACpE,GAAI,CAACqB,EAAM,OAEX,IAAIC,EAAmB,GACnBC,EAAY,EACZC,EAAW,GACf,QAASC,EAAI,EAAGA,EAAIxB,EAAM,OAAQwB,IAC5BvB,EAAWuB,CAAC,EAAE,OAAS,IAAGD,EAAW,IACpCd,GAAcR,EAAYD,EAAOwB,CAAC,IACrCF,GAAa,EACTD,IAAqB,KAAIA,EAAmBG,IAIpD,GAAIH,IAAqB,GAAI,CAK3BD,EAAK,MAAM,QAAU,OACrBA,EAAK,UAAU,IAAI,6BAA6B,EAChD,IAAMK,EAASL,EAAK,cAChBK,GAAUA,EAAO,oBAAsBL,GACzCK,EAAO,aAAaL,EAAMK,EAAO,iBAAiB,EAEpD,MACF,CACAL,EAAK,MAAM,QAAU,GACrBA,EAAK,UAAU,OAAO,6BAA6B,EACnDA,EAAK,aAAa,iBAAkB,OAAOC,CAAgB,CAAC,EAE5D,IAAMK,EAAQC,GAAUJ,EAAUrB,CAAC,EAC7B0B,EAAUR,EAAK,cACnB,kCACF,EACIQ,IAASA,EAAQ,YAAcF,GACnC,IAAMG,EAAQT,EAAK,cAA2B,0BAA0B,EACpES,IACFA,EAAM,YAAcC,GAAaP,EAAUD,EAAWtB,EAAM,OAAQE,CAAC,GAEvE,IAAM6B,EAAW/B,EAAMqB,CAAgB,EAAE,MAAQ,GACjDD,EAAK,aAAa,aAAcW,EAAWL,EAAQ,KAAOK,EAAWL,CAAK,EAE1E,IAAMM,EAASjC,EAAU,cACvB,qBAAuBsB,EAAmB,IAC5C,EACIW,GAAUA,EAAO,eAAiBA,EAAO,yBAA2BZ,GACtEY,EAAO,cAAc,aAAaZ,EAAMY,CAAM,CAElD,CAEO,SAASC,GACdnC,EACAoC,EACAC,EACAC,EACM,CACN,GAAM,CAAE,UAAArC,EAAW,MAAAC,EAAO,WAAAC,CAAW,EAAIH,EACnCuC,EAAiBtC,EAAU,cAC/B,wBACF,EAEA,GAAI,CAACuC,GAAarC,EAAYD,CAAK,EAAG,CAChCqC,IAAgBA,EAAe,MAAM,QAAU,QACnD,MACF,CACIA,IAAgBA,EAAe,MAAM,QAAU,IAEnD,IAAIE,EAAa,EACjB,QAAWC,KAAQvC,EACjB,QAAWwC,KAAQD,EAAMD,GAAcE,EAAK,OAASA,EAAK,UAAY,GAGpEF,EAAa,GAAKL,EAAK,cACzBE,EACErC,EACAoC,EAAaI,EAAYL,EAAK,aAAcA,EAAK,eAAiB,CAAC,EACnEK,CACF,CAEJ,CAGO,SAASG,GAAe5C,EAAsB,CACnD,GAAM,CAAE,UAAAC,EAAW,MAAAC,EAAO,WAAAC,EAAY,EAAAC,CAAE,EAAIJ,EAExC6C,EAAY,EAChB,QAASnB,EAAI,EAAGA,EAAIxB,EAAM,OAAQwB,IAC5Bf,GAAcR,EAAYD,EAAOwB,CAAC,IAAGmB,GAAa,GAExD,IAAMrB,EAAYtB,EAAM,OAAS2C,EAE3BC,EAAW7C,EAAU,iBACzB,yCACF,EACA,QAAS8C,EAAI,EAAGA,EAAID,EAAS,OAAQC,IACnCD,EAASC,CAAC,EAAE,UAAU,OACpB,yCACAA,EAAIF,CACN,EAGF,IAAMG,EAAgB/C,EAAU,cAC9B,uBACF,EACI+C,IACFA,EAAc,YAAcC,GAAoBJ,EAAW3C,EAAM,OAAQE,CAAC,GAG5E,IAAM8C,EAAoBjD,EAAU,cAClC,2BACF,EACIiD,IACFA,EAAkB,YAAcC,GAAc3B,EAAWpB,CAAC,GAG5D,IAAMgD,EAAUnD,EAAU,cACxB,kCACF,EACImD,GAASA,EAAQ,aAAa,gBAAiB,OAAOP,CAAS,CAAC,CACtE,CAEO,SAASQ,GAAUrD,EAAsB,CAC9C,IAAMsD,EAAStD,EAAK,UAAU,cAC5B,mBACF,EACA,GAAI,CAACsD,EAAQ,OAEbA,EAAO,SAAW,CAACd,GAAaxC,EAAK,WAAYA,EAAK,KAAK,EAE3D,IAAMuD,EAAWD,EAAO,cAA2B,kBAAkB,EACjEC,IACFA,EAAS,YACPD,EAAO,aAAa,eAAe,GAAKtD,EAAK,EAAE,WAAa,cAElE,CCnOO,SAASwD,GAAeC,EAAwC,CACrE,IAAMC,EAAsC,OAAO,OAAO,IAAI,EACxDC,EAAkB,CAAC,EAEzB,QAAWC,KAAQH,EACjB,QAAWI,KAAQD,EAAM,CACvB,IAAME,EAAMD,EAAK,UAAY,EACvBE,EAAM,OAAOF,EAAK,SAAS,EAC7BH,EAAUK,CAAG,GACfL,EAAUK,CAAG,EAAE,UAAYD,EAC3BJ,EAAUK,CAAG,EAAE,YACZL,EAAUK,CAAG,EAAE,YAAc,IAAMF,EAAK,OAAS,GAAKC,IAEzDJ,EAAUK,CAAG,EAAI,CACf,UAAWF,EAAK,UAChB,SAAUC,EACV,YAAaD,EAAK,OAAS,GAAKC,CAClC,EACAH,EAAM,KAAKI,CAAG,EAElB,CAGF,OAAOJ,EAAM,IAAKI,GAAQL,EAAUK,CAAG,CAAC,CAC1C,CCbA,IAAMC,GAAY,UACZC,GAAsB,IACtBC,GAAqB,IAMrBC,GAAkB,IAOlBC,GAAuB,IAkCtB,SAASC,GAAaC,EAA0B,CACrD,GAAM,CAAE,UAAAC,EAAW,MAAAC,EAAO,WAAAC,EAAY,EAAAC,CAAE,EAAIJ,EAEtCK,EAAUJ,EAAU,cAA2B,sBAAsB,EAIvEI,GAAWL,EAAK,cAAcA,EAAK,aAAa,YAAYK,CAAO,EAEvE,IAAMC,EAA4BC,GAChCF,EAAUA,EAAQ,cAAiBE,CAAG,EAAI,KAEtCC,EAAcF,EAAe,iBAAiB,EAC9CG,EAAaH,EAAe,4BAA4B,EACxDI,EAAcJ,EAAoB,qBAAqB,EACvDK,EAAcL,EAAe,2BAA2B,EACxDM,EAAYN,EAAe,mBAAmB,EAC9CO,EAAaP,EAAe,oBAAoB,EAChDQ,EAAYR,EAAe,mBAAmB,EAC9CS,EAAWT,EAAe,oBAAoB,EAC9CU,EAAUV,EAAqB,mBAAmB,EAClDW,EAAUX,EAAqB,mBAAmB,EAClDY,EAAUZ,EAAqB,mBAAmB,EAClDa,EAAgBb,EAAe,2BAA2B,EAC1Dc,EAAad,EAAe,uBAAuB,EACnDe,EAAaf,EAAe,4BAA4B,EACxDgB,EAAmBhB,EAAe,sBAAsB,EAE1DiB,EAAa7B,GACb8B,EAAY,GAEZC,EAAY,EACZC,EAAoB,CAAC,EAErBC,EAAqD,KAErDC,EAAyD,KAEvDC,EAAeC,GACnB5B,EAAM4B,CAAC,EAAE,kBAAoB,CAAC,EAQ1BC,EAA2B,CAC/B,eAAgB,IAAMC,GAAa7B,EAAYD,EAAOuB,CAAS,EAC/D,OAASQ,GAAc,CACrB,IAAMC,EAAMC,GAAqBhC,EAAYsB,EAAWQ,CAAS,EACjE,OAAOC,IAAQ,GAAK,EAAI/B,EAAWsB,CAAS,EAAES,CAAG,EAAE,UAAY,CACjE,EAEA,sBAAwBD,GAAc,CACpC,IAAIG,EAAM,EACV,QAAWC,KAAQlC,EACjB,QAAWmC,KAAQD,EACbC,EAAK,YAAcL,IAAWG,GAAOE,EAAK,UAAY,GAG9D,OAAOF,CACT,EACA,sBAAuB,CAACG,EAAWN,IAAc,CAC/C,IAAIG,EAAM,EACV,QAASN,EAAI,EAAGA,EAAI3B,EAAW,OAAQ2B,IACrC,QAAWU,KAAMrC,EAAW2B,CAAC,EACvBU,EAAG,YAAcD,IACjBT,IAAML,GAAae,EAAG,YAAcP,IACxCG,GAAOI,EAAG,UAAY,IAG1B,OAAOJ,CACT,EACA,UAAW,CAACG,EAAWN,IACrBQ,GAAiBtC,EAAYsB,EAAWc,EAAWN,EAAWjC,EAAK,QAAQ,EAC7E,WAAaiC,GACXE,GAAqBhC,EAAYsB,EAAWQ,CAAS,IAAM,GAC7D,UAAYA,GAAc,CACxB,IAAMC,EAAMC,GAAqBhC,EAAYsB,EAAWQ,CAAS,EACjE,OAAOC,IAAQ,IAAMlC,EAAK,UAAUG,EAAWsB,CAAS,EAAES,CAAG,CAAC,CAChE,EACA,OAAQ,IAAMF,GAAa7B,EAAYD,EAAOuB,CAAS,IAAM,EAC7D,UAAW,CAACQ,EAAWG,IAAQ,CAC7B,IAAMF,EAAMC,GAAqBhC,EAAYsB,EAAWQ,CAAS,EACjE,OAAIC,IAAQ,IAAM/B,EAAWsB,CAAS,EAAES,CAAG,EAAE,WAAaE,EAAY,IACtEjC,EAAWsB,CAAS,EAAES,CAAG,EAAE,SAAWE,EAC/B,GACT,CACF,EAKA,SAASM,EAAkBZ,EAAiB,CAC1C,GAAI,CAACT,EAAY,OACjB,IAAMsB,EAAMzC,EAAM4B,CAAC,EAAE,aAAe,EACpCT,EAAW,UAAY,GACvBA,EAAW,aAAa,gBAAiB,OAAOsB,CAAG,CAAC,EACpD,QAASC,EAAI,EAAGA,EAAID,EAAKC,IAAK,CAC5B,IAAMC,EAAM,SAAS,cAAc,MAAM,EACzCA,EAAI,UAAY,iCAChBA,EAAI,aAAa,wBAAyB,EAAE,EAC5CxB,EAAW,YAAYwB,CAAG,CAC5B,CACAC,EAAkB,CACpB,CAEA,SAASA,GAA0B,CACjC,GAAI,CAACzB,EAAY,OACjB,IAAMsB,EAAMzC,EAAMuB,CAAS,EAAE,aAAe,EACtCsB,EAAQ,KAAK,IAAIC,GAAY7C,EAAYsB,CAAS,EAAGkB,CAAG,EACxDM,EAAO5B,EAAW,iBACtB,yBACF,EACA,QAASuB,EAAI,EAAGA,EAAIK,EAAK,OAAQL,IAC/BK,EAAKL,CAAC,EAAE,UAAU,OAChB,yCACAA,EAAIG,CACN,EAEF1B,EAAW,aAAa,gBAAiB,OAAO0B,CAAK,CAAC,CACxD,CAIA,SAASG,GAAwB,CAC/B,IAAMP,EAAMzC,EAAMuB,CAAS,EAAE,aAAe,EACtCsB,EAAQ,KAAK,IAAIC,GAAY7C,EAAYsB,CAAS,EAAGkB,CAAG,EAE1DxB,IACFA,EAAc,YAAcgC,GAAgBJ,EAAOJ,EAAKvC,CAAC,GAEvDgB,IACFA,EAAW,YACTgC,GAAY3B,EAAWvB,EAAM,OAAQE,CAAC,EACtC,UACCF,EAAMuB,CAAS,EAAE,MAAQ,KAG9B,IAAM4B,EAAYC,GAAcnD,EAAYD,EAAOuB,CAAS,EACtD8B,EAAS9B,IAAcvB,EAAM,OAAS,EAExCc,IAASA,EAAQ,MAAM,QAAUS,EAAY,EAAI,GAAK,QACtDR,IACFA,EAAQ,MAAM,QAAUsC,EAAS,OAAS,GAE1CtC,EAAQ,SAAW,CAACoC,GAElBnC,IACFA,EAAQ,MAAM,QAAUqC,EAAS,GAAK,OACtCrC,EAAQ,SAAW,CAACmC,GAEtBP,EAAkB,CACpB,CAEA,SAASU,GAAgB,CACvB,QAAWC,KAAK/B,EAAM+B,EAAE,QAAQ,EAChCP,EAAgB,CAClB,CAeA,SAASQ,GAAyB,CAKhC,GAJI,CAAC1D,EAAK,aAENyB,GAAavB,EAAM,OAAS,GAE5B8B,GAAa7B,EAAYD,EAAOuB,CAAS,IAAM,EAAG,OAEtD,IAAMkC,EAAOlC,EACTG,IAAqB,MAAM,aAAaA,CAAgB,EAC5DA,EAAmB,WAAW,IAAM,CAClCA,EAAmB,KACdJ,GACLoC,EAASD,EAAO,CAAC,CACnB,EAAG7D,EAAoB,CACzB,CAIA,SAAS+D,EAAiBC,EAAuB,CAC/C,GAAI,CAAClD,EAAW,OAEhBZ,EAAK,UAAU,UAAUY,CAAS,EAClCA,EAAU,UAAY,GACtBc,EAAO,CAAC,EAER,IAAMqC,EAAOlC,EAAYiC,CAAO,EAChC,QAAShC,EAAI,EAAGA,EAAIiC,EAAK,OAAQjC,IAAK,CACpC,IAAMkC,EAAMC,GAAeF,EAAKjC,CAAC,EAAGA,EAAG,CACrC,EAAA1B,EACA,SAAUJ,EAAK,SACf,SAAU+B,EACV,gBAAiB/B,EAAK,gBACtB,YAAaA,EAAK,YAClB,sBAAuB,IAAM,CAC3BA,EAAK,mBAAmB,EACxBwD,EAAQ,EACRE,EAAiB,CACnB,CACF,CAAC,EACDhC,EAAK,KAAKsC,CAAG,EACbpD,EAAU,YAAYoD,EAAI,EAAE,CAC9B,CAKA,IAAME,EAAY/D,EAAW2D,CAAO,GAAK,CAAC,EACpCK,EAA2C,OAAO,OAAO,IAAI,EACnE,QAAW3B,KAAM0B,EAAWC,EAAmB,OAAO3B,EAAG,SAAS,CAAC,EAAI,GACvE4B,GACExD,EACAc,EAAK,IAAK+B,GAAMA,EAAE,EAAE,EACnB3B,GAAMqC,EAAmB,OAAOJ,EAAKjC,CAAC,EAAE,EAAE,CAAC,IAAM,EACpD,EAEA9B,EAAK,UAAU,QAAQY,CAAS,CAClC,CAEA,SAASyD,EAAYC,EAAqB,CACxC,IAAMC,EAAUD,EAAE,OAAuB,QACvC,oBACF,EACA,GAAI,CAACC,GAAUA,EAAO,SAAU,OAGhC,IAAMC,EADO3C,EAAYJ,CAAS,EAChB,SAAS8C,EAAO,aAAa,kBAAkB,GAAK,GAAI,EAAE,CAAC,EAC7E,GAAI,CAACC,EAAM,OAEX,IAAMR,EAAMO,EAAO,QAAqB,8BAA8B,EAChEE,EAAaT,GAAOA,EAAI,aAAa,0BAA0B,EACjEU,GACDD,EACGD,EAAK,SAAS,KAAMG,IAAMA,GAAE,KAAO,SAASF,EAAY,EAAE,CAAC,EAC3D,OAAS,KAIf,GAHKC,IACHA,EAAkBF,EAAK,SAAS,KAAMG,IAAMA,GAAE,SAAS,GAAK,MAE1D,CAACD,EAAiB,OAItB,IAAME,EAAWzC,GACfhC,EACAsB,EACAiD,EAAgB,EAClB,EACA,GAAIE,IAAa,GAAI,CACnB,GAAI,CAAC5E,EAAK,UAAUG,EAAWsB,CAAS,EAAEmD,CAAQ,CAAC,EAAG,OACtDzE,EAAWsB,CAAS,EAAE,OAAOmD,EAAU,CAAC,EACxC5E,EAAK,mBAAmB,EACxBwD,EAAQ,EACR,MACF,CAEA,IAAMqB,GAAWb,GAAK,cAA2B,gBAAgB,EAC3Dc,GAAYD,GACd,KAAK,IACH,EACA,SACEA,GAAS,aAAa,UAAU,GAAKA,GAAS,aAAe,GAC7D,EACF,GAAK,CACP,EACAE,GAAoBP,EAAK,GAAIxE,EAAK,SAAU0E,EAAgB,EAAE,EAE5DM,GAAwB,CAC5B,UAAWR,EAAK,GAChB,UAAWE,EAAgB,GAC3B,MAAOF,EAAK,MACZ,IAAKA,EAAK,KAAO,KACjB,aAAcE,EAAgB,MAC9B,cAAeA,EAAgB,OAASF,EAAK,eAAiB,KAC9D,MAAOE,EAAgB,MACvB,eAAgBA,EAAgB,gBAAkB,KAClD,UAAWA,EAAgB,WAAa,KACxC,SAAUI,EACZ,EAIMG,GAAYxC,GAChBtC,EACAsB,EACA+C,EAAK,GACLE,EAAgB,GAChB1E,EAAK,QACP,EACA,GAAIiF,GAAY,EAAG,CACjBjF,EAAK,YAAYyB,EAAWuD,GAAS,KAAK,IAAIF,GAAWG,EAAS,CAAC,EACnEzB,EAAQ,EACR,MACF,CAEA,IAAM0B,GAAOlD,GAAa7B,EAAYD,EAAOuB,CAAS,EAClDyD,KAAS,KAAYJ,GAAYI,KAErC/E,EAAWsB,CAAS,EAAE,KAAKuD,EAAO,EAClChF,EAAK,mBAAmB,EACxBwD,EAAQ,EACRE,EAAiB,EACnB,CAEI9C,GAAWA,EAAU,iBAAiB,QAASyD,CAAW,EAI9D,SAASc,EAAarB,EAAuB,CAC3C,GAAI,CAACxC,GAAoB,CAACtB,EAAK,gBAAiB,OAEhD,IAAMoF,EAAkB,CAAC,EACnBC,EAAgC,OAAO,OAAO,IAAI,EACxD,QAAWC,KAAKzD,EAAYiC,CAAO,EAAG,CACpC,IAAMyB,GAAMD,EAAE,MAAQ,IAAI,KAAK,EAC3BC,GAAM,CAACF,EAAKE,CAAE,IAChBF,EAAKE,CAAE,EAAI,GACXH,EAAM,KAAKG,CAAE,EAEjB,CAEA,GAAIH,EAAM,OAAS,EAAG,CACpB9D,EAAiB,MAAM,QAAU,OACjCd,GAAa,UAAU,IAAI,qCAAqC,EAChE,MACF,CAEAc,EAAiB,MAAM,QAAU,GACjCd,GAAa,UAAU,OAAO,qCAAqC,EAEnEc,EAAiB,UAAY,GAC7B,QAAWkE,IAAS,CAAC9F,GAAW,GAAG0F,CAAK,EAAG,CACzC,IAAMK,EAAO,SAAS,cAAc,QAAQ,EAC5CA,EAAK,KAAO,SACZA,EAAK,UAAY,uBACjBA,EAAK,aAAa,cAAeD,CAAK,EACtCC,EAAK,aAAa,eAAgBD,IAAUjE,EAAa,OAAS,OAAO,EACrEiE,IAAUjE,GAAYkE,EAAK,UAAU,IAAI,8BAA8B,EAC3EA,EAAK,YAAcD,IAAU9F,GAAYU,EAAE,UAAY,MAAQoF,EAC/DC,EAAK,iBAAiB,QAAS,IAAM,CACnClE,EAAaiE,EACbE,EAAqB,EACrBC,EAAejF,EAAcA,EAAY,MAAQ,EAAE,CACrD,CAAC,EACDY,EAAiB,YAAYmE,CAAI,CACnC,CACF,CAEA,SAASC,GAA6B,CACpC,GAAI,CAACpE,EAAkB,OACvB,IAAMsE,EAAQtE,EAAiB,iBAA8B,eAAe,EAC5E,QAASQ,EAAI,EAAGA,EAAI8D,EAAM,OAAQ9D,IAAK,CACrC,IAAM+D,EAAKD,EAAM9D,CAAC,EAAE,aAAa,aAAa,IAAMP,EACpDqE,EAAM9D,CAAC,EAAE,UAAU,OAAO,+BAAgC+D,CAAE,EAC5DD,EAAM9D,CAAC,EAAE,aAAa,eAAgB+D,EAAK,OAAS,OAAO,CAC7D,CACF,CAEA,SAASF,EAAeG,EAAqB,CAC3C,GAAI,CAAClF,EAAW,OAChB,IAAMmF,EAAkBC,GAAcF,CAAK,EACrCG,EAAYrF,EAAU,iBAC1B,qBACF,EACIsF,EAAe,EAEnB,QAASpE,EAAI,EAAGA,EAAImE,EAAU,OAAQnE,IAAK,CACzC,IAAMqE,EAAQF,EAAUnE,CAAC,EAAE,aAAa,YAAY,GAAK,GACnDsE,EAAOH,EAAUnE,CAAC,EAAE,aAAa,WAAW,GAAK,GACjDuE,GACH,CAACN,GAAmBI,EAAM,QAAQJ,CAAe,IAAM,MACvDxE,IAAe7B,IAAa0G,IAAS7E,GACxC0E,EAAUnE,CAAC,EAAE,UAAU,OAAO,YAAa,CAACuE,CAAO,EAC/CA,GAASH,GACf,CAEIrF,IACFA,EAAW,MAAM,QACfqF,IAAiB,GAAKH,EAAgB,OAAS,EAAI,GAAK,QAExDpF,IACFA,EAAY,MAAM,QAAUmF,EAAM,OAAS,EAAI,GAAK,QAElDhF,IACFA,EAAU,YAAcwF,EACtBlG,EAAE,gBAAkB,2BACpB,CAAE,MAAO8F,CAAa,CACxB,EAEJ,CAEA,GAAIxF,EAAa,CACf,IAAM6F,EAAeC,GACnB,IAAMb,EAAejF,EAAY,KAAK,EACtCd,EACF,EACAc,EAAY,iBAAiB,QAAS6F,CAAY,CACpD,CACI5F,GACFA,EAAY,iBAAiB,QAAS,IAAM,CACrCD,IACLA,EAAY,MAAQ,GACpBiF,EAAe,EAAE,EACjBjF,EAAY,MAAM,EACpB,CAAC,EAuBH,SAASkD,EACP9B,EACA2E,EAAmD,CAAC,EAC9C,CACN,IAAMC,EAAS,KAAK,IAAI,EAAG,KAAK,IAAIxG,EAAM,OAAS,EAAG4B,CAAC,CAAC,EAOxD,GAJIF,IAAqB,OACvB,aAAaA,CAAgB,EAC7BA,EAAmB,MAEjBpB,IAGEmB,IAAiB,MAAM,aAAaA,CAAY,EACpDA,EAAe,KACfnB,EAAY,gBAAgB,sBAAsB,EAC9CiG,EAAK,UAAY,IAASC,IAAWjF,GAAW,CAC7CjB,EAAY,aACjBA,EAAY,aACV,uBACAkG,EAASjF,EAAY,UAAY,MACnC,EACA,IAAMkF,EAASnG,EACfmB,EAAe,WAAW,IAAM,CAC9BA,EAAe,KACfgF,EAAO,gBAAgB,sBAAsB,CAC/C,EAAG9G,EAAe,CACpB,CAkBF,GAhBA4B,EAAYiF,EAEZ7C,EAAiBpC,CAAS,EAC1B0D,EAAa1D,CAAS,EACtBF,EAAa7B,GACbgG,EAAqB,EACjBhF,IAAaA,EAAY,MAAQ,IACrCiF,EAAe,EAAE,EAIb/E,IAAWA,EAAU,UAAY,GAErC8B,EAAkBjB,CAAS,EAC3B+B,EAAQ,EAEJ1C,EAAW,CACb,IAAM6B,EAAMzC,EAAMuB,CAAS,EAAE,aAAe,EAC5CX,EAAU,YACRsC,GAAY3B,EAAWvB,EAAM,OAAQE,CAAC,EACtC,MACCF,EAAMuB,CAAS,EAAE,MAAQ,IAC1B,KACA0B,GACE,KAAK,IAAIH,GAAY7C,EAAYsB,CAAS,EAAGkB,CAAG,EAChDA,EACAvC,CACF,CACJ,CACI,CAACqG,EAAK,WAAahG,GAAc,OAAOA,EAAW,OAAU,YAC/DA,EAAW,MAAM,CAErB,CAEA,SAASmG,EAAKC,EAAuB,CAC/B,CAACxG,GAAWmB,IAChBA,EAAY,GAGZoC,EAAS,OAAOiD,GAAW,SAAWA,EAAS,EAAG,CAChD,UAAW,GACX,QAAS,EACX,CAAC,EAEDxG,EAAQ,MAAM,QAAU,GACnBA,EAAQ,aACbA,EAAQ,UAAU,IAAI,mCAAmC,EAEzDyG,GAAK,EAIL,WAAW,IAAM,CACX/F,EAAUA,EAAS,MAAM,EACxBP,GAAa,MAAM,CAC1B,EAAG,EAAE,EACP,CAEA,SAASuG,GAAc,CACrB,GAAI,CAAC1G,GAAW,CAACmB,EAAW,OAC5BA,EAAY,GAIRI,IAAqB,OACvB,aAAaA,CAAgB,EAC7BA,EAAmB,MAEjBD,IAAiB,OACnB,aAAaA,CAAY,EACzBA,EAAe,MAGjBtB,EAAQ,UAAU,OAAO,mCAAmC,EAC5D2G,GAAO,EAEP,WAAW,IAAM,CACVxF,IAAWnB,EAAQ,MAAM,QAAU,OAC1C,EAAGV,EAAmB,EAKtB,IAAMsH,EAAWC,GACfA,GAAMA,EAAG,eAAiB,KAAOA,EAAK,KAClCC,EACJF,EACEhH,EAAU,cACR,mBAAqBwB,EAAY,IACnC,CACF,GACAwF,EAAQhH,EAAU,cAA2B,kBAAkB,CAAC,GAChED,EAAK,UACHmH,GAAa,OAAOA,EAAU,OAAU,YAC1C,WAAW,IAAMA,EAAU,MAAM,EAAG,CAAC,CAEzC,CAgBA,GAdApG,GAAU,iBAAiB,QAASgG,CAAK,EACzC7F,GAAS,iBAAiB,QAAS6F,CAAK,EACxC/F,GAAS,iBAAiB,QAAS,IAAM,CACnCS,EAAY,GAAGmC,EAASnC,EAAY,CAAC,CAC3C,CAAC,EACDR,GAAS,iBAAiB,QAAS,IAAM,CAErCQ,EAAYvB,EAAM,OAAS,GAC3BoD,GAAcnD,EAAYD,EAAOuB,CAAS,GAE1CmC,EAASnC,EAAY,CAAC,CAE1B,CAAC,EAEGpB,EAAS,CACX,IAAI+G,EAAsC,KAC1C/G,EAAQ,iBAAiB,YAAciE,GAAM,CAC3C8C,EAAkB9C,EAAE,MACtB,CAAC,EACDjE,EAAQ,iBAAiB,UAAYiE,GAAM,CACrCA,EAAE,SAAWjE,GAAW+G,IAAoB/G,GAAS0G,EAAM,EAC/DK,EAAkB,IACpB,CAAC,CACH,CAEA,gBAAS,iBAAiB,UAAY9C,GAAM,CAC1C,GAAK9C,EAEL,IAAI8C,EAAE,MAAQ,SAAU,CACtBA,EAAE,eAAe,EACjByC,EAAM,EACN,MACF,CAEA,GAAIzC,EAAE,MAAQ,OAAS9D,EAAa,CAClC,IAAM6G,EAAYC,GAAa9G,CAAW,EAC1C,GAAI6G,EAAU,SAAW,EAAG,CAC1B/C,EAAE,eAAe,EACjB,MACF,CACA,IAAMiD,EAAQF,EAAU,CAAC,EACnBG,EAAOH,EAAUA,EAAU,OAAS,CAAC,EACvC/C,EAAE,UAAY,SAAS,gBAAkBiD,GAC3CjD,EAAE,eAAe,EACjBkD,EAAK,MAAM,GACF,CAAClD,EAAE,UAAY,SAAS,gBAAkBkD,IACnDlD,EAAE,eAAe,EACjBiD,EAAM,MAAM,EAEhB,EACF,CAAC,EAEM,CAAE,KAAAX,EAAM,MAAAG,EAAO,OAAQ,IAAMvF,EAAW,QAAAgC,CAAQ,CACzD,CC9oBA,SAASiE,IAA+B,CACtC,IAAMC,EAAK,6BACLC,EAAM,SAAS,gBAAgBD,EAAI,KAAK,EAC9CC,EAAI,aAAa,QAAS,IAAI,EAC9BA,EAAI,aAAa,SAAU,IAAI,EAC/BA,EAAI,aAAa,UAAW,WAAW,EACvCA,EAAI,aAAa,OAAQ,MAAM,EAC/B,OAAW,CAACC,EAAIC,EAAIC,EAAIC,CAAE,GAAK,CAC7B,CAAC,IAAK,IAAK,IAAK,IAAI,EACpB,CAAC,IAAK,IAAK,KAAM,GAAG,CACtB,EAAG,CACD,IAAMC,EAAO,SAAS,gBAAgBN,EAAI,MAAM,EAChDM,EAAK,aAAa,KAAMJ,CAAE,EAC1BI,EAAK,aAAa,KAAMH,CAAE,EAC1BG,EAAK,aAAa,KAAMF,CAAE,EAC1BE,EAAK,aAAa,KAAMD,CAAE,EAC1BC,EAAK,aAAa,SAAU,cAAc,EAC1CA,EAAK,aAAa,eAAgB,GAAG,EACrCA,EAAK,aAAa,iBAAkB,OAAO,EAC3CL,EAAI,YAAYK,CAAI,CACtB,CACA,OAAOL,CACT,CAEO,SAASM,GACdC,EACAC,EACAC,EACAC,EACM,CACN,IAAMC,EAAKH,EAAO,aACZI,EAAIC,GACJC,EAAWC,GAAcP,EAAO,aAAcA,EAAO,YAAY,EACjEQ,EAAQR,EAAO,OAAS,CAAC,EAC/B,GAAI,CAACQ,EAAM,OAAQ,OAEnB,IAAMC,EAAa,CACjB,kBAAmBT,EAAO,kBAC1B,kBAAmBA,EAAO,iBAC5B,EAGMU,EAA6BF,EAAM,IAAI,CAACG,EAAGC,IAC/CC,GAAgBb,EAAQY,CAAC,EAAE,IAAKE,GAAY,CAC1C,IAAMC,EAAIC,GAAaF,EAASd,EAAO,GAAI,CACzC,WAAAS,EACA,kBAAmB,IACrB,CAAC,EACD,MAAO,CACL,GAAI,OAAOM,EAAE,SAAS,EACtB,MAAOA,EAAE,OAAS,GAClB,IAAKA,EAAE,KAAO,KACd,KAAMD,EAAQ,aAAe,GAC7B,cAAeC,EAAE,eAAiB,KAClC,UAAWA,EAAE,SAAS,KAAME,GAAMA,EAAE,SAAS,EAC7C,YAAaF,EAAE,YACf,SAAUA,EAAE,SAAS,IAAKE,IAAO,CAC/B,GAAI,OAAOA,EAAE,EAAE,EACf,MAAOA,EAAE,MACT,QAASA,EAAE,QACX,UAAWA,EAAE,UACb,MAAOA,EAAE,MACT,eAAgBA,EAAE,eAClB,UAAWA,EAAE,UACb,MAAOA,EAAE,MACT,kBAAmBA,EAAE,mBAAqB,IAC5C,EAAE,CACJ,CACF,CAAC,CACH,EAQA,GAAI,EADFP,EAAM,OAAS,GAAKA,EAAM,MAAOQ,GAASA,EAAK,KAAMH,GAAMA,EAAE,SAAS,CAAC,GACjD,OAExB,IAAMI,EAAcC,GAClBpB,EAAO,SAAS,CAAC,GAAG,WAAW,gBAAgB,cAAgB,KACjE,EAEMqB,EAAQC,GAAiB,gBAAiB,CAC9C,MAAOtB,EAAO,MACd,SAAUA,EAAO,YACjB,eAAgBG,EAAG,YAAY,UAAY,GAC3C,iBAAkBA,EAAG,SAAS,qBAAuB,GACrD,QAASA,EAAG,KAAK,SAAWC,EAAE,WAAa,cAC3C,OAAQD,EAAG,WAAW,gBAAkB,GAAQ,KAAOH,EAAO,MAChE,CAAC,EAGDqB,EAAM,SAAS,OAAOE,GAAiBf,EAAM,MAAM,CAAC,EAOpD,IAAMgB,EAAS,SAAS,cAAc,KAAK,EAC3CA,EAAO,UAAY,wBACnBA,EAAO,aAAa,mBAAoB,EAAE,EAE1C,IAAMC,EAAO,SAAS,cAAc,QAAQ,EAC5CA,EAAK,KAAO,SACZA,EAAK,UAAY,gDACjBA,EAAK,aAAa,iBAAkB,GAAG,EACvC,IAAMC,EAAW,SAAS,cAAc,MAAM,EAC9CA,EAAS,UAAY,iCACrBA,EAAS,aAAa,cAAe,MAAM,EAC3CA,EAAS,YAAYpC,GAAc,CAAC,EACpCmC,EAAK,YAAYC,CAAQ,EACzB,IAAMC,EAAW,SAAS,cAAc,MAAM,EAC9CA,EAAS,UAAY,2BACrB,IAAMC,EAAY,SAAS,cAAc,MAAM,EAC/CA,EAAU,UAAY,kCACtBD,EAAS,YAAYC,CAAS,EAC9B,IAAMC,EAAU,SAAS,cAAc,MAAM,EAC7CA,EAAQ,UAAY,0BACpBF,EAAS,YAAYE,CAAO,EAC5BJ,EAAK,YAAYE,CAAQ,EACzBH,EAAO,YAAYC,CAAI,EAEvBjB,EAAM,QAAQ,CAACsB,EAAMlB,IAAM,CACzB,IAAMmB,EAAQ,SAAS,cAAc,SAAS,EAC9CA,EAAM,UAAY,uBAClBA,EAAM,aAAa,kBAAmB,OAAOnB,CAAC,CAAC,EAC/CmB,EAAM,aAAa,iBAAkB,KAAK,EAE1C,IAAMC,EAAa,IAAmB,CACpC,IAAMC,EAAQ,SAAS,cAAc,MAAM,EAC3C,OAAAA,EAAM,UAAY,6BAClBA,EAAM,aAAa,kBAAmB,OAAOrB,CAAC,CAAC,EACxCqB,CACT,EAEMC,EAAM,SAAS,cAAc,QAAQ,EAC3CA,EAAI,KAAO,SACXA,EAAI,UAAY,0BAChBA,EAAI,aAAa,gBAAiB,OAAOtB,CAAC,CAAC,EAC3CsB,EAAI,aAAa,aAAc,oBAAsBJ,EAAK,IAAI,EAC9D,IAAMK,EAAK,SAAS,cAAc,MAAM,EACxCA,EAAG,UAAY,yBACfA,EAAG,aAAa,cAAe,MAAM,EACrCA,EAAG,YAAc,OAAOvB,EAAI,CAAC,EAC7BsB,EAAI,YAAYC,CAAE,EAClB,IAAMC,EAAU,SAAS,cAAc,MAAM,EAC7CA,EAAQ,UAAY,4BACpBA,EAAQ,YAAcN,EAAK,KAC3BI,EAAI,YAAYE,CAAO,EACvBF,EAAI,YAAYF,EAAW,CAAC,EAC5BD,EAAM,YAAYG,CAAG,EAErB,IAAMG,EAAU,SAAS,cAAc,KAAK,EAC5CA,EAAQ,UAAY,+BACpB,IAAMC,EAAO,SAAS,cAAc,MAAM,EAC1CA,EAAK,UAAY,4BACjBA,EAAK,YAAcR,EAAK,KACxBO,EAAQ,YAAYC,CAAI,EACxBD,EAAQ,YAAYL,EAAW,CAAC,EAChCD,EAAM,YAAYM,CAAO,EAEzB,IAAME,EAAQ,SAAS,cAAc,KAAK,EAC1CA,EAAM,UAAY,8DAClBA,EAAM,aAAa,kBAAmB,OAAO3B,CAAC,CAAC,EAC/CmB,EAAM,YAAYQ,CAAK,EAEvBf,EAAO,YAAYO,CAAK,CAC1B,CAAC,EACDV,EAAM,SAAS,YAAYG,CAAM,EAKjC,IAAMgB,EAAUC,GAAiB,CAC/B,UAAWzC,EAAO,GAClB,MAAOA,EAAO,GAAG,QAAQ,MAAO,EAAE,EAClC,WAAYG,EAAG,aAAe,GAC9B,gBAAiBA,EAAG,0BAA4B,GAChD,aAAc,KACd,OAAQ,EACV,CAAC,EACDkB,EAAM,KAAK,YAAYmB,CAAO,EAE9B,IAAME,EAA6BlC,EAAM,IAAI,IAAM,CAAC,CAAC,EAE/CmC,EAAqB,CACzB,UAAWtB,EAAM,KACjB,MAAOb,EAAM,IAAI,CAACoC,EAAGhC,KAAO,CAC1B,KAAMgC,EAAE,KACR,YAAaA,EAAE,YACf,YAAaA,EAAE,YACf,iBAAkBlC,EAAME,CAAC,CAC3B,EAAE,EACF,WAAA8B,EACA,EAAAtC,EACA,SAAAE,EACA,YAAAa,EACA,SAAU,CAACW,EAAMe,IAAU,CACzB,IAAMC,EAAOJ,EAAWZ,CAAI,EAAEe,CAAK,EAC/B,CAACC,GAAQ,CAACC,GAAcL,EAAYI,EAAMxC,CAAQ,IACtDoC,EAAWZ,CAAI,EAAE,OAAOe,EAAO,CAAC,EAChCG,EAAU,EACNC,EAAO,OAAO,GAAGA,EAAO,QAAQ,EACtC,EACA,OAASnB,GAASmB,EAAO,KAAKnB,CAAI,CACpC,EAEA,SAASkB,GAAkB,CACzBE,GAAiBP,CAAQ,EACzBQ,GAAWR,CAAQ,EACnBS,GACET,EACA,CACE,aAAc3C,EAAO,eAAe,aACpC,cAAeA,EAAO,eAAe,aACvC,EACA,CAACqD,EAAOC,EAAMC,IACZC,GAAkBH,EAAOC,EAAuCC,CAAK,EACvEH,GAAajC,CAAW,CAC1B,EACAsC,GAAed,CAAQ,EACvBe,GAAUf,CAAQ,CACpB,CAEA,IAAMM,EAASU,GAAa,CAC1B,UAAWtC,EAAM,KACjB,MAAOsB,EAAS,MAChB,WAAAD,EACA,EAAAtC,EACA,SAAAE,EACA,gBAAiBH,EAAG,+BAAiC,GACrD,gBAAiBA,EAAG,0BAA4B,GAEhD,YAAaA,EAAG,uBAAyB,GACzC,YAAAgB,EACA,SAAU,OACV,aAAc,KACd,UAAWE,EAAM,IACjB,mBAAoB2B,EACpB,YAAa,CAACY,EAASd,EAAMe,IAAQ,CACnC,IAAIC,EAAQ,EACNhC,EAAOY,EAAWkB,CAAO,EAC/B,QAASG,EAAIjC,EAAK,OAAS,EAAGiC,GAAK,GAAKD,EAAQD,EAAKE,IAAK,CACxD,IAAMC,EAAKlC,EAAKiC,CAAC,EACjB,GAAIC,EAAG,YAAclB,EAAK,WAAakB,EAAG,YAAclB,EAAK,UAC3D,SAEF,IAAMmB,EAAO,KAAK,IAAID,EAAG,UAAY,EAAGH,EAAMC,CAAK,EAC/CG,IAASD,EAAG,UAAY,GAAIlC,EAAK,OAAOiC,EAAG,CAAC,EAC3CC,EAAG,UAAYA,EAAG,UAAY,GAAKC,EACxCH,GAASG,CACX,CACIH,EAAQ,IACVhB,EAAK,SAAWgB,EAChBhC,EAAK,KAAKgB,CAAI,EACdE,EAAU,EAEd,EACA,UAAYF,GAASC,GAAcL,EAAYI,EAAMxC,CAAQ,CAC/D,CAAC,EAIDmB,EAAK,iBAAiB,QAAS,IAAM,CAC/BwB,EAAO,OAAO,GAClBA,EAAO,KAAK,SAASxB,EAAK,aAAa,gBAAgB,GAAK,GAAI,EAAE,GAAK,CAAC,CAC1E,CAAC,EACDJ,EAAM,KAAK,iBAA8B,iBAAiB,EAAE,QAAS6C,GAAO,CAC1EA,EAAG,iBAAiB,QAAS,IAAM,CAC7BjB,EAAO,OAAO,GAClBA,EAAO,KAAK,SAASiB,EAAG,aAAa,eAAe,GAAK,GAAI,EAAE,GAAK,CAAC,CACvE,CAAC,CACH,CAAC,EAED7C,EAAM,IAAI,iBAAiB,QAAS,IAAM,CACnC8C,GAAazB,EAAYC,EAAS,KAAK,GAC5C1C,EACEmE,GAAe1B,CAAU,EAAE,IAAKI,IAAU,CACxC,cAAe,gCAAkCA,EAAK,UACtD,SAAUA,EAAK,SACf,WAAYuB,GAAqBrE,EAAO,GAAIA,EAAO,UAAU,CAC/D,EAAE,CACJ,CACF,CAAC,EAEDD,EAAU,YAAYsB,EAAM,IAAI,EAChCiD,GAAcjD,EAAM,IAAI,EACxB2B,EAAU,EAEV9C,IAAYqE,GAAclD,EAAM,IAAI,CAAC,CACvC,CCvTA,IAAMmD,GAAW,IAAI,IAAI,CACvB,MACA,UACA,YACA,YACA,aACA,OACA,MACA,SACA,WACA,QACA,IACA,QACF,CAAC,EAEKC,GAAU,IAAI,IAChBC,GAAoB,GAExB,SAASC,GAAOC,EAA4C,CAC1D,IAAMC,EAAMD,IAAO,cAAgB,iBAAmB,cACtD,QAAWE,KAAML,GACfK,EAAG,UAAU,IAAIF,CAAE,EACnBE,EAAG,UAAU,OAAOD,CAAG,CAE3B,CAEA,SAASE,GAAU,EAAwB,CACrCP,GAAS,IAAI,EAAE,GAAG,GAAGG,GAAO,gBAAgB,CAClD,CAEA,SAASK,IAAsB,CAC7BL,GAAO,aAAa,CACtB,CAEA,SAASM,IAAwB,CAC3BP,KACJA,GAAoB,GACpB,SAAS,iBAAiB,UAAWK,GAAW,EAAI,EACpD,SAAS,iBAAiB,cAAeC,GAAe,EAAI,EAC9D,CAEA,SAASE,IAAwB,CAC1BR,KACLA,GAAoB,GACpB,SAAS,oBAAoB,UAAWK,GAAW,EAAI,EACvD,SAAS,oBAAoB,cAAeC,GAAe,EAAI,EACjE,CAEO,SAASG,GAAeC,EAAiC,CAC9D,OAAAA,EAAO,UAAU,IAAI,aAAa,EAClCA,EAAO,UAAU,OAAO,gBAAgB,EACxCX,GAAQ,IAAIW,CAAM,EAClBH,GAAgB,EAET,IAAM,CACXR,GAAQ,OAAOW,CAAM,EACrBA,EAAO,UAAU,OAAO,aAAa,EACrCA,EAAO,UAAU,OAAO,gBAAgB,EACpCX,GAAQ,OAAS,GAAGS,GAAgB,CAC1C,CACF,CCpEO,IAAMG,GAAkB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAysBlBC,GAAmB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA8FnBC,GAAuB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAwiCvBC,GAAoB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAyLpBC,GAAkB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA2ElBC,GAAwB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAgWxBC,GAAsB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;A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","names":["src_exports","__export","LimeBundleElement","trackInputMode","StorefrontApiError","errors","e","DEFAULT_API_VERSION","resolveBuyer","buyer","hasInContext","config","createStorefrontClient","version","endpoint","query","variables","options","headers","mergedVariables","response","json","withInContext","_match","name","args","trimmed","extra","combined","BUNDLE_METAOBJECT_QUERY","SHOP_SETTINGS_QUERY","BUNDLES_FOR_PRODUCT_QUERY","CART_CREATE_MUTATION","CART_LINES_ADD_MUTATION","CART_LINES_QUERY","CART_LINES_REMOVE_MUTATION","BundleParseError","message","reason","RADIUS_PRESET_PX","WIDGET_CONFIG_DEFAULTS","mergeWidgetConfig","raw","input","sanitizeDefaultTier","sanitizeRadiusPreset","flattenWidgetConfig","config","CSS_VAR_MAP","PX_KEYS","applyWidgetConfigVars","el","flat","flatKey","cssVar","value","serialized","variantChevronUrl","ratioVars","thumbnailRatioVars","pickerRatioVars","strokeHex","ratio","VALID_BUNDLE_TYPES","ACTIVE_STATUSES","parseMetaobjectBundle","metaobjectId","fields","parseMetaobjectBundleStrict","err","fieldMap","f","title","rawBundleType","rawStatus","bundleType","status","startsAt","endsAt","now","start","end","products","resolveProducts","discountConfig","parseDiscountConfig","widgetConfig","parseWidgetConfig","base","parseSelectedVariantIds","parseNumericRecord","parseMarketVisibility","parseMarketIds","parseStringArrayField","parseAbWeight","parseVolumeTiers","parseIntField","parseRuleMap","parseBogoConfig","parseMultiSteps","parseCollectionProductIds","parseJsonField","stringList","v","x","steps","entry","s","minRaw","minQuantity","maxQuantity","out","collectionField","node","p","cfg","buyProductId","getProductId","clampQty","n","sideVariants","id","productsField","result","key","record","k","num","pid","rule","min","max","required","minN","maxN","minClamped","maxClamped","parsed","t","parseOneVolumeTier","minQty","tier","options","normalizeCurrencyRate","rate","convertBundleToPresentment","bundle","r","converted","bestValueTierIndex","tiers","discountType","best","bestIdx","i","savings","resolveDefaultTierIndex","defaultTier","tierCount","bestIndex","resolvePopularTierIndex","tierIndex","visible","WILDCARD","idKey","id","listMatches","list","candidate","compareByIdSuffix","key","isVisibleToBuyer","bundle","context","productIdMatches","gid","ref","resolveContextProduct","products","context","id","byId","p","handle","byHandle","resolveBundleQty","bundle","productId","variantId","vq","productsForStep","bundle","stepIndex","step","pinned","fromCollections","collectionId","productId","seen","result","product","reportImpression","config","event","sendEvent","reportAddToCart","observeImpression","element","callback","observer","entries","entry","payload","url","body","r","MAX_CSS_LENGTH","BLOCKED_PATTERNS","SAFE_URL_VALUE","sanitizeCustomCss","raw","css","_","hex","codePoint","pattern","label","urlPattern","urlMatch","urlValue","STYLE_ID_PREFIX","injectCustomCss","shopDomain","rawCss","sanitized","id","simpleHash","style","input","hash","i","formatMoney","amount","currencyCode","num","calculateDiscount","price","discountType","discountValue","UNIT_LABEL","formatUnitPrice","unitPrice","measurement","fallbackCurrency","currency","formatted","value","measurementText","dropdown_exports","__export","computePosition","emptyTypeAheadState","firstEnabled","handleKey","pushTypeAheadChar","DEFAULT_TRIGGER_MARGIN","trigger","viewportHeight","desiredHeight","margin","clip","upperBound","lowerBound","spaceBelow","spaceAbove","placement","maxHeight","offsetTop","PASSTHROUGH","options","i","lastEnabled","nextEnabled","from","step","idx","prevEnabled","isPrintable","key","event","state","isOpen","activeIndex","selectedIndex","TYPE_AHEAD_RESET_MS","char","now","resetMs","buffer","newState","opt","picker_exports","findVariantByOptions","isOptionValueAvailable","toPickerVariant","isUntrackedStock","variant","isFulfillable","requiredQty","stockCapForVariant","findVariantByOptions","variants","optionValues","v","match","j","isOptionValueAvailable","optionIndex","value","selected","ok","toPickerVariant","variant","requiredQty","o","isFulfillable","VISITOR_ID_KEY","inMemoryVisitorId","hashString","input","hash","i","getVisitorId","stored","fresh","randomId","pickAbSide","members","visitorId","testId","ordered","b","roll","acc","member","resolveAbTests","bundles","groups","bundle","existing","chosen","intlFormatMoney","currencyCode","cents","BUNDLE_GID_ATTRIBUTE","BUNDLE_TYPE_ATTRIBUTE","BUNDLE_ROLE_ATTRIBUTE","BUNDLE_ROLE_GET","bundleLineAttributes","bundleGid","bundleType","numericId","gid","tail","toCents","amount","n","imageAspectRatio","image","w","h","adaptVariant","v","requiredQty","fallbackCurrency","o","isFulfillable","formatUnitPrice","stockCapForVariant","adaptProduct","product","bundleId","opts","allowed","nodes","currency","productQty","resolveBundleQty","variants","qty","adapted","initial","adaptProducts","bundle","idx","findVariant","product","variantId","i","resolveSellableVariant","seeded","findVariantByOptions","optionValues","v","match","j","isOptionValueAvailable","variants","optionIndex","value","selected","ok","buildCartItems","products","items","i","variant","findVariant","qty","computeBundlePricing","products","discountType","discountValue","totalPrice","salePrice","variant","findVariant","qty","linePrice","unitDiscount","flatCents","recalcPricing","container","formatMoney","fixed","savings","saleEl","compareEl","savingsBar","savingsAmountEl","savingsPercentEl","pct","bindVariantSelects","row","getProduct","onSelected","optionSelects","positionOf","sel","collectOptionValues","values","i","pos","syncSelectsToVariant","variant","expected","recomputeDisabledState","product","selected","opts","o","isOptionValueAvailable","seeded","initialVariant","findVariant","e","findVariantByOptions","prev","hydrateRowControls","row","product","selected","badge","groups","buildOptionGroups","container","optionNames","i","group","label","select","seen","v","value","opt","productImgSrc","updateThumbnail","row","variant","productImage","thumb","img","seed","nextSrc","newImg","updateInlineQuantity","qty","el","applyVariant","product","formatMoney","priceEl","compareEl","compare","unitEl","box","tag","className","attrs","el","k","buildRowSkeleton","product","opts","eligible","row","thumb","ratio","info","title","oos","prices","qty","badge","buildWidgetShell","typeClass","root","header","heading","sub","countdown","label","products","summary","text","savings","cta","ctaLabel","buildVolumeTiers","tiers","groupLabel","showCompare","showPriceEach","group","tier","i","radio","price","unit","right","buildProgressBar","segmentCount","wrap","segments","labels","pad","n","initCountdown","container","countdown","widget","endsAt","endTime","timerEl","interval","update","remaining","days","hours","mins","secs","DEFAULT_STRINGS","formatItemsOutOfStock","t","count","raw","tpl","form","findScrollableAncestor","el","doc","win","cur","overflowY","openInstances","documentListenersAttached","eventInsideDropdown","event","inst","path","p","target","closeAllOutside","i","onWindowResize","attachDocListeners","detachDocListeners","registerOpen","instance","registerClosed","idx","closeOthers","except","ITEM_HEIGHT","LIST_PADDING_Y","MAX_VISIBLE_ITEMS","instances","instanceFor","select","bindDropdown","selectEl","doc","labelText","idBase","shell","trigger","listboxId","triggerLabel","chevron","listbox","modalOverlay","isOpen","activeIndex","typeAhead","dropdown_exports","optionEls","readOptionsState","sel","i","o","syncFromSelect","idx","li","updateActive","newIndex","liTop","liBottom","visTop","visBottom","position","rect","desiredHeight","scrollable","findScrollableAncestor","clipRect","pos","open","closeOthers","instance","selIdx","registerOpen","close","restoreFocus","registerClosed","commit","index","opt","onKeydown","event","optsState","action","r","onTriggerClick","optionIndexFrom","target","onListboxClick","onListboxMousemove","onShellFocusout","onSelectChange","observer","onListboxMousedown","destroy","BIND_SELECTOR","bindAll","root","selects","BIND_SELECTOR","i","bindDropdown","unbindAll","bound","instanceFor","bindDropdowns","root","bindAll","unbindAll","renderFixedBundle","container","bundle","onAddToCart","onCleanup","wc","products","adaptProducts","oosProducts","p","v","currency","formatMoney","intlFormatMoney","t","DEFAULT_STRINGS","shell","buildWidgetShell","product","isOos","row","buildRowSkeleton","sellable","resolveSellableVariant","selected","findVariant","hydrateRowControls","applyVariant","bindVariantSelects","variant","recalcPricing","label","formatItemsOutOfStock","buildCartItems","item","bundleLineAttributes","initCountdown","bindDropdowns","fill","template","values","out","key","formatNeedsSpots","min","map","form","formatOfSelected","count","total","t","fill","formatMoreToGo","remaining","formatSubtitle","discountType","discountValue","pct","DEFAULT_RULE","ruleFor","productId","rulesMap","variantId","vRule","rule","variantRuleFor","mergeRuleMaps","productRules","variantRules","products","keys","merged","key","defaultPickQuantity","totalUnits","items","qty","i","indexOfVariantInBundle","committedQtyForVariant","idx","isRowAddable","remainingSpots","stepperCeiling","stock","ownQty","productOtherUnits","variantMax","ceiling","THUMB_WIDTHS","PICKER_WIDTHS","THUMB_SIZES","PICKER_SIZES","imgWidth","url","w","srcset","widths","debounce","fn","delay","timer","args","normalizeText","str","PICKER_BASE_WIDTH","buildPickerRow","p","rowIndex","ctx","t","rulesMap","capacity","formatMoney","pRule","ruleFor","row","normalizeText","rowDisabled","stepperDisabled","thumbDiv","addedBadge","firstAvailable","v","initialThumbSrc","img","imgWidth","srcset","PICKER_WIDTHS","PICKER_SIZES","infoDiv","titleEl","titleLink","currentVariant","ruleForVariant","modalQty","priceEl","priceSaleEl","priceCompareEl","paintRowCompare","el","variant","qty","cmp","price","unitPriceEl","initialUnit","qtyGroup","qtyValueEl","current","effectiveMax","paint","setBoundsForVariant","stepperWrap","qtyMinusBtn","qtyPlusBtn","valueEl","alreadyInBundle","variantId","ownQty","otherUnits","swapUnits","stock","othersInBundle","stockCap","vMax","variantRuleFor","rMin","stepperCeiling","commitQtyToBundle","availVariants","optionSelects","optionNames","findMatchingVariant","values","s","o","j","syncSelectsToVariant","i","isValueAvailable","optionIndex","value","selected","ok","recomputeDisabled","sel","onOptionChange","resolvedQty","rowImg","imgSrc","refresh","groupsContainer","on","optName","groupEl","labelEl","seenValues","currentVal","av","val","opt","varLabel","needsSpotsEl","addBtn","actionsDiv","soldOutLabel","lastVariantId","already","inBundle","committed","rowSwapUnits","vRule","inSwapState","remaining","needsMoreSpots","isRowAddable","formatNeedsSpots","swapLabel","label","noStock","productMaxed","FOCUSABLE","getFocusable","container","nodes","result","i","savedY","lockCount","lock","body","unlock","sinkAddedRows","list","rowEls","isAdded","front","back","el","ALL_TYPES","CLOSE_TRANSITION_MS","SEARCH_DEBOUNCE_MS","createPickerModal","deps","container","data","t","eligibleProducts","items","overlay","q","sel","modalDialog","searchInput","searchClear","modalList","modalEmpty","modalLive","closeBtn","doneBtn","footerCountEl","subtitleEl","filtersContainer","showTypeFilters","activeType","productListBuilt","filtersBuilt","modalOpen","rows","refreshAllStepperBounds","r","updateModalMeta","count","totalUnits","remaining","formatOfSelected","formatSubtitle","refresh","buildProductList","i","row","buildPickerRow","onListClick","e","addBtn","prodIndex","prod","selectedId","selectedVariant","rowQtyEl","pickedQty","defaultPickQuantity","newItem","swapUnits","buildFilters","types","seen","p","ty","value","pill","syncActiveFilterPill","filterProducts","pills","on","query","normalizedQuery","normalizeText","listItems","visibleCount","title","type","matches","fill","handleSearch","debounce","open","selectedProductIds","it","sinkAddedRows","lock","close","unlock","elToFocus","mouseDownTarget","focusable","getFocusable","first","last","remainingUnits","items","requiredQty","totalUnits","atCapacity","alreadyInBundleForVariant","variantId","qty","i","productUnitsInOtherSlots","productId","excludeVariantId","it","canRemovePick","item","rulesMap","variantRuleFor","rule","ruleFor","unitsElsewhere","ci","cs","swapUnitsFor","indexOfVariantInBundle","removeIcon","NS","svg","x1","y1","x2","y2","line","buildSlotCard","item","trans","formatMoney","removable","onRemove","onEdit","card","thumb","img","imgWidth","srcset","THUMB_WIDTHS","THUMB_SIZES","info","title","variant","priceRow","unitCompare","strikeEl","saleEl","qtyInline","unitPriceEl","removeBtn","e","requiredChip","updateSlotUI","cont","items","reqQty","d","deps","trans","slotsWrap","i","index","buildSlotCard","canRemovePick","trigger","totalUnits","ctaBtn","ctaLabel","updatePricing","calcDiscount","paint","pricingSection","totalPrice","updateProgress","overlay","count","remaining","roots","root","segments","s","progressCount","formatOfSelected","progressRemaining","formatMoreToGo","segWrap","updatePricing","formatMoney","container","salePrice","comparePrice","saleEl","compareEl","savingsBar","savingsAmountEl","savingsPercentEl","savings","pct","seedSelection","eligibleProducts","rulesMap","requiredQty","courtesy","firstAvailable","p","v","pick","quantity","required","seededVariant","vRule","variantRuleFor","rule","ruleFor","qualifies","seed","icon","paths","NS","svg","x1","y1","x2","y2","line","el","tag","className","attrs","node","k","buildPickerModal","opts","titleId","overlay","dialog","header","top","heading","title","close","progress","segments","i","search","input","clear","empty","emptyText","footer","back","next","done","renderMixMatchBundle","container","bundle","onAddToCart","onCleanup","currentProductHandle","wc","t","DEFAULT_STRINGS","requiredQty","rulesMap","mergeRuleMaps","eligibleProducts","adaptProducts","p","i","v","formatMoney","intlFormatMoney","shell","buildWidgetShell","buildProgressBar","slots","addTrigger","overlay","buildPickerModal","currentIdx","selectedItems","seedSelection","rowCapacity","remainingUnits","committedQtyForVariant","alreadyInBundleForVariant","productUnitsInOtherSlots","swapUnitsFor","indexOfVariantInBundle","canRemovePick","atCapacity","qty","updateAll","updateSlotUI","modal","updatePricing","total","type","value","calculateDiscount","updateProgress","createPickerModal","item","totalUnits","vMax","variantRuleFor","headroom","ruleFor","freed","it","take","bundleLineAttributes","initCountdown","bindDropdowns","TIER_OOS_CLASS","tierQty","el","tierUnavailable","applyTierStockState","tierEls","cap","i","resolveAvailableTierIndex","preferred","p","stepToAvailableTier","from","direction","calcTierPrice","basePrice","discountType","tierEl","amt","pct","computeTierTotals","qty","priceEach","totalPrice","undiscountedTotal","formatItemCount","translations","form","updateAllTierPrices","allTiers","formatMoney","i","priceEachEl","compareEl","selectTier","container","index","totalSavings","itemCountEl","label","totalPriceEl","savingsBar","savingsAmountEl","savingsPercentEl","savingsPct","renderVolumeBundle","container","bundle","onAddToCart","productContext","wc","product","resolveContextProduct","variants","minTierQty","tier","pricingVariant","v","isFulfillable","basePrice","toCents","discountType","formatMoney","intlFormatMoney","t","DEFAULT_STRINGS","bestIdx","bestValueTierIndex","defaultIdx","resolveDefaultTierIndex","popularIdx","resolvePopularTierIndex","tiers","i","percent","amountCents","priceEach","savedPct","fill","shell","buildWidgetShell","group","buildVolumeTiers","tierEls","updateAllTierPrices","applyTierStockState","stockCapForVariant","selectedIndex","resolveAvailableTierIndex","noTierAvailable","apply","selectTier","e","el","tierUnavailable","stepToAvailableTier","label","qty","bundleLineAttributes","initCountdown","discountedUnit","priceCents","percent","computeBogoTotals","sides","buyQty","getQty","buyVariant","findVariant","getVariant","totalPrice","salePrice","repriceRow","row","variant","isGet","formatMoney","priceEl","compareEl","compare","recalcPricing","container","savings","saleEl","savingsBar","savingsAmountEl","savingsPercentEl","pct","buildBogoCartItems","sides","percent","buyQty","getQty","buyVariant","findVariant","getVariant","buyLine","getPriceCents","discountedUnit","BUNDLE_ROLE_ATTRIBUTE","BUNDLE_ROLE_GET","buildBogoPlus","plus","renderBogoBundle","container","bundle","onAddToCart","onCleanup","wc","buyProduct","p","getProduct","buyQty","getQty","percent","quantities","buyAllowed","getAllowed","sides","adaptProduct","oosCount","s","v","formatMoney","intlFormatMoney","t","DEFAULT_STRINGS","shell","buildWidgetShell","badgeText","side","i","isGet","isOos","row","buildRowSkeleton","initial","findVariant","hydrateRowControls","updateThumbnail","repriceRow","bindVariantSelects","variant","recalcPricing","label","formatItemsOutOfStock","buildBogoCartItems","item","bundleLineAttributes","initCountdown","bindDropdowns","stepOfLabel","i","total","t","fill","ofSelectedLabel","count","stepsCompletedLabel","completed","moreToGoLabel","remaining","fillPlural","map","locale","form","doorLabel","anyPicks","doorSubLabel","quick","unitsInStep","selections","i","qty","j","stepSatisfied","steps","stepHeadroom","max","allSatisfied","canRemovePick","selections","item","rulesMap","variantRuleFor","rule","ruleFor","unitsElsewhere","step","pick","swapUnitsForStep","i","productId","variantId","indexOfVariantInStep","qty","it","j","updateStepGroups","deps","container","steps","selections","t","min","section","countLabel","ofSelectedLabel","unitsInStep","met","stepSatisfied","countEls","c","slotsWrap","j","stepIdx","slotIdx","item","buildSlotCard","canRemovePick","updateDoor","door","firstUnsatisfied","remaining","anyPicks","i","parent","label","doorLabel","labelEl","subEl","doorSubLabel","stepName","target","updatePricing","data","calcDiscount","paint","pricingSection","allSatisfied","totalPrice","step","pick","updateProgress","completed","segments","s","progressCount","stepsCompletedLabel","progressRemaining","moreToGoLabel","segWrap","updateCta","ctaBtn","ctaLabel","mergeCartLines","selections","byVariant","order","step","item","qty","key","ALL_TYPES","CLOSE_TRANSITION_MS","SEARCH_DEBOUNCE_MS","STEP_CASCADE_MS","AUTO_ADVANCE_HOLD_MS","createWizard","deps","container","steps","selections","t","overlay","q","sel","modalDialog","modalTitle","searchInput","searchClear","modalList","modalEmpty","modalLive","closeBtn","backBtn","nextBtn","doneBtn","footerCountEl","subtitleEl","segmentsEl","filtersContainer","activeType","modalOpen","stepIndex","rows","cascadeTimer","autoAdvanceTimer","productsFor","i","rowCapacity","stepHeadroom","variantId","idx","indexOfVariantInStep","qty","step","pick","productId","it","swapUnitsForStep","buildStepSegments","min","s","seg","paintStepSegments","count","unitsInStep","segs","updateModalMeta","ofSelectedLabel","stepOfLabel","satisfied","stepSatisfied","isLast","refresh","r","maybeAutoAdvance","from","goToStep","buildProductList","forStep","pool","row","buildPickerRow","stepPicks","selectedProductIds","sinkAddedRows","onListClick","e","addBtn","prod","selectedId","selectedVariant","v","existing","rowQtyEl","pickedQty","defaultPickQuantity","newItem","swapUnits","room","buildFilters","types","seen","p","ty","value","pill","syncActiveFilterPill","filterProducts","pills","on","query","normalizedQuery","normalizeText","listItems","visibleCount","title","type","matches","fill","handleSearch","debounce","opts","target","dialog","open","atStep","lock","close","unlock","visible","el","elToFocus","mouseDownTarget","focusable","getFocusable","first","last","buildPlusIcon","NS","svg","x1","y1","x2","y2","line","renderMultiStepBundle","container","bundle","onAddToCart","onCleanup","wc","t","DEFAULT_STRINGS","rulesMap","mergeRuleMaps","steps","quantities","pools","_","i","productsForStep","product","p","adaptProduct","v","pool","formatMoney","intlFormatMoney","shell","buildWidgetShell","buildProgressBar","groups","door","doorIcon","doorCopy","doorLabel","doorSub","step","group","buildCount","count","row","ix","rowName","heading","name","slots","overlay","buildPickerModal","selections","bodyDeps","s","index","item","canRemovePick","updateAll","wizard","updateStepGroups","updateDoor","updatePricing","total","type","value","calculateDiscount","updateProgress","updateCta","createWizard","stepIdx","qty","freed","j","it","take","el","allSatisfied","mergeCartLines","bundleLineAttributes","initCountdown","bindDropdowns","NAV_KEYS","targets","listenersAttached","setAll","on","off","el","onKeyDown","onPointerDown","attachListeners","detachListeners","trackInputMode","target","BUNDLE_BASE_CSS","BUNDLE_FIXED_CSS","BUNDLE_MIX_MATCH_CSS","BUNDLE_VOLUME_CSS","BUNDLE_BOGO_CSS","BUNDLE_MULTI_STEP_CSS","BUNDLE_DROPDOWN_CSS","BUNDLE_SKELETON_CSS","cartStorageKey","shopDomain","resolveProductHandle","explicit","meta","match","LimeBundleElement","cleanup","name","oldValue","newValue","query","hasInContext","withInContext","bundle","isVisibleToBuyer","controller","client","createStorefrontClient","bundlePromise","singleBundleMode","handle","shopSettingsPromise","SHOP_SETTINGS_QUERY","customCss","injectCustomCss","sanitized","sanitizeCustomCss","err","signal","data","BUNDLE_METAOBJECT_QUERY","parsed","parseMetaobjectBundle","convertBundleToPresentment","productHandle","BUNDLES_FOR_PRODUCT_QUERY","refs","bundles","ref","resolveAbTests","getVisitorId","lines","ev","allowDefault","storage","key","cartId","isStaleCartError","errors","e","stockCapped","warnings","w","fail","message","checkoutUrl","payload","CART_LINES_ADD_MUTATION","CART_CREATE_MUTATION","dropSavedCart","res","CART_LINES_QUERY","abandonedLineIds","line","attr","BUNDLE_GID_ATTRIBUTE","CART_LINES_REMOVE_MUTATION","quantity","sum","l","totalPrice","variant","p","v","price","reportAddToCart","style","BUNDLE_BASE_CSS","BUNDLE_FIXED_CSS","BUNDLE_MIX_MATCH_CSS","BUNDLE_MULTI_STEP_CSS","BUNDLE_VOLUME_CSS","BUNDLE_BOGO_CSS","BUNDLE_DROPDOWN_CSS","customStyle","container","applyWidgetConfigVars","trackInputMode","dispatch","registerCleanup","fn","renderFixedBundle","renderMixMatchBundle","renderVolumeBundle","renderBogoBundle","renderMultiStepBundle","first","b","element","observeImpression","reportImpression","BUNDLE_SKELETON_CSS","LimeBundleElement"]}