PageSourceSearch

https://coffee-labo.co.jp/wp-content/plugins/coffee-labo-commerce/assets/js/storefront.js?ver=0.5.45

js coffee-labo.co.jp collected 2026-10-02 10:35:58 UTC 132,870 bytes, 4,004 lines download raw bytes

1(function () {
2  "use strict";
3
4  document.documentElement.classList.add("clc-js");
5
6  var settings = window.CLCCommerce || {};
7  var labels = settings.labels || {};
8  var locale = document.documentElement.lang || "ja-JP";
9  var own = Object.prototype.hasOwnProperty;
10
11  function ready(callback) {
12    if (document.readyState === "loading") {
13      document.addEventListener("DOMContentLoaded", callback, { once: true });
14      return;
15    }
16
17    callback();
18  }
19
20  function toArray(list) {
21    return Array.prototype.slice.call(list || []);
22  }
23
24  function allIncluding(scope, selector) {
25    var matches = toArray(scope.querySelectorAll(selector));
26
27    if (scope.matches && scope.matches(selector)) {
28      matches.unshift(scope);
29    }
30
31    return matches;
32  }
33
34  function setHidden(element, hidden) {
35    if (!element) {
36      return;
37    }
38
39    element.hidden = Boolean(hidden);
40  }
41
42  function setText(element, value) {
43    if (element) {
44      element.textContent = value == null ? "" : String(value);
45    }
46  }
47
48  function normalize(value) {
49    return String(value == null ? "" : value).trim().toLocaleLowerCase(locale);
50  }
51
52  function renderTemplate(template, replacements) {
53    var output = String(template || "");
54
55    Object.keys(replacements).forEach(function (key) {
56      var value = String(replacements[key]);
57      output = output.replace(new RegExp("\\{" + key + "\\}", "g"), value);
58    });
59
60    if (own.call(replacements, "price")) {
61      output = output.replace(/%s/g, String(replacements.price));
62    }
63
64    if (own.call(replacements, "count")) {
65      output = output.replace(/%d/g, String(replacements.count));
66    }
67
68    return output;
69  }
70
71  function connectionNodes(connection) {
72    if (!connection) {
73      return [];
74    }
75
76    if (Array.isArray(connection)) {
77      return connection.map(function (entry) {
78        return entry && entry.node ? entry.node : entry;
79      }).filter(Boolean);
80    }
81
82    if (Array.isArray(connection.nodes)) {
83      return connection.nodes.filter(Boolean);
84    }
85
86    if (Array.isArray(connection.edges)) {
87      return connection.edges.map(function (edge) {
88        return edge && edge.node ? edge.node : edge;
89      }).filter(Boolean);
90    }
91
92    return [];
93  }
94
95  function extractProduct(payload) {
96    var product;
97
98    if (!payload || typeof payload !== "object") {
99      return null;
100    }
101
102    if (payload.data && payload.data.product) {
103      product = Object.assign({}, payload.data.product);
104      if (payload.data.variants) {
105        product.variants = payload.data.variants;
106      }
107      return product;
108    }
109
110    if (payload.product) {
111      product = Object.assign({}, payload.product);
112      if (payload.variants) {
113        product.variants = payload.variants;
114      }
115      return product;
116    }
117
118    if (payload.node && payload.node.variants) {
119      return payload.node;
120    }
121
122    return payload;
123  }
124
125  function parseProductData(productElement) {
126    var script = productElement.querySelector("script.clc-product-data[type=\"application/json\"]");
127
128    if (!script) {
129      return null;
130    }
131
132    try {
133      return extractProduct(JSON.parse(script.textContent || "{}"));
134    } catch (error) {
135      return null;
136    }
137  }
138
139  function moneyValue(value, fallbackCurrency) {
140    var amount;
141    var currency = fallbackCurrency || settings.currency || "JPY";
142
143    if (value == null || value === "") {
144      return null;
145    }
146
147    if (typeof value === "object") {
148      amount = value.amount;
149      currency = value.currencyCode || currency;
150    } else {
151      amount = value;
152    }
153
154    amount = Number(amount);
155
156    if (!Number.isFinite(amount)) {
157      return null;
158    }
159
160    return {
161      amount: amount,
162      currencyCode: currency
163    };
164  }
165
166  function productCurrency(product, variants) {
167    var priceRange = product.priceRange || {};
168    var rangePrice = priceRange.minVariantPrice || priceRange.maxVariantPrice || {};
169    var firstPrice = variants[0] && variants[0].price;
170
171    return product.currencyCode || rangePrice.currencyCode ||
172      (firstPrice && firstPrice.currencyCode) || settings.currency || "JPY";
173  }
174
175  function formatMoney(money) {
176    if (!money) {
177      return "";
178    }
179
180    try {
181      return new Intl.NumberFormat(locale, {
182        style: "currency",
183        currency: money.currencyCode || "JPY",
184        currencyDisplay: "symbol"
185      }).format(money.amount);
186    } catch (error) {
187      return String(money.amount) + " " + String(money.currencyCode || "");
188    }
189  }
190
191  function optionMap(inputs) {
192    var selected = Object.create(null);
193
194    inputs.forEach(function (input) {
195      var name = input.getAttribute("data-option-name");
196
197      if (name && input.checked) {
198        selected[name] = input.value;
199      }
200    });
201
202    return selected;
203  }
204
205  function variantOptions(variant) {
206    var selected = Object.create(null);
207
208    toArray(variant && variant.selectedOptions).forEach(function (option) {
209      if (option && option.name != null) {
210        selected[String(option.name)] = String(option.value == null ? "" : option.value);
211      }
212    });
213
214    return selected;
215  }
216
217  function matchesOptions(variant, selected) {
218    var options = variantOptions(variant);
219    var names = Object.keys(selected);
220
221    return names.every(function (name) {
222      return own.call(options, name) && String(options[name]) === String(selected[name]);
223    });
224  }
225
226  function selectedOptionNames(inputs) {
227    var names = Object.create(null);
228
229    inputs.forEach(function (input) {
230      var name = input.getAttribute("data-option-name");
231      if (name) {
232        names[name] = true;
233      }
234    });
235
236    return Object.keys(names);
237  }
238
239  function findSelectedVariant(variants, inputs) {
240    var selected = optionMap(inputs);
241    var requiredCount = selectedOptionNames(inputs).length;
242
243    if (!requiredCount || Object.keys(selected).length < requiredCount) {
244      return null;
245    }
246
247    return variants.find(function (variant) {
248      return matchesOptions(variant, selected);
249    }) || null;
250  }
251
252  function isVariantAvailable(variant) {
253    if (!variant) {
254      return false;
255    }
256
257    if (typeof variant.availableForSale === "boolean") {
258      return variant.availableForSale;
259    }
260
261    if (typeof variant.available === "boolean") {
262      return variant.available;
263    }
264
265    return true;
266  }
267
268  function idEquals(left, right) {
269    return left != null && right != null && String(left) === String(right);
270  }
271
272  function numericVariantId(variant) {
273    var match = String(variant && variant.id || "").match(/(?:^|\/)([1-9][0-9]*)$/);
274    return match ? match[1] : "";
275  }
276
277  function synchronizeVariantUrl(product, variant, productElement) {
278    var handle = String(product && product.handle || productElement.getAttribute("data-product-handle") || "");
279    var variantId = numericVariantId(variant);
280    var url;
281    var pathMatch;
282
283    if (!handle || !variantId || !window.history || typeof window.history.replaceState !== "function") {
284      return;
285    }
286
287    try {
288      url = new URL(window.location.href);
289      pathMatch = url.pathname.match(/\/products\/([^/]+)\/?$/);
290      if (!pathMatch || decodeURIComponent(pathMatch[1]) !== handle) {
291        return;
292      }
293      if (url.searchParams.get("variant") === variantId) {
294        return;
295      }
296
297      // Keep campaign, discount, and any future public query parameters intact;
298      // only Shopify's selected numeric variant is replaced in-place.
299      url.searchParams.set("variant", variantId);
300      window.history.replaceState(window.history.state, "", url.toString());
301    } catch (error) {
302      // URL synchronization is progressive enhancement. A restricted history
303      // implementation must never interrupt variant selection or purchasing.
304    }
305  }
306
307  function preferredVariant(product, variants, productElement, inputs) {
308    var selected = findSelectedVariant(variants, inputs);
309    var requestedId = productElement.getAttribute("data-selected-variant-id") ||
310      product.selectedVariantId ||
311      (product.selectedOrFirstAvailableVariant && product.selectedOrFirstAvailableVariant.id);
312
313    if (selected) {
314      return selected;
315    }
316
317    if (requestedId) {
318      selected = variants.find(function (variant) {
319        return idEquals(variant.id, requestedId);
320      });
321
322      if (selected) {
323        return selected;
324      }
325    }
326
327    return variants.find(isVariantAvailable) || variants[0] || null;
328  }
329
330  function synchronizeOptionInputs(inputs, variant) {
331    var selected = variantOptions(variant);
332
333    inputs.forEach(function (input) {
334      var name = input.getAttribute("data-option-name");
335
336      if (name && own.call(selected, name)) {
337        input.checked = String(input.value) === String(selected[name]);
338      }
339    });
340  }
341
342  function allocationsFor(variant) {
343    var allocations = connectionNodes(variant && variant.sellingPlanAllocations);
344
345    if (allocations.length || !variant) {
346      return allocations;
347    }
348
349    // The PHP view model intentionally flattens Storefront API allocations so
350    // the browser receives only the selling-plan fields it renders.
351    return connectionNodes(variant.sellingPlans).map(function (plan) {
352      var adjustments = connectionNodes(plan.priceAdjustments);
353
354      if (!adjustments.length && (plan.price || plan.perDeliveryPrice)) {
355        adjustments = [{
356          price: plan.price,
357          compareAtPrice: plan.compareAtPrice,
358          perDeliveryPrice: plan.perDeliveryPrice,
359          orderCount: null
360        }];
361      }
362
363      return {
364        sellingPlan: plan,
365        priceAdjustments: adjustments
366      };
367    });
368  }
369
370  function allocationFor(variant, sellingPlanId) {
371    if (!sellingPlanId) {
372      return null;
373    }
374
375    return allocationsFor(variant).find(function (allocation) {
376      return allocation && allocation.sellingPlan && idEquals(allocation.sellingPlan.id, sellingPlanId);
377    }) || null;
378  }
379
380  function selectedPlanId(planInputs) {
381    var checked = planInputs.find(function (input) {
382      return input.checked && !input.disabled;
383    });
384
385    return checked ? String(checked.value || "") : "";
386  }
387
388  function synchronizePlanInputs(planInputs, variant, requiresSellingPlan, allowFallback) {
389    var ids = Object.create(null);
390    var selectedWasDisabled = false;
391
392    allocationsFor(variant).forEach(function (allocation) {
393      if (allocation && allocation.sellingPlan && allocation.sellingPlan.id != null) {
394        ids[String(allocation.sellingPlan.id)] = true;
395      }
396    });
397
398    planInputs.forEach(function (input) {
399      var isOneTime = String(input.value || "") === "";
400      var enabled = (isOneTime && !requiresSellingPlan) || (!isOneTime && Boolean(ids[String(input.value)]));
401      input.disabled = !enabled;
402
403      if (!enabled && input.checked) {
404        input.checked = false;
405        selectedWasDisabled = true;
406      }
407    });
408
409    if (allowFallback && (selectedWasDisabled || !planInputs.some(function (input) { return input.checked && !input.disabled; }))) {
410      var fallback = planInputs.find(function (input) {
411        return !input.disabled && String(input.value || "") === "";
412      }) || planInputs.find(function (input) {
413        return !input.disabled;
414      });
415
416      if (fallback) {
417        fallback.checked = true;
418      }
419    }
420
421    return {
422      selectionCleared: selectedWasDisabled,
423      hasSelection: planInputs.some(function (input) { return input.checked && !input.disabled; })
424    };
425  }
426
427  function variantPricing(variant, sellingPlanId, currency) {
428    var allocation = allocationFor(variant, sellingPlanId);
429    var adjustments = allocation ? connectionNodes(allocation.priceAdjustments) : [];
430    var adjustment = adjustments[0];
431    var laterAdjustment = adjustments[1] || null;
432    var regular = moneyValue(variant && variant.price, currency);
433    var current = moneyValue(adjustment && adjustment.price, currency) || regular;
434    var compare = moneyValue(adjustment && adjustment.compareAtPrice, currency) ||
435      moneyValue(variant && variant.compareAtPrice, currency);
436    var perDelivery = moneyValue(adjustment && adjustment.perDeliveryPrice, currency);
437    var later = moneyValue(laterAdjustment && (laterAdjustment.perDeliveryPrice || laterAdjustment.price), currency);
438
439    return {
440      allocation: allocation,
441      adjustment: adjustment,
442      adjustments: adjustments,
443      laterAdjustment: laterAdjustment,
444      current: current,
445      compare: compare,
446      perDelivery: perDelivery,
447      later: later
448    };
449  }
450
451  function productImage(variant) {
452    if (!variant || !variant.image) {
453      return null;
454    }
455
456    if (typeof variant.image === "string") {
457      return { url: variant.image, altText: "" };
458    }
459
460    return {
461      url: variant.image.url || variant.image.src || "",
462      altText: variant.image.altText || variant.image.alt || "",
463      width: Number(variant.image.width) || 0,
464      height: Number(variant.image.height) || 0
465    };
466  }
467
468  function responsiveShopifyImageUrl(url, width) {
469    var parsed;
470
471    try {
472      parsed = new URL(String(url || ""), window.location.href);
473    } catch (error) {
474      return "";
475    }
476
477    if (parsed.protocol !== "https:" || parsed.hostname !== "cdn.shopify.com") {
478      return "";
479    }
480
481    parsed.searchParams.set("width", String(width));
482    return parsed.toString();
483  }
484
485  function applyResponsiveProductImage(imageElement, url) {
486    var widths = [480, 800, 1200];
487    var sources = widths.map(function (width) {
488      var source = responsiveShopifyImageUrl(url, width);
489      return source ? source + " " + String(width) + "w" : "";
490    }).filter(Boolean);
491
492    imageElement.src = url;
493    if (sources.length === widths.length) {
494      imageElement.srcset = sources.join(", ");
495      imageElement.sizes = "(max-width: 900px) 100vw, 58vw";
496    } else {
497      imageElement.removeAttribute("srcset");
498      imageElement.removeAttribute("sizes");
499    }
500  }
501
502  function analyticsItem(product, variant, pricing, quantity, sellingPlanId) {
503    var price = pricing && pricing.current ? pricing.current.amount : 0;
504    var item = {
505      item_id: String(product.id || product.handle || ""),
506      item_name: String(product.title || ""),
507      price: price,
508      quantity: quantity || 1
509    };
510
511    if (variant && variant.title) {
512      item.item_variant = String(variant.title);
513    }
514
515    if (variant && variant.sku) {
516      item.item_variant_id = String(variant.sku);
517    }
518
519    if (product.vendor) {
520      item.item_brand = String(product.vendor);
521    }
522
523    if (product.productType) {
524      item.item_category = String(product.productType);
525    }
526
527    if (sellingPlanId) {
528      item.item_subscription_plan = String(sellingPlanId);
529    }
530
531    return item;
532  }
533
534  function sendAnalyticsEvent(eventName, ecommerce, onComplete) {
535    var completed = false;
536    var callbackTimer = 0;
537    var eventPayload;
538
539    if (!eventName || !ecommerce) {
540      if (typeof onComplete === "function") {
541        onComplete();
542      }
543      return;
544    }
545
546    window.dataLayer = window.dataLayer || [];
547    if (typeof window.gtag !== "function") {
548      window.gtag = function () {
549        window.dataLayer.push(arguments);
550      };
551    }
552
553    eventPayload = Object.assign({}, ecommerce);
554    if (typeof onComplete === "function") {
555      eventPayload.event_callback = function () {
556        if (completed) {
557          return;
558        }
559        completed = true;
560        window.clearTimeout(callbackTimer);
561        onComplete();
562      };
563      eventPayload.event_timeout = 1200;
564      callbackTimer = window.setTimeout(eventPayload.event_callback, 1500);
565    }
566
567    // The live site uses the Google tag directly, not a GTM container. Queue
568    // one gtag command only so events are not counted twice. Checkout waits for
569    // its callback (or a bounded fallback timer) before leaving the document.
570    window.gtag("event", eventName, eventPayload);
571  }
572
573  function navigateToPreparedCheckout(checkoutLink, fallbackUrl) {
574    // Read the live href on the next task. Google's linker decorates the real
575    // Checkout anchor while the trusted click bubbles to document; deferring
576    // this read preserves that `_gl` value even when gtag calls back
577    // synchronously. The validated Storefront API URL remains a fail-open
578    // fallback so analytics blocking can never block a purchase.
579    window.setTimeout(function () {
580      var destination = checkoutLink && checkoutLink.href ? checkoutLink.href : fallbackUrl;
581      if (destination) {
582        window.location.assign(String(destination));
583      }
584    }, 0);
585  }
586
587  function pushEcommerce(eventName, product, variant, pricing, quantity, sellingPlanId) {
588    if (!eventName || !product || !variant || !pricing || !pricing.current) {
589      return;
590    }
591
592    var count = quantity || 1;
593    var item = analyticsItem(product, variant, pricing, count, sellingPlanId);
594    var ecommerce = {
595      currency: pricing.current.currencyCode,
596      value: Number((pricing.current.amount * count).toFixed(2)),
597      items: [item]
598    };
599
600    sendAnalyticsEvent(eventName, ecommerce);
601  }
602
603  function pushViewItemOnce(product, variant, pricing) {
604    var key = String(product.id || product.handle || product.title || "product");
605    var viewed = window.__CLCCommerceViewedItems;
606
607    if (!viewed || typeof viewed.has !== "function" || typeof viewed.add !== "function") {
608      viewed = new Set();
609      window.__CLCCommerceViewedItems = viewed;
610    }
611
612    if (viewed.has(key)) {
613      return;
614    }
615
616    viewed.add(key);
617    pushEcommerce("view_item", product, variant, pricing, 1, "");
618  }
619
620  var cartStorageKey = "clc_commerce_cart_v1";
621  var pendingCheckoutStorageKey = "clc_commerce_pending_checkouts_v1";
622  var checkoutReconciliationLockKey = "clc_commerce_checkout_reconciliation_lock_v1";
623  var discountStorageKey = "clc_commerce_discount_v1";
624  var cartMemory = [];
625  var pendingCheckoutMemory = [];
626  var cartStorageAvailable = null;
627  var pendingCheckoutStorageAvailable = null;
628  var checkoutReconciliationRunning = false;
629
630  function createClientId(prefix) {
631    var random = "";
632
633    try {
634      if (window.crypto && typeof window.crypto.randomUUID === "function") {
635        random = window.crypto.randomUUID();
636      }
637    } catch (error) {
638      random = "";
639    }
640    if (!random) {
641      random = String(Date.now()) + "-" + Math.random().toString(36).slice(2);
642    }
643    return String(prefix || "clc") + "-" + random;
644  }
645
646  function normalizeDiscountCode(value) {
647    var code = String(value == null ? "" : value).trim();
648    return /^[A-Za-z0-9_-]{1,255}$/.test(code) ? code : "";
649  }
650
651  function activeDiscountCode() {
652    var queryCode = "";
653    var storedCode = "";
654
655    try {
656      queryCode = normalizeDiscountCode(new URL(window.location.href).searchParams.get("discount"));
657    } catch (error) {
658      queryCode = "";
659    }
660
661    if (queryCode) {
662      try {
663        window.sessionStorage.setItem(discountStorageKey, queryCode);
664      } catch (error) {
665        // Strict storage settings can block sessionStorage. The current URL
666        // still carries the code for this page's Checkout preparation.
667      }
668      return queryCode;
669    }
670
671    try {
672      storedCode = normalizeDiscountCode(window.sessionStorage.getItem(discountStorageKey));
673    } catch (error) {
674      storedCode = "";
675    }
676    return storedCode;
677  }
678
679  function clearDiscountCode(expectedCode) {
680    var expected = normalizeDiscountCode(expectedCode);
681    var stored = "";
682
683    try {
684      stored = normalizeDiscountCode(window.sessionStorage.getItem(discountStorageKey));
685      if (!expected || !stored || stored === expected) {
686        window.sessionStorage.removeItem(discountStorageKey);
687      }
688    } catch (error) {
689      // There is no persistent coupon state to clear when storage is blocked.
690    }
691  }
692
693  function cartItemMaximum(item) {
694    return String(item && item.productHandle || "") === "trial-set" ? 1 : 20;
695  }
696
697  function normalizeCartItem(item) {
698    var quantity;
699    var amount;
700
701    if (!item || typeof item !== "object") {
702      return null;
703    }
704
705    if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(String(item.productHandle || ""))) {
706      return null;
707    }
708
709    if (!/^gid:\/\/shopify\/ProductVariant\/[1-9][0-9]*$/.test(String(item.variantId || ""))) {
710      return null;
711    }
712
713    if (item.sellingPlanId && !/^gid:\/\/shopify\/SellingPlan\/[1-9][0-9]*$/.test(String(item.sellingPlanId))) {
714      return null;
715    }
716
717    quantity = Math.min(cartItemMaximum(item), Math.max(1, Number.parseInt(item.quantity, 10) || 1));
718    amount = Number(item.price && item.price.amount);
719
720    return {
721      key: String(item.variantId) + "::" + String(item.sellingPlanId || ""),
722      entryId: /^[A-Za-z0-9._:-]{1,160}$/.test(String(item.entryId || "")) ? String(item.entryId) : "",
723      productHandle: String(item.productHandle),
724      productId: String(item.productId || item.productHandle),
725      title: String(item.title || "商品"),
726      variantId: String(item.variantId),
727      variantTitle: String(item.variantTitle || ""),
728      sellingPlanId: item.sellingPlanId ? String(item.sellingPlanId) : "",
729      sellingPlanName: String(item.sellingPlanName || ""),
730      quantity: quantity,
731      price: Number.isFinite(amount) ? {
732        amount: amount,
733        currencyCode: String((item.price && item.price.currencyCode) || "JPY")
734      } : null,
735      imageUrl: String(item.imageUrl || ""),
736      imageAlt: String(item.imageAlt || item.title || "")
737    };
738  }
739
740  function readCart() {
741    var parsed;
742    var stored;
743    var migrated = false;
744
745    if (cartStorageAvailable === false) {
746      parsed = cartMemory;
747    } else {
748      try {
749        stored = window.localStorage.getItem(cartStorageKey);
750        parsed = JSON.parse(stored || "[]");
751        cartStorageAvailable = true;
752      } catch (error) {
753        cartStorageAvailable = false;
754        parsed = cartMemory;
755      }
756    }
757
758    if (!Array.isArray(parsed)) {
759      parsed = [];
760    }
761
762    cartMemory = parsed.slice(0, 20).map(normalizeCartItem).filter(Boolean).map(function (item) {
763      if (!item.entryId) {
764        item.entryId = createClientId("cart");
765        migrated = true;
766      }
767      return item;
768    });
769    if (migrated && cartStorageAvailable !== false) {
770      try {
771        window.localStorage.setItem(cartStorageKey, JSON.stringify(cartMemory));
772      } catch (error) {
773        cartStorageAvailable = false;
774      }
775    }
776    return cartMemory.slice();
777  }
778
779  function writeCart(items) {
780    cartMemory = items.slice(0, 20).map(normalizeCartItem).filter(Boolean).map(function (item) {
781      if (!item.entryId) {
782        item.entryId = createClientId("cart");
783      }
784      return item;
785    });
786
787    try {
788      window.localStorage.setItem(cartStorageKey, JSON.stringify(cartMemory));
789      cartStorageAvailable = true;
790    } catch (error) {
791      cartStorageAvailable = false;
792      // Private browsing or strict storage settings can block localStorage.
793    }
794
795    document.dispatchEvent(new CustomEvent("clc:cart-changed", {
796      detail: { items: cartMemory.slice() }
797    }));
798  }
799
800  function normalizePendingCheckout(checkout) {
801    var cartId;
802    var cartToken;
803    var items;
804    var createdAt;
805    var checkedAt;
806
807    if (!checkout || typeof checkout !== "object") {
808      return null;
809    }
810
811    cartId = String(checkout.cartId || "");
812    if (
813      cartId.indexOf("gid://shopify/Cart/") !== 0 ||
814      cartId.length > 4096 ||
815      /[\u0000-\u0020\u007f<>"']/.test(cartId)
816    ) {
817      return null;
818    }
819    cartToken = String(checkout.cartToken || "");
820    if (!/^[A-Za-z0-9_-]+\.[a-f0-9]{64}$/.test(cartToken) || cartToken.length > 512) {
821      return null;
822    }
823
824    items = Array.isArray(checkout.items) ? checkout.items.map(normalizeCartItem).filter(Boolean) : [];
825    if (!items.length) {
826      return null;
827    }
828
829    createdAt = Number(checkout.createdAt);
830    checkedAt = Number(checkout.checkedAt);
831    return {
832      cartId: cartId,
833      cartToken: cartToken,
834      items: items,
835      discountCode: normalizeDiscountCode(checkout.discountCode),
836      createdAt: Number.isFinite(createdAt) && createdAt > 0 ? createdAt : Date.now(),
837      checkedAt: Number.isFinite(checkedAt) && checkedAt > 0 ? checkedAt : 0
838    };
839  }
840
841  function readPendingCheckouts() {
842    var parsed;
843    var stored;
844
845    if (pendingCheckoutStorageAvailable === false) {
846      parsed = pendingCheckoutMemory;
847    } else {
848      try {
849        stored = window.localStorage.getItem(pendingCheckoutStorageKey);
850        parsed = JSON.parse(stored || "[]");
851        pendingCheckoutStorageAvailable = true;
852      } catch (error) {
853        pendingCheckoutStorageAvailable = false;
854        parsed = pendingCheckoutMemory;
855      }
856    }
857
858    pendingCheckoutMemory = (Array.isArray(parsed) ? parsed : []).map(normalizePendingCheckout).filter(Boolean).slice(-5);
859    return pendingCheckoutMemory.slice();
860  }
861
862  function writePendingCheckouts(checkouts) {
863    var normalized = (Array.isArray(checkouts) ? checkouts : []).map(normalizePendingCheckout).filter(Boolean).slice(-5);
864
865    pendingCheckoutMemory = normalized.slice();
866
867    try {
868      if (normalized.length) {
869        window.localStorage.setItem(pendingCheckoutStorageKey, JSON.stringify(normalized));
870      } else {
871        window.localStorage.removeItem(pendingCheckoutStorageKey);
872      }
873      pendingCheckoutStorageAvailable = true;
874    } catch (error) {
875      pendingCheckoutStorageAvailable = false;
876      // The cart remains untouched when persistent storage is unavailable.
877    }
878  }
879
880  function rememberPendingCheckout(cartId, cartToken, items, discountCode) {
881    var pending = normalizePendingCheckout({
882      cartId: cartId,
883      cartToken: cartToken,
884      items: items,
885      discountCode: discountCode,
886      createdAt: Date.now(),
887      checkedAt: 0
888    });
889    var checkouts;
890
891    if (!pending) {
892      return;
893    }
894
895    checkouts = readPendingCheckouts().filter(function (checkout) {
896      return checkout.cartId !== pending.cartId;
897    });
898    checkouts.push(pending);
899    writePendingCheckouts(checkouts);
900  }
901
902  function consumePendingCheckout(cartId) {
903    var checkouts = readPendingCheckouts();
904    var claimed = checkouts.find(function (checkout) { return checkout.cartId === cartId; });
905
906    if (!claimed) {
907      return null;
908    }
909
910    writePendingCheckouts(checkouts.filter(function (checkout) {
911      return checkout.cartId !== cartId;
912    }));
913    return claimed;
914  }
915
916  function markPendingCheckoutChecked(cartId, checkedAt) {
917    var checkouts = readPendingCheckouts();
918
919    checkouts.forEach(function (checkout) {
920      if (checkout.cartId === cartId) {
921        checkout.checkedAt = checkedAt;
922      }
923    });
924    writePendingCheckouts(checkouts);
925  }
926
927  function removeCheckoutItems(purchasedItems) {
928    var remaining = readCart();
929
930    purchasedItems.forEach(function (purchased) {
931      var index = remaining.findIndex(function (item) {
932        if (purchased.entryId) {
933          return item.entryId === purchased.entryId;
934        }
935        return item.key === purchased.key;
936      });
937
938      if (index < 0) {
939        return;
940      }
941      remaining[index].quantity -= purchased.quantity;
942      if (remaining[index].quantity <= 0) {
943        remaining.splice(index, 1);
944      }
945    });
946    writeCart(remaining);
947  }
948
949  function withCheckoutReconciliationLock(callback) {
950    var lockName = "clc-commerce-checkout-reconciliation";
951    var leaseToken;
952    var leaseUntil;
953    var existing;
954
955    if (window.navigator && window.navigator.locks && typeof window.navigator.locks.request === "function") {
956      return window.navigator.locks.request(lockName, { mode: "exclusive", ifAvailable: true }, function (lock) {
957        return lock ? callback() : null;
958      });
959    }
960
961    leaseToken = createClientId("lock");
962    leaseUntil = Date.now() + 30000;
963    try {
964      existing = JSON.parse(window.localStorage.getItem(checkoutReconciliationLockKey) || "null");
965      if (existing && Number(existing.until) > Date.now()) {
966        return Promise.resolve(null);
967      }
968      window.localStorage.setItem(checkoutReconciliationLockKey, JSON.stringify({ token: leaseToken, until: leaseUntil }));
969      existing = JSON.parse(window.localStorage.getItem(checkoutReconciliationLockKey) || "null");
970      if (!existing || existing.token !== leaseToken) {
971        return Promise.resolve(null);
972      }
973    } catch (error) {
974      // In storage-restricted browsers the existing per-document guard remains.
975      return Promise.resolve().then(callback);
976    }
977
978    return Promise.resolve().then(callback).finally(function () {
979      try {
980        existing = JSON.parse(window.localStorage.getItem(checkoutReconciliationLockKey) || "null");
981        if (existing && existing.token === leaseToken) {
982          window.localStorage.removeItem(checkoutReconciliationLockKey);
983        }
984      } catch (error) {
985        // The short lease expires automatically if storage becomes unavailable.
986      }
987    });
988  }
989
990  function reconcilePendingCheckouts(force) {
991    if (checkoutReconciliationRunning || !settings.cartStatusUrl || !settings.nonce) {
992      return;
993    }
994
995    checkoutReconciliationRunning = true;
996    withCheckoutReconciliationLock(function () {
997      var now = Date.now();
998      var maxAge = 30 * 24 * 60 * 60 * 1000;
999      var completionWindow = 24 * 60 * 60 * 1000;
1000      var checkouts = readPendingCheckouts().filter(function (checkout) {
1001        return now - checkout.createdAt <= maxAge;
1002      });
1003      var due = checkouts.filter(function (checkout) {
1004        return Boolean(force) || !checkout.checkedAt || now - checkout.checkedAt >= 60000;
1005      });
1006
1007      writePendingCheckouts(checkouts);
1008      if (!due.length) {
1009        return null;
1010      }
1011
1012      return Promise.all(due.map(function (checkout) {
1013      return window.fetch(settings.cartStatusUrl, {
1014        method: "POST",
1015        credentials: "same-origin",
1016        headers: {
1017          "Content-Type": "application/json",
1018          "X-WP-Nonce": settings.nonce
1019        },
1020        body: JSON.stringify({ cart_id: checkout.cartId, cart_token: checkout.cartToken })
1021      }).then(function (response) {
1022        return response.json().catch(function () { return {}; }).then(function (payload) {
1023          if (!response.ok) {
1024            throw new Error(payload.message || "Cart status unavailable");
1025          }
1026          return payload;
1027        });
1028      }).then(function (payload) {
1029        var claimed;
1030
1031        if (payload.status === "unavailable" || payload.status === "completed_or_expired") {
1032          claimed = consumePendingCheckout(checkout.cartId);
1033          if (claimed && Date.now() - claimed.createdAt <= completionWindow) {
1034            removeCheckoutItems(claimed.items);
1035            clearDiscountCode(claimed.discountCode);
1036          }
1037          return;
1038        }
1039        if (payload.status === "active") {
1040          markPendingCheckoutChecked(checkout.cartId, Date.now());
1041        }
1042      }).catch(function () {
1043        markPendingCheckoutChecked(checkout.cartId, Date.now());
1044      });
1045      }));
1046    }).finally(function () {
1047      checkoutReconciliationRunning = false;
1048    });
1049  }
1050
1051  function addCartItem(item) {
1052    var normalized = normalizeCartItem(item);
1053    var items = readCart();
1054    var existing;
1055    var addedQuantity;
1056
1057    if (!normalized) {
1058      return false;
1059    }
1060
1061    existing = items.find(function (candidate) {
1062      return candidate.key === normalized.key;
1063    });
1064
1065    if (existing) {
1066      addedQuantity = Math.min(cartItemMaximum(existing), existing.quantity + normalized.quantity) - existing.quantity;
1067      if (addedQuantity <= 0) {
1068        return 0;
1069      }
1070      existing.quantity += addedQuantity;
1071    } else if (items.length < 20) {
1072      normalized.entryId = normalized.entryId || createClientId("cart");
1073      items.push(normalized);
1074      addedQuantity = normalized.quantity;
1075    } else {
1076      return 0;
1077    }
1078
1079    writeCart(items);
1080    return addedQuantity;
1081  }
1082
1083  function updateCartItem(key, quantity) {
1084    var items = readCart();
1085    var target = items.find(function (item) { return item.key === key; });
1086
1087    if (!target) {
1088      return;
1089    }
1090
1091    if (quantity <= 0) {
1092      items = items.filter(function (item) { return item.key !== key; });
1093    } else {
1094      target.quantity = Math.min(cartItemMaximum(target), Math.max(1, Number.parseInt(quantity, 10) || 1));
1095    }
1096
1097    writeCart(items);
1098  }
1099
1100  function cartAnalyticsItem(item, index, quantity) {
1101    return {
1102      item_id: item.productId || item.productHandle,
1103      item_name: item.title,
1104      item_variant: item.variantTitle,
1105      item_subscription_plan: item.sellingPlanId || undefined,
1106      price: item.price ? item.price.amount : 0,
1107      quantity: quantity == null ? item.quantity : quantity,
1108      index: index + 1
1109    };
1110  }
1111
1112  function pushCartQuantityEvent(eventName, item, quantity, index) {
1113    var count = Number(quantity);
1114    var price;
1115
1116    if (!eventName || !item || !Number.isFinite(count) || count <= 0) {
1117      return;
1118    }
1119
1120    price = item.price ? Number(item.price.amount) : 0;
1121    if (!Number.isFinite(price)) {
1122      price = 0;
1123    }
1124    sendAnalyticsEvent(eventName, {
1125      currency: item.price && item.price.currencyCode || settings.currency || "JPY",
1126      value: Number((price * count).toFixed(2)),
1127      items: [cartAnalyticsItem(item, Number.isFinite(index) ? index : 0, count)]
1128    });
1129  }
1130
1131  function openUtilityPanel(panelId, trigger) {
1132    document.dispatchEvent(new CustomEvent("clc:open-panel", {
1133      detail: { panelId: panelId, trigger: trigger || null }
1134    }));
1135  }
1136
1137  function revealGalleryThumbnail(button) {
1138    var strip = button && button.closest(".clc-gallery-thumbnails, .clc-gallery-thumbs");
1139    var stripRect;
1140    var buttonRect;
1141    var inset = 6;
1142
1143    if (!strip) {
1144      return;
1145    }
1146
1147    stripRect = strip.getBoundingClientRect();
1148    buttonRect = button.getBoundingClientRect();
1149
1150    if (buttonRect.left < stripRect.left + inset) {
1151      strip.scrollLeft -= stripRect.left + inset - buttonRect.left;
1152    } else if (buttonRect.right > stripRect.right - inset) {
1153      strip.scrollLeft += buttonRect.right - stripRect.right + inset;
1154    }
1155
1156    if (buttonRect.top < stripRect.top + inset) {
1157      strip.scrollTop -= stripRect.top + inset - buttonRect.top;
1158    } else if (buttonRect.bottom > stripRect.bottom - inset) {
1159      strip.scrollTop += buttonRect.bottom - stripRect.bottom + inset;
1160    }
1161  }
1162
1163  function initGallery(productElement) {
1164    var galleryMain = productElement.querySelector(".clc-gallery-main");
1165    var mainImage = productElement.querySelector(".clc-gallery-main img");
1166    var thumbnails = toArray(productElement.querySelectorAll(".clc-gallery-thumb"));
1167    var swipePointerId = null;
1168    var swipeStartX = null;
1169    var swipeStartY = null;
1170    var swipeThreshold = 42;
1171
1172    if (!mainImage || !thumbnails.length) {
1173      return;
1174    }
1175
1176    function activate(button) {
1177      var url = button.getAttribute("data-url");
1178      var alt = button.getAttribute("data-alt");
1179      var thumbnailImage = button.querySelector("img");
1180
1181      if (!url && thumbnailImage) {
1182        url = thumbnailImage.currentSrc || thumbnailImage.src;
1183      }
1184
1185      if (alt == null && thumbnailImage) {
1186        alt = thumbnailImage.alt;
1187      }
1188
1189      if (!url) {
1190        return;
1191      }
1192
1193      applyResponsiveProductImage(mainImage, url);
1194      mainImage.alt = alt || "";
1195
1196      thumbnails.forEach(function (thumbnail) {
1197        var active = thumbnail === button;
1198        thumbnail.classList.toggle("is-active", active);
1199        thumbnail.setAttribute("aria-pressed", active ? "true" : "false");
1200      });
1201
1202      revealGalleryThumbnail(button);
1203    }
1204
1205    function activeThumbnailIndex() {
1206      var activeIndex = thumbnails.findIndex(function (thumbnail) {
1207        return thumbnail.classList.contains("is-active") || thumbnail.getAttribute("aria-pressed") === "true";
1208      });
1209
1210      return activeIndex >= 0 ? activeIndex : 0;
1211    }
1212
1213    function activateRelative(direction) {
1214      var currentIndex = activeThumbnailIndex();
1215      var targetIndex = Math.max(0, Math.min(thumbnails.length - 1, currentIndex + direction));
1216
1217      if (targetIndex !== currentIndex) {
1218        activate(thumbnails[targetIndex]);
1219      }
1220    }
1221
1222    function beginSwipe(clientX, clientY) {
1223      swipeStartX = Number(clientX);
1224      swipeStartY = Number(clientY);
1225    }
1226
1227    function resetSwipe() {
1228      swipePointerId = null;
1229      swipeStartX = null;
1230      swipeStartY = null;
1231    }
1232
1233    function finishSwipe(clientX, clientY) {
1234      var deltaX;
1235      var deltaY;
1236      var horizontalDistance;
1237      var verticalDistance;
1238
1239      if (swipeStartX == null || swipeStartY == null) {
1240        resetSwipe();
1241        return;
1242      }
1243
1244      deltaX = Number(clientX) - swipeStartX;
1245      deltaY = Number(clientY) - swipeStartY;
1246      horizontalDistance = Math.abs(deltaX);
1247      verticalDistance = Math.abs(deltaY);
1248      resetSwipe();
1249
1250      if (horizontalDistance < swipeThreshold || horizontalDistance <= verticalDistance * 1.15) {
1251        return;
1252      }
1253
1254      activateRelative(deltaX < 0 ? 1 : -1);
1255    }
1256
1257    thumbnails.forEach(function (button) {
1258      button.addEventListener("focus", function () {
1259        revealGalleryThumbnail(button);
1260      });
1261
1262      button.addEventListener("click", function (event) {
1263        event.preventDefault();
1264        activate(button);
1265      });
1266
1267      button.addEventListener("keydown", function (event) {
1268        var index = thumbnails.indexOf(button);
1269        var targetIndex = index;
1270
1271        if (event.key === "ArrowRight" || event.key === "ArrowDown") {
1272          targetIndex = (index + 1) % thumbnails.length;
1273        } else if (event.key === "ArrowLeft" || event.key === "ArrowUp") {
1274          targetIndex = (index - 1 + thumbnails.length) % thumbnails.length;
1275        } else if (event.key === "Home") {
1276          targetIndex = 0;
1277        } else if (event.key === "End") {
1278          targetIndex = thumbnails.length - 1;
1279        } else {
1280          return;
1281        }
1282
1283        event.preventDefault();
1284        activate(thumbnails[targetIndex]);
1285        thumbnails[targetIndex].focus();
1286      });
1287    });
1288
1289    if (!galleryMain || thumbnails.length < 2) {
1290      return;
1291    }
1292
1293    galleryMain.setAttribute("data-clc-gallery-swipe-ready", "true");
1294    mainImage.setAttribute("draggable", "false");
1295
1296    if (window.PointerEvent) {
1297      galleryMain.addEventListener("pointerdown", function (event) {
1298        if (
1299          event.isPrimary === false ||
1300          (event.pointerType && event.pointerType !== "touch" && event.pointerType !== "pen")
1301        ) {
1302          return;
1303        }
1304
1305        swipePointerId = event.pointerId;
1306        beginSwipe(event.clientX, event.clientY);
1307
1308        if (typeof galleryMain.setPointerCapture === "function") {
1309          try {
1310            galleryMain.setPointerCapture(event.pointerId);
1311          } catch (error) {
1312            // Synthetic events and older WebViews can reject pointer capture.
1313          }
1314        }
1315      });
1316
1317      galleryMain.addEventListener("pointerup", function (event) {
1318        if (swipePointerId == null || event.pointerId !== swipePointerId) {
1319          return;
1320        }
1321
1322        finishSwipe(event.clientX, event.clientY);
1323      });
1324
1325      galleryMain.addEventListener("pointercancel", resetSwipe);
1326    } else {
1327      galleryMain.addEventListener("touchstart", function (event) {
1328        if (event.touches.length !== 1) {
1329          resetSwipe();
1330          return;
1331        }
1332
1333        beginSwipe(event.touches[0].clientX, event.touches[0].clientY);
1334      }, { passive: true });
1335
1336      galleryMain.addEventListener("touchend", function (event) {
1337        if (event.changedTouches.length !== 1) {
1338          resetSwipe();
1339          return;
1340        }
1341
1342        finishSwipe(event.changedTouches[0].clientX, event.changedTouches[0].clientY);
1343      }, { passive: true });
1344
1345      galleryMain.addEventListener("touchcancel", resetSwipe, { passive: true });
1346    }
1347  }
1348
1349  function initQuantity(productElement) {
1350    var input = productElement.querySelector(".clc-qty-input");
1351    var buttons = toArray(productElement.querySelectorAll("[data-clc-qty]"));
1352    var productMaximum = productElement.getAttribute("data-product-handle") === "trial-set" ? 1 : 20;
1353
1354    if (!input) {
1355      return function () { return 1; };
1356    }
1357
1358    function bounds() {
1359      var minimum = Number(input.min);
1360      var maximum = Number(input.max);
1361      var step = Number(input.step);
1362
1363      return {
1364        min: Number.isFinite(minimum) ? minimum : 1,
1365        max: Number.isFinite(maximum) && maximum > 0 ? Math.min(maximum, productMaximum) : productMaximum,
1366        step: Number.isFinite(step) && step > 0 ? step : 1
1367      };
1368    }
1369
1370    function normalizeQuantity(value) {
1371      var limits = bounds();
1372      var quantity = Number.parseInt(value, 10);
1373
1374      if (!Number.isFinite(quantity)) {
1375        quantity = limits.min;
1376      }
1377
1378      return Math.min(limits.max, Math.max(limits.min, quantity));
1379    }
1380
1381    function synchronizeButtons(quantity) {
1382      var limits = bounds();
1383
1384      buttons.forEach(function (button) {
1385        var direction = button.getAttribute("data-clc-qty");
1386        button.disabled = direction === "increase" ? quantity >= limits.max : quantity <= limits.min;
1387      });
1388    }
1389
1390    function commit(value) {
1391      var quantity = normalizeQuantity(value);
1392
1393      input.value = String(quantity);
1394      synchronizeButtons(quantity);
1395      input.dispatchEvent(new Event("input", { bubbles: true }));
1396    }
1397
1398    buttons.forEach(function (button) {
1399      button.addEventListener("click", function (event) {
1400        var limits = bounds();
1401        var direction = button.getAttribute("data-clc-qty");
1402        var delta = direction === "decrease" ? -limits.step : limits.step;
1403        event.preventDefault();
1404        commit(normalizeQuantity(input.value) + delta);
1405      });
1406    });
1407
1408    input.addEventListener("change", function () {
1409      commit(input.value);
1410    });
1411
1412    input.max = String(Math.min(Number(input.max) || productMaximum, productMaximum));
1413    commit(input.value);
1414
1415    return function () {
1416      return normalizeQuantity(input.value);
1417    };
1418  }
1419
1420  function initProduct(productElement) {
1421    if (productElement.getAttribute("data-clc-initialized") === "true") {
1422      return;
1423    }
1424
1425    productElement.setAttribute("data-clc-initialized", "true");
1426
1427    var product = parseProductData(productElement);
1428    var variants = connectionNodes(product && product.variants);
1429    var optionInputs = toArray(productElement.querySelectorAll(".clc-option-input[data-option-name]"));
1430    var planInputs = toArray(productElement.querySelectorAll(".clc-plan-input"));
1431    var priceCurrent = productElement.querySelector(".clc-price-current");
1432    var priceCompare = productElement.querySelector(".clc-price-compare");
1433    var priceNote = productElement.querySelector(".clc-price-note");
1434    var availability = productElement.querySelector(".clc-availability");
1435    var checkoutButton = productElement.querySelector(".clc-checkout-button");
1436    var purchaseForm = productElement.querySelector(".clc-purchase-form, .clc-product-form");
1437    var errorBox = productElement.querySelector(".clc-purchase-error");
1438    var productStatus = productElement.querySelector("[data-clc-product-status]");
1439    var mainImage = productElement.querySelector(".clc-gallery-main img");
1440    var currency;
1441    var currentVariant;
1442    var currentPricing;
1443    var busy = false;
1444    var hasRendered = false;
1445    var getQuantity;
1446
1447    initGallery(productElement);
1448    getQuantity = initQuantity(productElement);
1449
1450    if (!product || !variants.length) {
1451      if (checkoutButton) {
1452        checkoutButton.disabled = true;
1453      }
1454      return;
1455    }
1456
1457    currency = productCurrency(product, variants);
1458    currentVariant = preferredVariant(product, variants, productElement, optionInputs);
1459    synchronizeOptionInputs(optionInputs, currentVariant);
1460
1461    if (priceNote && !own.call(priceNote.dataset, "defaultText")) {
1462      priceNote.dataset.defaultText = priceNote.textContent || "";
1463    }
1464
1465    if (checkoutButton && !checkoutButton.dataset.defaultLabel) {
1466      checkoutButton.dataset.defaultLabel = checkoutButton.textContent.trim();
1467    }
1468
1469    if (errorBox && !errorBox.getAttribute("role")) {
1470      errorBox.setAttribute("role", "alert");
1471    }
1472
1473    function showError(message) {
1474      if (!errorBox) {
1475        return;
1476      }
1477
1478      setText(errorBox, message);
1479      setHidden(errorBox, !message);
1480    }
1481
1482    function selectedPlan() {
1483      return selectedPlanId(planInputs);
1484    }
1485
1486    function announceProduct(message) {
1487      setText(productStatus, message || "");
1488    }
1489
1490    function updateVariantImage(variant) {
1491      var image = productImage(variant);
1492
1493      if (!mainImage || !image || !image.url) {
1494        return;
1495      }
1496
1497      applyResponsiveProductImage(mainImage, image.url);
1498      mainImage.alt = image.altText || product.title || "";
1499      if (image.width > 0 && image.height > 0) {
1500        mainImage.width = image.width;
1501        mainImage.height = image.height;
1502      }
1503
1504      toArray(productElement.querySelectorAll(".clc-gallery-thumb")).forEach(function (thumbnail) {
1505        var active = thumbnail.getAttribute("data-url") === image.url;
1506        thumbnail.classList.toggle("is-active", active);
1507        thumbnail.setAttribute("aria-pressed", active ? "true" : "false");
1508        if (active) {
1509          revealGalleryThumbnail(thumbnail);
1510        }
1511      });
1512    }
1513
1514    function render() {
1515      var planId;
1516      var inventoryAvailable = isVariantAvailable(currentVariant);
1517      var available;
1518      var compareVisible;
1519      var note = "";
1520      var inactiveLabel;
1521      var planSync;
1522
1523      planSync = synchronizePlanInputs(
1524        planInputs,
1525        currentVariant,
1526        Boolean(product.requiresSellingPlan),
1527        !hasRendered
1528      );
1529      planId = selectedPlan();
1530      available = inventoryAvailable && Boolean(currentVariant && currentVariant.id) &&
1531        (!product.requiresSellingPlan || Boolean(planId));
1532      inactiveLabel = !currentVariant ?
1533        (labels.unavailable || "選択した組み合わせは購入できません。") :
1534        (inventoryAvailable && product.requiresSellingPlan && !planId ?
1535          (labels.selectPlan || "購入方法を選択してください") :
1536          (labels.soldOut || labels.unavailable || "現在購入できません"));
1537      currentPricing = variantPricing(currentVariant, planId, currency);
1538
1539      setText(priceCurrent, formatMoney(currentPricing.current));
1540
1541      compareVisible = Boolean(
1542        currentPricing.compare && currentPricing.current &&
1543        currentPricing.compare.amount > currentPricing.current.amount
1544      );
1545      setText(priceCompare, compareVisible ? formatMoney(currentPricing.compare) : "");
1546      setHidden(priceCompare, !compareVisible);
1547
1548      if (priceCurrent) {
1549        priceCurrent.classList.toggle("is-sale", compareVisible);
1550      }
1551
1552      if (currentPricing.perDelivery && currentPricing.later) {
1553        var orderCount = Number(currentPricing.adjustment && currentPricing.adjustment.orderCount) || 1;
1554        note = orderCount === 1 ?
1555          renderTemplate(labels.stagedPrice || "初回 {price} / 2回目以降 {later}(税込)", {
1556            price: formatMoney(currentPricing.perDelivery),
1557            later: formatMoney(currentPricing.later)
1558          }) :
1559          renderTemplate(labels.multiStagePrice || "1〜{count}回目 {price} / {next}回目以降 {later}(税込)", {
1560            count: orderCount,
1561            next: orderCount + 1,
1562            price: formatMoney(currentPricing.perDelivery),
1563            later: formatMoney(currentPricing.later)
1564          });
1565      } else if (currentPricing.perDelivery) {
1566        note = renderTemplate(labels.perDelivery || labels.perDeliveryPrice || "1回あたり {price}", {
1567          price: formatMoney(currentPricing.perDelivery)
1568        });
1569      } else if (currentPricing.allocation && currentPricing.allocation.sellingPlan) {
1570        note = currentPricing.allocation.sellingPlan.description || "";
1571      } else if (priceNote) {
1572        note = priceNote.dataset.defaultText || "";
1573      }
1574
1575      setText(priceNote, note);
1576      setHidden(priceNote, !note);
1577
1578      var summaryPanel = productElement.querySelector(".clc-subscription-summary");
1579      if (summaryPanel) {
1580        var summary = currentPricing.allocation && currentPricing.allocation.sellingPlan &&
1581          currentPricing.allocation.sellingPlan.purchaseSummary;
1582        setHidden(summaryPanel, !summary || !available);
1583        if (summary && available) {
1584          toArray(summaryPanel.querySelectorAll("[data-clc-summary]")).forEach(function (field) {
1585            var key = field.getAttribute("data-clc-summary");
1586            setText(field, key === "price" ? note : (summary[key] || ""));
1587          });
1588        }
1589      }
1590
1591      if (availability) {
1592        availability.classList.toggle("is-available", available);
1593        availability.classList.toggle("is-unavailable", !available);
1594        setText(availability, available ?
1595          (labels.inStock || labels.available || "在庫あり") :
1596          inactiveLabel);
1597      }
1598
1599      if (checkoutButton) {
1600        checkoutButton.disabled = busy || !available;
1601
1602        if (!busy) {
1603          setText(checkoutButton, available ?
1604            (checkoutButton.dataset.defaultLabel || labels.checkout || "購入手続きへ") :
1605            inactiveLabel);
1606        }
1607
1608        if (currentVariant && currentVariant.id != null) {
1609          checkoutButton.setAttribute("data-variant-id", String(currentVariant.id));
1610        } else {
1611          checkoutButton.removeAttribute("data-variant-id");
1612        }
1613      }
1614
1615      updateVariantImage(currentVariant);
1616      synchronizeVariantUrl(product, currentVariant, productElement);
1617
1618      if (hasRendered && planSync.selectionCleared) {
1619        announceProduct(labels.planChanged || "選択したコースでは購入方法が変わるため、購入方法を選び直してください。");
1620      }
1621
1622      hasRendered = true;
1623    }
1624
1625    function selectVariantFromInputs(pushEvent) {
1626      var matched = findSelectedVariant(variants, optionInputs);
1627
1628      showError("");
1629      announceProduct("");
1630      currentVariant = matched;
1631      render();
1632
1633      if (pushEvent && currentVariant) {
1634        pushEcommerce("select_variant", product, currentVariant, currentPricing, 1, selectedPlan());
1635      }
1636    }
1637
1638    optionInputs.forEach(function (input) {
1639      input.addEventListener("change", function () {
1640        selectVariantFromInputs(true);
1641      });
1642    });
1643
1644    planInputs.forEach(function (input) {
1645      input.addEventListener("change", function () {
1646        if (!input.checked || input.disabled) {
1647          return;
1648        }
1649
1650        showError("");
1651        announceProduct("");
1652        render();
1653        pushEcommerce("select_subscription_plan", product, currentVariant, currentPricing, 1, selectedPlan());
1654      });
1655    });
1656
1657    if (purchaseForm && checkoutButton) {
1658      purchaseForm.addEventListener("submit", function (event) {
1659        var quantity;
1660        var image;
1661        var allocation;
1662        var added;
1663
1664        event.preventDefault();
1665
1666        if (busy || !currentVariant || !isVariantAvailable(currentVariant)) {
1667          return;
1668        }
1669
1670        if (product.requiresSellingPlan && !selectedPlan()) {
1671          showError(labels.selectPlan || "購入方法を選択してください。");
1672          return;
1673        }
1674
1675        quantity = getQuantity();
1676
1677        if (!Number.isFinite(quantity) || quantity < 1) {
1678          showError(labels.quantityError || "数量を1以上にしてください。");
1679          return;
1680        }
1681
1682        image = productImage(currentVariant) || product.featuredImage || {};
1683        allocation = currentPricing && currentPricing.allocation;
1684        added = addCartItem({
1685          productHandle: String(product.handle || productElement.getAttribute("data-product-handle") || ""),
1686          productId: String(product.id || product.handle || ""),
1687          title: String(product.title || "商品"),
1688          variantId: String(currentVariant.id),
1689          variantTitle: String(currentVariant.title || ""),
1690          sellingPlanId: selectedPlan() || "",
1691          sellingPlanName: String(allocation && allocation.sellingPlan && allocation.sellingPlan.name || ""),
1692          quantity: quantity,
1693          price: currentPricing && currentPricing.current,
1694          imageUrl: String(image.url || ""),
1695          imageAlt: String(image.altText || product.title || "")
1696        });
1697
1698        if (!added) {
1699          showError(labels.cartFull || "カートに追加できる商品数の上限に達しました。");
1700          return;
1701        }
1702
1703        showError("");
1704        pushEcommerce("add_to_cart", product, currentVariant, currentPricing, added, selectedPlan());
1705        announceProduct(labels.addedToCart || "カートに追加しました。");
1706        setText(checkoutButton, labels.addedToCart || "カートに追加しました");
1707        openUtilityPanel("clc-cart-panel", checkoutButton);
1708
1709        window.setTimeout(function () {
1710          render();
1711        }, 900);
1712      });
1713    }
1714
1715    showError("");
1716    render();
1717    pushViewItemOnce(product, currentVariant, currentPricing);
1718  }
1719
1720  function initRichText(root) {
1721    if (!root || root.getAttribute("data-clc-rich-text-ready") === "true") {
1722      return;
1723    }
1724
1725    root.setAttribute("data-clc-rich-text-ready", "true");
1726
1727    toArray(root.querySelectorAll("table")).forEach(function (table, tableIndex) {
1728      var rows = toArray(table.rows || []);
1729      var caption = table.querySelector("caption");
1730      var heading = table.previousElementSibling;
1731      var headingText = "";
1732      var consistentShape;
1733      var columnCount;
1734      var firstRowIsHeader;
1735
1736      while (heading && !/^H[2-4]$/.test(heading.tagName)) {
1737        heading = heading.previousElementSibling;
1738      }
1739      if (heading) {
1740        headingText = String(heading.textContent || "").trim();
1741      }
1742      if (!caption) {
1743        caption = document.createElement("caption");
1744        caption.className = "clc-screen-reader-text";
1745        caption.textContent = headingText || (labels.tableScroll || "商品・サービス情報表") + " " + String(tableIndex + 1);
1746        table.insertBefore(caption, table.firstChild);
1747      }
1748
1749      columnCount = rows.length && rows[0].cells ? rows[0].cells.length : 0;
1750      consistentShape = columnCount > 0 && rows.every(function (row) {
1751        return row.cells && row.cells.length === columnCount;
1752      });
1753      if (!consistentShape || table.querySelector("th")) {
1754        return;
1755      }
1756
1757      function promoteCell(cell, scope) {
1758        var header = document.createElement("th");
1759        toArray(cell.attributes || []).forEach(function (attribute) {
1760          header.setAttribute(attribute.name, attribute.value);
1761        });
1762        header.setAttribute("scope", scope);
1763        while (cell.firstChild) {
1764          header.appendChild(cell.firstChild);
1765        }
1766        cell.parentNode.replaceChild(header, cell);
1767      }
1768
1769      firstRowIsHeader = Boolean(table.tHead) ||
1770        /background(?:-color)?\s*:/i.test(rows[0].getAttribute("style") || "") ||
1771        toArray(rows[0].cells).every(function (cell) {
1772          return Boolean(cell.querySelector("strong, b"));
1773        });
1774      if (firstRowIsHeader) {
1775        toArray(rows[0].cells).forEach(function (cell) {
1776          promoteCell(cell, "col");
1777        });
1778      }
1779      rows.slice(firstRowIsHeader ? 1 : 0).forEach(function (row) {
1780        if (row.cells && row.cells[0]) {
1781          promoteCell(row.cells[0], "row");
1782        }
1783      });
1784    });
1785
1786    var wrappers = toArray(root.querySelectorAll("table")).map(function (table) {
1787      var parent = table.parentElement;
1788      var wrapper;
1789
1790      if (parent && parent.classList.contains("clc-table-scroll")) {
1791        return parent;
1792      }
1793
1794      wrapper = document.createElement("div");
1795      wrapper.className = "clc-table-scroll";
1796      wrapper.setAttribute("data-clc-table-scroll", "true");
1797      table.parentNode.insertBefore(wrapper, table);
1798      wrapper.appendChild(table);
1799      return wrapper;
1800    });
1801
1802    function synchronizeTableAccess() {
1803      wrappers.forEach(function (wrapper) {
1804        var table = wrapper.querySelector("table");
1805        var caption = table && table.querySelector("caption");
1806        var isScrollable = wrapper.scrollWidth > wrapper.clientWidth + 1;
1807
1808        if (isScrollable) {
1809          wrapper.tabIndex = 0;
1810          wrapper.setAttribute("role", "region");
1811          wrapper.setAttribute(
1812            "aria-label",
1813            caption && caption.textContent.trim() ?
1814              caption.textContent.trim() :
1815              (labels.tableScroll || "商品情報表。横にスクロールできます。")
1816          );
1817        } else {
1818          wrapper.removeAttribute("tabindex");
1819          wrapper.removeAttribute("role");
1820          wrapper.removeAttribute("aria-label");
1821        }
1822      });
1823    }
1824
1825    synchronizeTableAccess();
1826
1827    if (typeof window.requestAnimationFrame === "function") {
1828      window.requestAnimationFrame(synchronizeTableAccess);
1829    }
1830
1831    if (wrappers.length) {
1832      window.addEventListener("resize", synchronizeTableAccess, { passive: true });
1833    }
1834  }
1835
1836  function initHorizontalScroll(root) {
1837    if (!root || root.getAttribute("data-clc-horizontal-scroll-ready") === "true") {
1838      return;
1839    }
1840
1841    root.setAttribute("data-clc-horizontal-scroll-ready", "true");
1842
1843    function synchronizeAccess() {
1844      var isScrollable = root.scrollWidth > root.clientWidth + 1;
1845
1846      if (isScrollable) {
1847        root.tabIndex = 0;
1848        root.setAttribute("role", "region");
1849        root.setAttribute(
1850          "aria-label",
1851          root.getAttribute("data-clc-scroll-label") || labels.horizontalScroll || "横にスクロールできます。"
1852        );
1853      } else {
1854        root.removeAttribute("tabindex");
1855        root.removeAttribute("role");
1856        root.removeAttribute("aria-label");
1857      }
1858    }
1859
1860    synchronizeAccess();
1861    if (typeof window.requestAnimationFrame === "function") {
1862      window.requestAnimationFrame(synchronizeAccess);
1863    }
1864    window.addEventListener("resize", synchronizeAccess, { passive: true });
1865  }
1866
1867  function controlFilterName(control) {
1868    var owner = control.hasAttribute("data-clc-filter") ? control : control.closest("[data-clc-filter]");
1869    return owner ? owner.getAttribute("data-clc-filter") : "";
1870  }
1871
1872  function filterControls(collection) {
1873    var controls = [];
1874
1875    toArray(collection.querySelectorAll("[data-clc-filter]")).forEach(function (element) {
1876      if (element.matches("input[type=\"checkbox\"], input[type=\"radio\"]")) {
1877        controls.push(element);
1878        return;
1879      }
1880
1881      toArray(element.querySelectorAll("input[type=\"checkbox\"], input[type=\"radio\"]")).forEac
1881h(function (input) {
1882        if (controls.indexOf(input) === -1) {
1883          controls.push(input);
1884        }
1885      });
1886    });
1887
1888    return controls;
1889  }
1890
1891  function numericCardValue(card, field) {
1892    var value = card.getAttribute("data-" + field) || "";
1893    var parsed = Number(String(value).replace(/[^0-9.-]/g, ""));
1894    return Number.isFinite(parsed) ? parsed : 0;
1895  }
1896
1897  function cardFieldValue(card, field) {
1898    var normalizedField = field === "availability" ? "available" : field;
1899    normalizedField = normalizedField === "type" ? "product-type" : normalizedField;
1900    var value = card.getAttribute("data-" + normalizedField);
1901
1902    if (value == null && normalizedField === "title") {
1903      var title = card.querySelector(".clc-card-title");
1904      value = title ? title.textContent : "";
1905    }
1906
1907    return String(value == null ? "" : value);
1908  }
1909
1910  function matchesPriceFilter(card, control) {
1911    var owner = control.hasAttribute("data-clc-filter") ? control : control.closest("[data-clc-filter]");
1912    var price = numericCardValue(card, "price");
1913    var minimum = Number(control.getAttribute("data-min") || (owner && owner.getAttribute("data-min")));
1914    var maximum = Number(control.getAttribute("data-max") || (owner && owner.getAttribute("data-max")));
1915    var value = normalize(control.value);
1916    var range;
1917
1918    if (!Number.isFinite(minimum)) {
1919      minimum = -Infinity;
1920    }
1921
1922    if (!Number.isFinite(maximum)) {
1923      maximum = Infinity;
1924    }
1925
1926    if (minimum !== -Infinity || maximum !== Infinity) {
1927      return price >= minimum && price <= maximum;
1928    }
1929
1930    range = value.match(/^(?:Â¥|ï¿¥)?([0-9.]*)\s*[-:]\s*(?:Â¥|ï¿¥)?([0-9.]*)$/);
1931    if (range) {
1932      minimum = range[1] ? Number(range[1]) : -Infinity;
1933      maximum = range[2] ? Number(range[2]) : Infinity;
1934      return price >= minimum && price <= maximum;
1935    }
1936
1937    if (value.indexOf("under-") === 0) {
1938      return price < Number(value.replace("under-", ""));
1939    }
1940
1941    if (value.indexOf("over-") === 0) {
1942      return price >= Number(value.replace("over-", ""));
1943    }
1944
1945    return normalize(cardFieldValue(card, "price")) === value;
1946  }
1947
1948  function cardMatchesControl(card, field, control) {
1949    var expected = normalize(control.value);
1950    var actual;
1951
1952    if (field === "price") {
1953      return matchesPriceFilter(card, control);
1954    }
1955
1956    if (field === "available" || field === "availability") {
1957      actual = normalize(cardFieldValue(card, "available"));
1958
1959      if (["true", "1", "yes", "available", "in-stock", "instock"].indexOf(expected) !== -1) {
1960        return ["true", "1", "yes", "available", "in-stock", "instock"].indexOf(actual) !== -1;
1961      }
1962
1963      if (["false", "0", "no", "sold-out", "soldout", "unavailable"].indexOf(expected) !== -1) {
1964        return ["false", "0", "no", "sold-out", "soldout", "unavailable"].indexOf(actual) !== -1;
1965      }
1966    }
1967
1968    actual = normalize(cardFieldValue(card, field));
1969    return actual === expected;
1970  }
1971
1972  function initCollection(collection) {
1973    if (collection.getAttribute("data-clc-initialized") === "true") {
1974      return;
1975    }
1976
1977    collection.setAttribute("data-clc-initialized", "true");
1978
1979    var cards = toArray(collection.querySelectorAll(".clc-product-card"));
1980    var controls = filterControls(collection);
1981    var sortControl = collection.querySelector("[data-clc-sort]");
1982    var resultCount = collection.querySelector("[data-clc-result-count]");
1983    var collectionStatus = collection.querySelector("[data-clc-collection-status]");
1984    var emptyState = collection.querySelector("[data-clc-empty]");
1985    var grid = collection.querySelector(".clc-product-grid") || (cards[0] && cards[0].parentElement);
1986    var filterToggle = collection.querySelector("[data-clc-filter-toggle]");
1987    var filterPanel = collection.querySelector("[data-clc-filter-panel]");
1988    var filterClose = collection.querySelector("[data-clc-filter-close]");
1989    var filterClear = collection.querySelector("[data-clc-filter-clear]");
1990    var filterCount = collection.querySelector("[data-clc-filter-count]");
1991    var filterMedia = window.matchMedia ? window.matchMedia("(max-width: 767px)") : null;
1992    var searchSummary = collection.querySelector("[data-clc-search-query]");
1993    var searchQuery = normalize(searchSummary ? searchSummary.getAttribute("data-clc-search-query") : "");
1994    var serverSearch = collection.getAttribute("data-clc-search-mode") === "server";
1995    var visibleCount = cards.length;
1996    var collator = typeof Intl !== "undefined" && Intl.Collator ?
1997      new Intl.Collator(locale, { sensitivity: "base", numeric: true }) : null;
1998
1999    cards.forEach(function (card, index) {
2000      card.setAttribute("data-clc-original-index", String(index));
2001    });
2002
2003    function isMobileFilter() {
2004      return filterMedia ? filterMedia.matches : window.innerWidth <= 767;
2005    }
2006
2007    function announceCollection(message) {
2008      setText(collectionStatus, message || "");
2009    }
2010
2011    function setFilterOpen(open, returnFocus) {
2012      var expanded = Boolean(open) && isMobileFilter();
2013
2014      if (!filterPanel || !filterToggle) {
2015        return;
2016      }
2017
2018      filterToggle.setAttribute("aria-expanded", expanded ? "true" : "false");
2019      filterPanel.classList.toggle("is-open", expanded);
2020      filterPanel.hidden = isMobileFilter() && !expanded;
2021
2022      if (expanded) {
2023        (filterClose || filterPanel).focus();
2024      } else if (returnFocus) {
2025        filterToggle.focus();
2026      }
2027    }
2028
2029    function synchronizeFilterViewport() {
2030      if (!filterPanel || !filterToggle) {
2031        return;
2032      }
2033
2034      if (isMobileFilter()) {
2035        setFilterOpen(filterToggle.getAttribute("aria-expanded") === "true", false);
2036      } else {
2037        filterToggle.setAttribute("aria-expanded", "false");
2038        filterPanel.classList.remove("is-open");
2039        filterPanel.hidden = false;
2040      }
2041    }
2042
2043    function activeGroups() {
2044      var groups = Object.create(null);
2045
2046      controls.forEach(function (control) {
2047        var field = controlFilterName(control);
2048
2049        if (!field || !control.checked) {
2050          return;
2051        }
2052
2053        if (!groups[field]) {
2054          groups[field] = [];
2055        }
2056
2057        groups[field].push(control);
2058      });
2059
2060      return groups;
2061    }
2062
2063    function applyFilters(shouldAnnounce) {
2064      var groups = activeGroups();
2065      var groupNames = Object.keys(groups);
2066      var visible = 0;
2067      var activeCount = controls.filter(function (control) { return control.checked; }).length;
2068
2069      cards.forEach(function (card) {
2070        var matchesSearch = serverSearch || !searchQuery || normalize([
2071          cardFieldValue(card, "title"),
2072          cardFieldValue(card, "type"),
2073          card.getAttribute("data-handle") || ""
2074        ].join(" ")).indexOf(searchQuery) !== -1;
2075        var matches = matchesSearch && groupNames.every(function (field) {
2076          return groups[field].some(function (control) {
2077            return cardMatchesControl(card, field, control);
2078          });
2079        });
2080
2081        card.hidden = !matches;
2082        card.setAttribute("aria-hidden", matches ? "false" : "true");
2083
2084        if (matches) {
2085          visible += 1;
2086        }
2087      });
2088
2089      if (resultCount) {
2090        setText(resultCount, renderTemplate(labels.resultCount || "{count}件の商品", { count: visible }));
2091      }
2092
2093      visibleCount = visible;
2094      setText(filterCount, String(activeCount));
2095      setHidden(filterCount, activeCount === 0);
2096      setHidden(filterClear, activeCount === 0);
2097
2098      setHidden(emptyState, visible !== 0);
2099
2100      if (shouldAnnounce) {
2101        announceCollection(renderTemplate(labels.filtered || "絞り込み結果は{count}商品です。", { count: visible }));
2102      }
2103    }
2104
2105    function compareText(left, right, field) {
2106      var first = cardFieldValue(left, field);
2107      var second = cardFieldValue(right, field);
2108
2109      if (collator) {
2110        return collator.compare(first, second);
2111      }
2112
2113      return first < second ? -1 : (first > second ? 1 : 0);
2114    }
2115
2116    function applySort(shouldAnnounce) {
2117      var value = normalize(sortControl ? sortControl.value : "featured");
2118      var ordered = cards.slice();
2119      var selectedOption = sortControl && sortControl.options ? sortControl.options[sortControl.selectedIndex] : null;
2120
2121      ordered.sort(function (left, right) {
2122        if (["price-ascending", "price_asc", "price-asc", "price-low-high"].indexOf(value) !== -1) {
2123          return numericCardValue(left, "price") - numericCardValue(right, "price");
2124        }
2125
2126        if (["price-descending", "price_desc", "price-desc", "price-high-low"].indexOf(value) !== -1) {
2127          return numericCardValue(right, "price") - numericCardValue(left, "price");
2128        }
2129
2130        if (["title-ascending", "title_asc", "title-asc", "a-z"].indexOf(value) !== -1) {
2131          return compareText(left, right, "title");
2132        }
2133
2134        if (["title-descending", "title_desc", "title-desc", "z-a"].indexOf(value) !== -1) {
2135          return compareText(right, left, "title");
2136        }
2137
2138        return Number(left.getAttribute("data-clc-original-index")) -
2139          Number(right.getAttribute("data-clc-original-index"));
2140      });
2141
2142      if (grid) {
2143        ordered.forEach(function (card) {
2144          grid.appendChild(card);
2145        });
2146      }
2147
2148      if (shouldAnnounce) {
2149        announceCollection(renderTemplate(labels.sorted || "{sort}に並べ替えました。{count}商品あります。", {
2150          sort: selectedOption ? selectedOption.textContent.trim() : "おすすめ順",
2151          count: visibleCount
2152        }));
2153      }
2154    }
2155
2156    function listName() {
2157      var listTitle = collection.querySelector(".clc-collection-title");
2158      return listTitle ? listTitle.textContent.trim() : "商品一覧";
2159    }
2160
2161    function cardItem(card) {
2162      var link = card.querySelector("[data-clc-product-link], .clc-card-link");
2163      var itemId = card.getAttribute("data-product-id") || card.getAttribute("data-handle") || "";
2164      var itemName = cardFieldValue(card, "title");
2165      var displayedCards = grid ? toArray(grid.querySelectorAll(".clc-product-card:not([hidden])")) : cards;
2166      var displayedIndex = displayedCards.indexOf(card);
2167
2168      if (!itemId && link) {
2169        try {
2170          var pathParts = new URL(link.href, window.location.href).pathname.split("/").filter(
2170Boolean);
2171          itemId = decodeURIComponent(pathParts[pathParts.length - 1] || "");
2172        } catch (error) {
2173          itemId = link.getAttribute("href") || itemName;
2174        }
2175      }
2176
2177      return {
2178        item_id: String(itemId || itemName),
2179        item_name: itemName,
2180        item_category: cardFieldValue(card, "type"),
2181        item_list_name: listName(),
2182        price: numericCardValue(card, "price"),
2183        index: displayedIndex >= 0 ? displayedIndex + 1 : 1
2184      };
2185    }
2186
2187    function pushCardSelection(card) {
2188
2189      sendAnalyticsEvent("select_item", {
2190        item_list_name: listName(),
2191        items: [cardItem(card)]
2192      });
2193    }
2194
2195    function pushViewItemListOnce() {
2196      var key;
2197      var viewed;
2198
2199      if (!cards.length) {
2200        return;
2201      }
2202
2203      key = String(window.location.pathname || "") + "::" + listName();
2204      viewed = window.__CLCCommerceViewedLists;
2205
2206      if (!viewed || typeof viewed.has !== "function" || typeof viewed.add !== "function") {
2207        viewed = new Set();
2208        window.__CLCCommerceViewedLists = viewed;
2209      }
2210
2211      if (viewed.has(key)) {
2212        return;
2213      }
2214
2215      viewed.add(key);
2216      sendAnalyticsEvent("view_item_list", {
2217        item_list_name: listName(),
2218        items: cards.map(cardItem)
2219      });
2220    }
2221
2222    controls.forEach(function (control) {
2223      control.addEventListener("change", function () {
2224        applyFilters(true);
2225      });
2226    });
2227
2228    if (sortControl) {
2229      sortControl.addEventListener("change", function () {
2230        applySort(true);
2231      });
2232    }
2233
2234    if (filterToggle && filterPanel) {
2235      filterToggle.addEventListener("click", function () {
2236        setFilterOpen(filterToggle.getAttribute("aria-expanded") !== "true", false);
2237      });
2238
2239      if (filterClose) {
2240        filterClose.addEventListener("click", function () {
2241          setFilterOpen(false, true);
2242        });
2243      }
2244
2245      collection.addEventListener("keydown", function (event) {
2246        if (event.key === "Escape" && filterToggle.getAttribute("aria-expanded") === "true") {
2247          setFilterOpen(false, true);
2248        }
2249      });
2250
2251      if (filterMedia && typeof filterMedia.addEventListener === "function") {
2252        filterMedia.addEventListener("change", synchronizeFilterViewport);
2253      } else if (filterMedia && typeof filterMedia.addListener === "function") {
2254        filterMedia.addListener(synchronizeFilterViewport);
2255      } else {
2256        window.addEventListener("resize", synchronizeFilterViewport, { passive: true });
2257      }
2258    }
2259
2260    if (filterClear) {
2261      filterClear.addEventListener("click", function () {
2262        controls.forEach(function (control) {
2263          control.checked = false;
2264        });
2265        applyFilters(true);
2266      });
2267    }
2268
2269    cards.forEach(function (card) {
2270      card.addEventListener("click", function (event) {
2271        if (event.target.closest("[data-clc-product-link], .clc-card-link")) {
2272          pushCardSelection(card);
2273        }
2274      });
2275    });
2276
2277    synchronizeFilterViewport();
2278    applySort(false);
2279    applyFilters(false);
2280    pushViewItemListOnce();
2281  }
2282
2283  function initHomeHero(hero) {
2284    if (hero.getAttribute("data-clc-initialized") === "true") {
2285      return;
2286    }
2287
2288    var viewport = hero.querySelector("[data-clc-home-hero-viewport]");
2289    var slides = toArray(hero.querySelectorAll("[data-clc-home-slide]"));
2290    var dots = toArray(hero.querySelectorAll("[data-clc-home-hero-dot]"));
2291    var previous = hero.querySelector("[data-clc-home-hero-previous]");
2292    var next = hero.querySelector("[data-clc-home-hero-next]");
2293    var status = hero.querySelector("[data-clc-home-hero-status]");
2294    var reducedMotion = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
2295    var currentIndex = 0;
2296    var scrollTimer = 0;
2297
2298    if (!viewport || !slides.length) {
2299      return;
2300    }
2301
2302    hero.setAttribute("data-clc-initialized", "true");
2303
2304    function nearestIndex() {
2305      var left = viewport.scrollLeft;
2306      var bestIndex = 0;
2307      var bestDistance = Infinity;
2308
2309      slides.forEach(function (slide, index) {
2310        var distance = Math.abs(slide.offsetLeft - left);
2311        if (distance < bestDistance) {
2312          bestDistance = distance;
2313          bestIndex = index;
2314        }
2315      });
2316
2317      return bestIndex;
2318    }
2319
2320    function updateState(index, announce) {
2321      currentIndex = Math.max(0, Math.min(slides.length - 1, index));
2322      slides.forEach(function (slide, slideIndex) {
2323        var active = slideIndex === currentIndex;
2324
2325        if (!active && slide.contains(document.activeElement)) {
2326          try {
2327            viewport.focus({ preventScroll: true });
2328          } catch (error) {
2329            viewport.focus();
2330          }
2331        }
2332        slide.setAttribute("aria-hidden", active ? "false" : "true");
2333        if (active) {
2334          slide.removeAttribute("inert");
2335        } else {
2336          slide.setAttribute("inert", "");
2337        }
2338      });
2339      dots.forEach(function (dot, dotIndex) {
2340        dot.setAttribute("aria-pressed", dotIndex === currentIndex ? "true" : "false");
2341      });
2342      if (previous) {
2343        previous.disabled = currentIndex === 0;
2344      }
2345      if (next) {
2346        next.disabled = currentIndex === slides.length - 1;
2347      }
2348      if (announce) {
2349        setText(status, String(currentIndex + 1) + " / " + String(slides.length));
2350      }
2351    }
2352
2353    function showSlide(index, announce) {
2354      var targetIndex = Math.max(0, Math.min(slides.length - 1, index));
2355      var target = slides[targetIndex];
2356
2357      updateState(targetIndex, announce);
2358      if (typeof viewport.scrollTo === "function") {
2359        viewport.scrollTo({
2360          left: target.offsetLeft,
2361          behavior: reducedMotion ? "auto" : "smooth"
2362        });
2363      } else {
2364        viewport.scrollLeft = target.offsetLeft;
2365      }
2366    }
2367
2368    dots.forEach(function (dot) {
2369      dot.addEventListener("click", function () {
2370        showSlide(Number(dot.getAttribute("data-clc-home-hero-dot")) || 0, true);
2371      });
2372    });
2373
2374    if (previous) {
2375      previous.addEventListener("click", function () {
2376        showSlide(currentIndex - 1, true);
2377      });
2378    }
2379
2380    if (next) {
2381      next.addEventListener("click", function () {
2382        showSlide(currentIndex + 1, true);
2383      });
2384    }
2385
2386    viewport.addEventListener("keydown", function (event) {
2387      if (event.key === "ArrowLeft" || event.key === "ArrowRight") {
2388        event.preventDefault();
2389        showSlide(currentIndex + (event.key === "ArrowRight" ? 1 : -1), true);
2390      }
2391    });
2392
2393    viewport.addEventListener("scroll", function () {
2394      window.clearTimeout(scrollTimer);
2395      scrollTimer = window.setTimeout(function () {
2396        var nextIndex = nearestIndex();
2397        updateState(nextIndex, nextIndex !== currentIndex);
2398      }, 120);
2399    }, { passive: true });
2400
2401    window.addEventListener("resize", function () {
2402      showSlide(currentIndex, false);
2403    }, { passive: true });
2404
2405    updateState(0, false);
2406  }
2407
2408  function initHomeTestimonials(testimonials) {
2409    if (testimonials.getAttribute("data-clc-initialized") === "true") {
2410      return;
2411    }
2412
2413    var slides = toArray(testimonials.querySelectorAll("[data-clc-home-testimonial-slide]"));
2414    var controls = toArray(testimonials.querySelectorAll("[data-clc-home-testimonial-control]"));
2415    var stage = testimonials.querySelector(".clc-home-testimonial-stage");
2416    var status = testimonials.querySelector("[data-clc-home-testimonial-status]");
2417    var reducedMotion = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
2418    var currentIndex = 0;
2419    var autoplayTimer = 0;
2420    var touchStartX = null;
2421
2422    if (!slides.length || !controls.length) {
2423      return;
2424    }
2425
2426    testimonials.setAttribute("data-clc-initialized", "true");
2427
2428    function normalizedIndex(index) {
2429      return (index + slides.length) % slides.length;
2430    }
2431
2432    function updateSlide(index, announce) {
2433      currentIndex = normalizedIndex(index);
2434
2435      slides.forEach(function (slide, slideIndex) {
2436        var selected = slideIndex === currentIndex;
2437        setHidden(slide, !selected);
2438        slide.classList.toggle("is-active", selected);
2439      });
2440
2441      controls.forEach(function (control) {
2442        var selected = Number(control.getAttribute("data-clc-home-testimonial-control")) === currentIndex;
2443        control.setAttribute("aria-current", selected ? "true" : "false");
2444      });
2445
2446      if (announce) {
2447        setText(status, String(currentIndex + 1) + " / " + String(slides.length));
2448      }
2449    }
2450
2451    function stopAutoplay() {
2452      if (autoplayTimer) {
2453        window.clearInterval(autoplayTimer);
2454        autoplayTimer = 0;
2455      }
2456    }
2457
2458    function startAutoplay() {
2459      stopAutoplay();
2460      if (reducedMotion || document.hidden) {
2461        return;
2462      }
2463      autoplayTimer = window.setInterval(function () {
2464        updateSlide(currentIndex + 1, false);
2465      }, 5000);
2466    }
2467
2468    function activateControl(control, announce) {
2469      updateSlide(Number(control.getAttribute("data-clc-home-testimonial-control")) || 0, announce);
2470      startAutoplay();
2471    }
2472
2473    controls.forEach(function (control) {
2474      control.addEventListener("click", function () {
2475        activateControl(control, true);
2476      });
2477
2478      control.addEventListener("keydown", function (event) {
2479        var group;
2480        var groupControls;
2481        var groupIndex;
2482        var nextIndex;
2483
2484        if (["ArrowLeft", "ArrowRight", "Home", "End"].indexOf(event.key) === -1) {
2485          return;
2486        }
2487
2488        group = control.parentElement;
2489        groupControls = toArray(group.querySelectorAll("[data-clc-home-testimonial-control]"));
2490        groupIndex = groupControls.indexOf(control);
2491        nextIndex = groupIndex;
2492
2493        if (event.key === "Home") {
2494          nextIndex = 0;
2495        } else if (event.key === "End") {
2496          nextIndex = groupControls.length - 1;
2497        } else {
2498          nextIndex = (groupIndex + (event.key === "ArrowRight" ? 1 : -1) + groupControls.length) % groupControls.length;
2499        }
2500
2501        event.preventDefault();
2502        groupControls[nextIndex].focus();
2503        activateControl(groupControls[nextIndex], true);
2504      });
2505    });
2506
2507    testimonials.addEventListener("pointerenter", stopAutoplay);
2508    testimonials.addEventListener("pointerleave", startAutoplay);
2509    testimonials.addEventListener("focusin", stopAutoplay);
2510    testimonials.addEventListener("focusout", function () {
2511      window.setTimeout(function () {
2512        if (!testimonials.contains(document.activeElement)) {
2513          startAutoplay();
2514        }
2515      }, 0);
2516    });
2517
2518    if (stage) {
2519      stage.addEventListener("touchstart", function (event) {
2520        touchStartX = event.touches && event.touches[0] ? event.touches[0].clientX : null;
2521        stopAutoplay();
2522      }, { passive: true });
2523
2524      stage.addEventListener("touchend", function (event) {
2525        var touchEndX = event.changedTouches && event.changedTouches[0] ? event.changedTouches[0].clientX : null;
2526        var distance = touchStartX !== null && touchEndX !== null ? touchEndX - touchStartX : 0;
2527
2528        if (Math.abs(distance) >= 40) {
2529          updateSlide(currentIndex + (distance < 0 ? 1 : -1), true);
2530        }
2531        touchStartX = null;
2532        startAutoplay();
2533      }, { passive: true });
2534    }
2535
2536    document.addEventListener("visibilitychange", function () {
2537      if (document.hidden) {
2538        stopAutoplay();
2539      } else {
2540        startAutoplay();
2541      }
2542    });
2543
2544    updateSlide(0, false);
2545    startAutoplay();
2546  }
2547
2548  function initWeeklyCountdown(countdown) {
2549    if (countdown.getAttribute("data-clc-initialized") === "true") {
2550      return;
2551    }
2552
2553    var days = countdown.querySelector("[data-clc-countdown-days]");
2554    var hours = countdown.querySelector("[data-clc-countdown-hours]");
2555    var minutes = countdown.querySelector("[data-clc-countdown-minutes]");
2556    var seconds = countdown.querySelector("[data-clc-countdown-seconds]");
2557    var timer = 0;
2558    var tokyoOffset = 9 * 60 * 60 * 1000;
2559
2560    if (!days || !hours || !minutes || !seconds) {
2561      return;
2562    }
2563
2564    countdown.setAttribute("data-clc-initialized", "true");
2565
2566    function pad(value) {
2567      return String(Math.max(0, value)).padStart(2, "0");
2568    }
2569
2570    function nextDeadline(now) {
2571      var tokyoNow = new Date(now.getTime() + tokyoOffset);
2572      var weekday = tokyoNow.getUTCDay();
2573      var daysAhead = (1 - weekday + 7) % 7;
2574      var tokyoMidnight = Date.UTC(tokyoNow.getUTCFullYear(), tokyoNow.getUTCMonth(), tokyoNow.getUTCDate());
2575      var deadline = tokyoMidnight + (daysAhead * 86400000) - tokyoOffset;
2576
2577      if (deadline <= now.getTime()) {
2578        deadline += 7 * 86400000;
2579      }
2580      return deadline;
2581    }
2582
2583    function updateCountdown() {
2584      var now = new Date();
2585      var remaining = Math.max(0, Math.floor((nextDeadline(now) - now.getTime()) / 1000));
2586      var remainingDays = Math.floor(remaining / 86400);
2587      var remainingHours = Math.floor((remaining % 86400) / 3600);
2588      var remainingMinutes = Math.floor((remaining % 3600) / 60);
2589      var remainingSeconds = remaining % 60;
2590
2591      setText(days, pad(remainingDays));
2592      setText(hours, pad(remainingHours));
2593      setText(minutes, pad(remainingMinutes));
2594      setText(seconds, pad(remainingSeconds));
2595      countdown.setAttribute(
2596        "aria-label",
2597        "次の月曜日0時まで" + String(remainingDays) + "日" + String(remainingHours) + "時間" + String(remainingMinutes) + "分"
2598      );
2599    }
2600
2601    function startCountdown() {
2602      if (timer) {
2603        window.clearInterval(timer);
2604      }
2605      updateCountdown();
2606      timer = window.setInterval(updateCountdown, 1000);
2607    }
2608
2609    startCountdown();
2610
2611    document.addEventListener("visibilitychange", function () {
2612      if (!document.hidden) {
2613        updateCountdown();
2614      }
2615    });
2616
2617    window.addEventListener("pagehide", function () {
2618      window.clearInterval(timer);
2619      timer = 0;
2620    });
2621    window.addEventListener("pageshow", function (event) {
2622      if (event.persisted) {
2623        startCountdown();
2624      }
2625    });
2626  }
2627
2628  function initHomeProductList(list) {
2629    if (list.getAttribute("data-clc-initialized") === "true") {
2630      return;
2631    }
2632
2633    var cards = toArray(list.querySelectorAll(".clc-product-card"));
2634    var listName = list.getAttribute("data-list-name") || "注目の商品";
2635    var viewed = window.__CLCCommerceViewedLists;
2636    var viewKey = String(window.location.pathname || "") + "::" + listName;
2637
2638    list.setAttribute("data-clc-initialized", "true");
2639
2640    function cardItem(card, index) {
2641      var itemId = card.getAttribute("data-product-id") || card.getAttribute("data-handle") || "";
2642      return {
2643        item_id: String(itemId || cardFieldValue(card, "title")),
2644        item_name: cardFieldValue(card, "title"),
2645        item_category: cardFieldValue(card, "type"),
2646        item_list_name: listName,
2647        price: numericCardValue(card, "price"),
2648        index: index + 1
2649      };
2650    }
2651
2652    cards.forEach(function (card, index) {
2653      card.addEventListener("click", function (event) {
2654        if (event.target.closest("[data-clc-product-link], .clc-card-link")) {
2655          sendAnalyticsEvent("select_item", {
2656            item_list_name: listName,
2657            items: [cardItem(card, index)]
2658          });
2659        }
2660      });
2661    });
2662
2663    if (!cards.length) {
2664      return;
2665    }
2666
2667    if (!viewed || typeof viewed.has !== "function" || typeof viewed.add !== "function") {
2668      viewed = new Set();
2669      window.__CLCCommerceViewedLists = viewed;
2670    }
2671
2672    if (!viewed.has(viewKey)) {
2673      viewed.add(viewKey);
2674      sendAnalyticsEvent("view_item_list", {
2675        item_list_name: listName,
2676        items: cards.map(cardItem)
2677      });
2678    }
2679  }
2680
2681  function focusableElements(panel) {
2682    return toArray(panel.querySelectorAll(
2683      "a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex='-1'])"
2684    )).filter(function (element) {
2685      return !element.hidden &&
2686        !element.closest("[hidden]") &&
2687        element.getAttribute("aria-hidden") !== "true" &&
2688        !element.classList.contains("clc-panel-backdrop");
2689    });
2690  }
2691
2692  function initUtilityPanels(storefront) {
2693    var panels = toArray(storefront.querySelectorAll("[data-clc-panel]"));
2694    var triggers = toArray(storefront.querySelectorAll("[data-clc-panel-open]"));
2695    var activePanel = null;
2696    var activeTrigger = null;
2697    var inertedBackground = [];
2698
2699    if (!panels.length || storefront.getAttribute("data-clc-panels-ready") === "true") {
2700      return;
2701    }
2702    storefront.setAttribute("data-clc-panels-ready", "true");
2703
2704    function setTriggerState(panelId, expanded) {
2705      triggers.forEach(function (trigger) {
2706        if (trigger.getAttribute("data-clc-panel-open") !== panelId) {
2707          return;
2708        }
2709        trigger.setAttribute("aria-controls", panelId);
2710        trigger.setAttribute("aria-expanded", expanded ? "true" : "false");
2711      });
2712    }
2713
2714    function restoreBackground() {
2715      inertedBackground.forEach(function (state) {
2716        if (state.hadInert) {
2717          state.element.setAttribute("inert", "");
2718        } else {
2719          state.element.removeAttribute("inert");
2720        }
2721        if (state.ariaHidden == null) {
2722          state.element.removeAttribute("aria-hidden");
2723        } else {
2724          state.element.setAttribute("aria-hidden", state.ariaHidden);
2725        }
2726      });
2727      inertedBackground = [];
2728    }
2729
2730    function isolatePanel(panel) {
2731      restoreBackground();
2732      toArray(storefront.children).forEach(function (element) {
2733        if (element === panel) {
2734          return;
2735        }
2736        inertedBackground.push({
2737          element: element,
2738          hadInert: element.hasAttribute("inert"),
2739          ariaHidden: element.getAttribute("aria-hidden")
2740        });
2741        element.setAttribute("inert", "");
2742        element.setAttribute("aria-hidden", "true");
2743      });
2744    }
2745
2746    function closePanel(returnFocus) {
2747      var trigger = activeTrigger;
2748      var panelId;
2749
2750      if (!activePanel) {
2751        return;
2752      }
2753      panelId = activePanel.id;
2754      activePanel.classList.remove("is-open");
2755      activePanel.hidden = true;
2756      activePanel = null;
2757      activeTrigger = null;
2758      document.body.classList.remove("clc-overlay-open");
2759      restoreBackground();
2760      setTriggerState(panelId, false);
2761      if (returnFocus && trigger && document.contains(trigger)) {
2762        trigger.focus();
2763      }
2764    }
2765
2766    function showPanel(panelId, trigger) {
2767      var panel = storefront.querySelector("#" + String(panelId || ""));
2768      var focusable;
2769
2770      if (!panel) {
2771        return;
2772      }
2773      if (activePanel && activePanel !== panel) {
2774        closePanel(false);
2775      }
2776      activePanel = panel;
2777      activeTrigger = trigger || null;
2778      panel.hidden = false;
2779      setTriggerState(panel.id, true);
2780      isolatePanel(panel);
2781      document.body.classList.add("clc-overlay-open");
2782      document.dispatchEvent(new CustomEvent("clc:panel-opened", {
2783        detail: { panelId: panel.id, panel: panel, trigger: activeTrigger }
2784      }));
2785      window.requestAnimationFrame(function () {
2786        panel.classList.add("is-open");
2787        focusable = focusableElements(panel);
2788        if (focusable.length) {
2789          focusable[0].focus();
2790        }
2791      });
2792    }
2793
2794    triggers.forEach(function (trigger) {
2795      var panelId = trigger.getAttribute("data-clc-panel-open");
2796      trigger.setAttribute("aria-controls", panelId);
2797      trigger.setAttribute("aria-expanded", "false");
2798      trigger.addEventListener("click", function () {
2799        showPanel(panelId, trigger);
2800      });
2801    });
2802
2803    panels.forEach(function (panel) {
2804      toArray(panel.querySelectorAll("[data-clc-panel-close]")).forEach(function (button) {
2805        button.addEventListener("click", function () {
2806          closePanel(true);
2807        });
2808      });
2809
2810      panel.addEventListener("keydown", function (event) {
2811        var focusable;
2812        var first;
2813        var last;
2814
2815        if (event.key === "Escape") {
2816          event.preventDefault();
2817          closePanel(true);
2818          return;
2819        }
2820        if (event.key !== "Tab") {
2821          return;
2822        }
2823        focusable = focusableElements(panel);
2824        if (!focusable.length) {
2825          return;
2826        }
2827        first = focusable[0];
2828        last = focusable[focusable.length - 1];
2829        if (event.shiftKey && document.activeElement === first) {
2830          event.preventDefault();
2831          last.focus();
2832        } else if (!event.shiftKey && document.activeElement === last) {
2833          event.preventDefault();
2834          first.focus();
2835        }
2836      });
2837    });
2838
2839    document.addEventListener("keydown", function (event) {
2840      if (event.key === "Escape" && activePanel) {
2841        event.preventDefault();
2842        closePanel(true);
2843      }
2844    });
2845
2846    document.addEventListener("clc:open-panel", function (event) {
2847      var detail = event.detail || {};
2848      showPanel(detail.panelId, detail.trigger);
2849    });
2850  }
2851
2852  function initCartReminder(reminder) {
2853    var storefront = reminder.closest(".clc-storefront");
2854    var image = reminder.querySelector("[data-clc-cart-reminder-image]");
2855    var media = reminder.querySelector(".clc-cart-reminder-media");
2856    var name = reminder.querySelector("[data-clc-cart-reminder-name]");
2857    var meta = reminder.querySelector("[data-clc-cart-reminder-meta]");
2858    var price = reminder.querySelector("[data-clc-cart-reminder-price]");
2859    var quantity = reminder.querySelector("[data-clc-cart-reminder-quantity]");
2860    var subtotal = reminder.querySelector("[data-clc-cart-reminder-subtotal]");
2861    var extra = reminder.querySelector("[data-clc-cart-reminder-extra]");
2862    var openCart = reminder.querySelector("[data-clc-cart-reminder-open-cart]");
2863    var initialItems = readCart();
2864    var seenKey = "clc_cart_reminder_seen_v1";
2865    var seenInMemory = false;
2866    var timer = null;
2867
2868    if (
2869      !storefront ||
2870      !name ||
2871      !openCart ||
2872      !initialItems.length ||
2873      reminder.getAttribute("data-clc-cart-reminder-ready") === "true"
2874    ) {
2875      return;
2876    }
2877    reminder.setAttribute("data-clc-cart-reminder-ready", "true");
2878
2879    function hasBeenSeen() {
2880      if (seenInMemory) {
2881        return true;
2882      }
2883      try {
2884        return window.sessionStorage.getItem(seenKey) === "1";
2885      } catch (error) {
2886        return false;
2887      }
2888    }
2889
2890    function markSeen() {
2891      seenInMemory = true;
2892      try {
2893        window.sessionStorage.setItem(seenKey, "1");
2894      } catch (error) {
2895        // A session-only in-memory flag still prevents repeated interruption.
2896      }
2897    }
2898
2899    function render() {
2900      var items = readCart();
2901      var item = items[0];
2902      var details = [];
2903      var total = 0;
2904      var currency = "JPY";
2905
2906      if (!item) {
2907        return false;
2908      }
2909
2910      setText(name, item.title);
2911      if (item.variantTitle && normalize(item.variantTitle) !== "default title") {
2912        details.push(item.variantTitle);
2913      }
2914      if (item.sellingPlanName) {
2915        details.push(item.sellingPlanName);
2916      }
2917      setText(meta, details.join(" / "));
2918      setHidden(meta, details.length === 0);
2919      setText(price, item.price ? formatMoney(item.price) : "—");
2920      setText(quantity, item.quantity);
2921
2922      if (image && media) {
2923        /* imageUrl は localStorage 復元値を含むため Shopify CDN の https URL のみ許可
2924           (responsiveShopifyImageUrl がホスト検証を兼ねる。2026-08-30総点検 L-8) */
2925        var safeImageSrc = item.imageUrl ? responsiveShopifyImageUrl(item.imageUrl, 192) : "";
2926        if (safeImageSrc) {
2927          image.src = safeImageSrc;
2928          image.alt = item.imageAlt || item.title;
2929          setHidden(image, false);
2930          setHidden(media, false);
2931        } else {
2932          image.removeAttribute("src");
2933          image.alt = "";
2934          setHidden(image, true);
2935          setHidden(media, true);
2936        }
2937      }
2938
2939      items.forEach(function (cartItem) {
2940        if (cartItem.price) {
2941          total += cartItem.price.amount * cartItem.quantity;
2942          currency = cartItem.price.currencyCode || currency;
2943        }
2944      });
2945      setText(subtotal, formatMoney({ amount: total, currencyCode: currency }));
2946      setText(extra, items.length > 1 ? "ほか" + String(items.length - 1) + "商品がカートに入っています。" : "");
2947      setHidden(extra, items.length <= 1);
2948      return true;
2949    }
2950
2951    function showReminder() {
2952      var visiblePanel;
2953
2954      timer = null;
2955      if (hasBeenSeen() || !render()) {
2956        return;
2957      }
2958      if (document.visibilityState === "hidden") {
2959        document.addEventListener("visibilitychange", function onVisibilityChange() {
2960          if (document.visibilityState !== "visible") {
2961            return;
2962          }
2963          document.removeEventListener("visibilitychange", onVisibilityChange);
2964          timer = window.setTimeout(showReminder, 1200);
2965        });
2966        return;
2967      }
2968
2969      visiblePanel = storefront.querySelector("[data-clc-panel]:not([hidden])");
2970      if (visiblePanel && visiblePanel !== reminder) {
2971        return;
2972      }
2973      markSeen();
2974      openUtilityPanel(reminder.id, null);
2975    }
2976
2977    openCart.addEventListener("click", function () {
2978      var cartTrigger = storefront.querySelector('[data-clc-panel-open="clc-cart-panel"]');
2979      var close = reminder.querySelector("[data-clc-panel-close]");
2980
2981      markSeen();
2982      if (close) {
2983        close.click();
2984      }
2985      window.setTimeout(function () {
2986        openUtilityPanel("clc-cart-panel", cartTrigger || null);
2987      }, 0);
2988    });
2989
2990    document.addEventListener("clc:panel-opened", function (event) {
2991      var detail = event.detail || {};
2992      if (detail.panelId === "clc-cart-panel") {
2993        markSeen();
2994      }
2995    });
2996
2997    document.addEventListener("clc:cart-changed", function () {
2998      if (readCart().length) {
2999        if (!reminder.hidden) {
3000          render();
3001        }
3002        return;
3003      }
3004      if (!reminder.hidden) {
3005        var close = reminder.querySelector("[data-clc-panel-close]");
3006        if (close) {
3007          close.click();
3008        }
3009      }
3010      if (timer) {
3011        window.clearTimeout(timer);
3012        timer = null;
3013      }
3014    });
3015
3016    timer = window.setTimeout(showReminder, 12000);
3017  }
3018
3019  function initCart(cart) {
3020    var list = cart.querySelector("[data-clc-cart-items]");
3021    var empty = cart.querySelector("[data-clc-cart-empty]");
3022    var summary = cart.querySelector("[data-clc-cart-summary]");
3023    var subtotal = cart.querySelector("[data-clc-cart-subtotal]");
3024    var checkout = cart.querySelector("[data-clc-cart-checkout]");
3025    var errorBox = cart.querySelector("[data-clc-cart-error]");
3026    var statusBox = cart.querySelector("[data-clc-cart-status]");
3027    var cartHeading = cart.querySelector("#clc-cart-title");
3028    var discountNotice = cart.querySelector("[data-clc-cart-discount]");
3029    var discountCodeText = cart.querySelector("[data-clc-cart-discount-code]");
3030    var busy = false;
3031    var preparedCheckout = null;
3032    var prepareController = null;
3033    var prepareSequence = 0;
3034    var lastViewedFingerprint = "";
3035    var lastRenderedItemsFingerprint = "";
3036    var pendingCartFocus = null;
3037
3038    if (!list || !checkout || cart.getAttribute("data-clc-cart-ready") === "true") {
3039      return;
3040    }
3041    cart.setAttribute("data-clc-cart-ready", "true");
3042
3043    function updateCounts(items) {
3044      var count = items.reduce(function (total, item) { return total + item.quantity; }, 0);
3045      toArray(document.querySelectorAll("[data-clc-cart-count]")).forEach(function (badge) {
3046        setText(badge, String(count));
3047        setHidden(badge, count === 0);
3048      });
3049    }
3050
3051    function makeText(tag, className, value) {
3052      var element = document.createElement(tag);
3053      element.className = className;
3054      setText(element, value);
3055      return element;
3056    }
3057
3058    function cartFingerprint(items, discountCode) {
3059      return JSON.stringify({
3060        items: items.map(function (item) {
3061          return [item.entryId || "", item.key, item.productHandle, item.variantId, item.sellingPlanId || "", item.quantity];
3062        }),
3063        discountCode: normalizeDiscountCode(discountCode == null ? activeDiscountCode() : discountCode)
3064      });
3065    }
3066
3067    function checkoutLines(items) {
3068      return items.map(function (item) {
3069        return {
3070          product_handle: item.productHandle,
3071          variant_id: item.variantId,
3072          selling_plan_id: item.sellingPlanId || "",
3073          quantity: item.quantity
3074        };
3075      });
3076    }
3077
3078    function pushViewCartOnce(items) {
3079      var fingerprint;
3080      var value;
3081      var currency;
3082
3083      if (!items.length) {
3084        return;
3085      }
3086      fingerprint = cartFingerprint(items);
3087      if (fingerprint === lastViewedFingerprint) {
3088        return;
3089      }
3090
3091      lastViewedFingerprint = fingerprint;
3092      value = items.reduce(function (total, item) {
3093        return total + (item.price ? item.price.amount * item.quantity : 0);
3094      }, 0);
3095      currency = items.find(function (item) { return item.price && item.price.currencyCode; });
3096      sendAnalyticsEvent("view_cart", {
3097        currency: currency && currency.price.currencyCode || settings.currency || "JPY",
3098        value: Number(value.toFixed(2)),
3099        items: items.map(function (item, index) {
3100          return cartAnalyticsItem(item, index);
3101        })
3102      });
3103    }
3104
3105    function invalidatePreparedCheckout() {
3106      prepareSequence += 1;
3107      if (prepareController && typeof prepareController.abort === "function") {
3108        prepareController.abort();
3109      }
3110      prepareController = null;
3111      preparedCheckout = null;
3112      busy = false;
3113    }
3114
3115    function itemsRenderFingerprint(items) {
3116      return JSON.stringify(items.map(function (item) {
3117        return [item.entryId, item.key, item.title, item.variantTitle, item.sellingPlanName, item.quantity, item.imageUrl, item.price];
3118      }));
3119    }
3120
3121    function restoreCartFocus(items) {
3122      var intent = pendingCartFocus;
3123      var targetRow;
3124      var target;
3125
3126      pendingCartFocus = null;
3127      if (!intent) {
3128        return;
3129      }
3130      targetRow = toArray(list.querySelectorAll("[data-cart-entry-id]")).find(function (row) {
3131        return row.getAttribute("data-cart-entry-id") === intent.entryId;
3132      });
3133      if (!targetRow && items.length) {
3134        targetRow = list.children[Math.min(intent.index, items.length - 1)] || null;
3135      }
3136      if (targetRow) {
3137        target = targetRow.querySelector('[data-cart-action="' + intent.action + '"]') || targetRow.querySelector("[data-cart-action]");
3138      }
3139      if (!target && cartHeading) {
3140        target = cartHeading;
3141      }
3142      if (target && typeof target.focus === "function") {
3143        target.focus({ preventScroll: true });
3144      }
3145      if (statusBox && intent.message) {
3146        setText(statusBox, "");
3147        if (typeof window.requestAnimationFrame === "function") {
3148          window.requestAnimationFrame(function () { setText(statusBox, intent.message); });
3149        } else {
3150          window.setTimeout(function () { setText(statusBox, intent.message); }, 0);
3151        }
3152      }
3153    }
3154
3155    function preparedLineForItem(item) {
3156      var lines = preparedCheckout && Array.isArray(preparedCheckout.currentLines) ? preparedCheckout.currentLines : [];
3157      return lines.find(function (line) {
3158        return line.variantId === item.variantId && line.sellingPlanId === (item.sellingPlanId || "");
3159      }) || null;
3160    }
3161
3162    function analyticsItemWithCurrentPrice(item, index) {
3163      var analyticsItem = cartAnalyticsItem(item, index);
3164      var currentLine = preparedLineForItem(item);
3165      if (currentLine && currentLine.price) {
3166        analyticsItem.price = currentLine.price.amount;
3167      }
3168      return analyticsItem;
3169    }
3170
3171    function render() {
3172      var items = readCart();
3173      var total = 0;
3174      var currency = "JPY";
3175      var renderFingerprint = itemsRenderFingerprint(items);
3176
3177      if (renderFingerprint !== lastRenderedItemsFingerprint) {
3178        while (list.firstChild) {
3179          list.removeChild(list.firstChild);
3180        }
3181        items.forEach(function (item) {
3182        var row = document.createElement("li");
3183        var media = document.createElement("div");
3184        var content = document.createElement("div");
3185        var details = [];
3186        var controls = document.createElement("div");
3187        var decrease = document.createElement("button");
3188        var quantity = makeText("span", "clc-cart-item-quantity", item.quantity);
3189        var increase = document.createElement("button");
3190        var remove = document.createElement("button");
3191
3192        row.className = "clc-cart-item";
3193        row.setAttribute("data-cart-key", item.key);
3194        row.setAttribute("data-cart-entry-id", item.entryId || "");
3195        media.className = "clc-cart-item-media";
3196        /* localStorage 復元値を含むため、ホスト検証を通らないURLは描画しない(L-8) */
3197        var safeRowImageSrc = item.imageUrl ? responsiveShopifyImageUrl(item.imageUrl, 192) : "";
3198        if (safeRowImageSrc) {
3199          var image = document.createElement("img");
3200          image.src = safeRowImageSrc;
3201          image.alt = item.imageAlt;
3202          image.width = 96;
3203          image.height = 96;
3204          image.loading = "lazy";
3205          media.appendChild(image);
3206        }
3207        content.className = "clc-cart-item-content";
3208        content.appendChild(makeText("h3", "clc-cart-item-title", item.title));
3209        if (item.variantTitle && normalize(item.variantTitle) !== "default title") {
3210          details.push(item.variantTitle);
3211        }
3212        if (item.sellingPlanName) {
3213          details.push(item.sellingPlanName);
3214        }
3215        if (details.length) {
3216          content.appendChild(makeText("p", "clc-cart-item-meta", details.join(" / ")));
3217        }
3218        content.appendChild(makeText("p", "clc-cart-item-price", item.price ? formatMoney(item.price) : "—"));
3219
3220        controls.className = "clc-cart-item-controls";
3221        decrease.type = "button";
3222        decrease.className = "clc-cart-item-step";
3223        decrease.setAttribute("data-cart-action", "decrease");
3224        decrease.setAttribute("aria-label", item.title + "の数量を減らす");
3225        setText(decrease, "−");
3226        increase.type = "button";
3227        increase.className = "clc-cart-item-step";
3228        increase.setAttribute("data-cart-action", "increase");
3229        increase.setAttribute("aria-label", item.title + "の数量を増やす");
3230        increase.disabled = item.quantity >= cartItemMaximum(item);
3231        setText(increase, "+");
3232        remove.type = "button";
3233        remove.className = "clc-cart-item-remove";
3234        remove.setAttribute("data-cart-action", "remove");
3235        remove.setAttribute("aria-label", item.title + "をカートから削除");
3236        setText(remove, labels.remove || "削除");
3237        controls.appendChild(decrease);
3238        controls.appendChild(quantity);
3239        controls.appendChild(increase);
3240        controls.appendChild(remove);
3241        content.appendChild(controls);
3242        row.appendChild(media);
3243        row.appendChild(content);
3244        list.appendChild(row);
3245
3246        });
3247        lastRenderedItemsFingerprint = renderFingerprint;
3248        restoreCartFocus(items);
3249      }
3250
3251      items.forEach(function (item, index) {
3252        var currentLine = preparedCheckout && preparedCheckout.fingerprint === cartFingerprint(items) ? preparedLineForItem(item) : null;
3253        var displayedPrice = currentLine && currentLine.price ? currentLine.price : item.price;
3254        var row = list.children[index];
3255        if (row) {
3256          // Preserve quantity controls and keyboard focus while applying the
3257          // same current Shopify prices used by the subtotal and Checkout event.
3258          setText(row.querySelector(".clc-cart-item-price"), displayedPrice ? formatMoney(displayedPrice) : "—");
3259        }
3260        if (item.price) {
3261          total += item.price.amount * item.quantity;
3262          currency = item.price.currencyCode || currency;
3263        }
3264      });
3265
3266      if (preparedCheckout && preparedCheckout.fingerprint === cartFingerprint(items) && preparedCheckout.totalPrice) {
3267        total = preparedCheckout.totalPrice.amount;
3268        currency = preparedCheckout.totalPrice.currencyCode;
3269      }
3270
3271      updateCounts(items);
3272      setHidden(empty, items.length > 0);
3273      setHidden(summary, items.length === 0);
3274      setText(subtotal, items.length ? formatMoney({ amount: total, currencyCode: currency }) : "—");
3275      var discountCode = activeDiscountCode();
3276      setText(discountCodeText, discountCode);
3277      setHidden(discountNotice, !discountCode || items.length === 0);
3278      var fingerprint = cartFingerprint(items);
3279      var checkoutReady = Boolean(
3280        preparedCheckout &&
3281        preparedCheckout.fingerprint === fingerprint &&
3282        preparedCheckout.checkoutUrl
3283      );
3284
3285      checkout.setAttribute("aria-disabled", busy || items.length === 0 || !checkoutReady ? "true" : "false");
3286      checkout.setAttribute("aria-busy", busy ? "true" : "false");
3287      if (!busy && checkoutReady) {
3288        // This must be the final Shopify Checkout URL before the user clicks.
3289        // Google's cross-domain linker decorates this real anchor; JavaScript
3290        // navigation to an asynchronously returned URL is not supported.
3291        checkout.href = preparedCheckout.checkoutUrl;
3292        checkout.removeAttribute("role"); // hrefがある間は本物のリンクとして通知する
3293      } else {
3294        checkout.removeAttribute("href");
3295        checkout.setAttribute("role", "button"); // href無しの<a>はロール不定になるため明示する
3296      }
3297      if (busy) {
3298        setText(checkout, labels.cartLoading || "購入画面を準備しています…");
3299      } else {
3300        setText(checkout, labels.checkout || "購入手続きへ");
3301      }
3302    }
3303
3304    function prepareCheckout(items) {
3305      var discountCode = activeDiscountCode();
3306      var fingerprint = cartFingerprint(items, discountCode);
3307      var requestBody = { lines: checkoutLines(items) };
3308      var requestId;
3309
3310      if (discountCode) {
3311        requestBody.discount_code = discountCode;
3312      }
3313
3314      if (!items.length || !settings.restUrl || !settings.nonce) {
3315        invalidatePreparedCheckout();
3316        render();
3317        return Promise.resolve(null);
3318      }
3319
3320      if (preparedCheckout && preparedCheckout.fingerprint === fingerprint) {
3321        busy = false;
3322        setHidden(errorBox, true);
3323        render();
3324        return Promise.resolve(preparedCheckout);
3325      }
3326
3327      requestId = ++prepareSequence;
3328      if (prepareController && typeof prepareController.abort === "function") {
3329        prepareController.abort();
3330      }
3331      prepareController = typeof window.AbortController === "function" ? new window.AbortController() : null;
3332      preparedCheckout = null;
3333      busy = true;
3334      setHidden(errorBox, true);
3335      render();
3336
3337      return window.fetch(settings.restUrl, {
3338        method: "POST",
3339        credentials: "same-origin",
3340        headers: {
3341          "Content-Type": "application/json",
3342          "X-WP-Nonce": settings.nonce
3343        },
3344        signal: prepareController ? prepareController.signal : undefined,
3345        body: JSON.stringify(requestBody)
3346      }).then(function (response) {
3347        return response.json().catch(function () { return {}; }).then(function (payload) {
3348          if (!response.ok) {
3349            throw new Error(payload.message || labels.checkoutError || "購入画面を準備できませんでした。");
3350          }
3351          return payload;
3352        });
3353      }).then(function (payload) {
3354        if (!payload.checkout_url) {
3355          throw new Error(labels.checkoutError || "購入画面を準備できませんでした。");
3356        }
3357        if (requestId !== prepareSequence || cartFingerprint(readCart()) !== fingerprint) {
3358          return null;
3359        }
3360
3361        var totalPrice = payload.cart_total && Number.isFinite(Number(payload.cart_total.amount)) ? {
3362          amount: Number(payload.cart_total.amount),
3363          currencyCode: String(payload.cart_total.currency_code || settings.currency || "JPY")
3364        } : null;
3365        var currentLines = (Array.isArray(payload.cart_lines) ? payload.cart_lines : []).map(function (line) {
3366          var amount = Number(line && line.price && line.price.amount);
3367          if (!line || !/^gid:\/\/shopify\/ProductVariant\/[1-9][0-9]*$/.test(String(line.variant_id || "")) || !Number.isFinite(amount)) {
3368            return null;
3369          }
3370          return {
3371            variantId: String(line.variant_id),
3372            sellingPlanId: String(line.selling_plan_id || ""),
3373            quantity: Math.max(1, Number.parseInt(line.quantity, 10) || 1),
3374            price: { amount: amount, currencyCode: String(line.price.currency_code || settings.currency || "JPY") }
3375          };
3376        }).filter(Boolean);
3377
3378        preparedCheckout = {
3379          fingerprint: fingerprint,
3380          checkoutUrl: String(payload.checkout_url),
3381          cartId: String(payload.cart_id || ""),
3382          cartToken: String(payload.cart_token || ""),
3383          discountCode: discountCode,
3384          items: items.slice(),
3385          totalPrice: totalPrice,
3386          currentLines: currentLines
3387        };
3388        prepareController = null;
3389        busy = false;
3390        render();
3391        return preparedCheckout;
3392      }).catch(function (error) {
3393        if (requestId !== prepareSequence) {
3394          return null;
3395        }
3396        prepareController = null;
3397        if (error && error.name === "AbortError") {
3398          return null;
3399        }
3400        preparedCheckout = null;
3401        busy = false;
3402        render();
3403        setText(errorBox, error && error.message ? error.message : (labels.checkoutError || "購入画面を準備できませんでした。"));
3404        setHidden(errorBox, false);
3405        return null;
3406      });
3407    }
3408
3409    list.addEventListener("click", function (event) {
3410      var button = event.target.closest("[data-cart-action]");
3411      var row = button && button.closest("[data-cart-key]");
3412      var key = row && row.getAttribute("data-cart-key");
3413      var items = readCart();
3414      var itemIndex = key && items.findIndex(function (candidate) { return candidate.key === key; });
3415      var item = Number.isFinite(itemIndex) && itemIndex >= 0 ? items[itemIndex] : null;
3416      var action;
3417      var nextQuantity;
3418      var delta;
3419
3420      if (!button || !item) {
3421        return;
3422      }
3423      action = button.getAttribute("data-cart-action");
3424      pendingCartFocus = {
3425        entryId: item.entryId || "",
3426        index: itemIndex,
3427        action: action === "remove" ? "decrease" : action,
3428        message: action === "remove" ? item.title + (labels.cartRemoved || "をカートから削除しました。") : item.title + (labels.cartQuantityChanged || "の数量を変更しました。")
3429      };
3430      if (action === "increase") {
3431        nextQuantity = Math.min(cartItemMaximum(item), item.quantity + 1);
3432        delta = nextQuantity - item.quantity;
3433        if (delta <= 0) {
3434          return;
3435        }
3436        updateCartItem(key, nextQuantity);
3437        pushCartQuantityEvent("add_to_cart", item, delta, itemIndex);
3438      } else if (action === "decrease") {
3439        nextQuantity = Math.max(0, item.quantity - 1);
3440        delta = item.quantity - nextQuantity;
3441        updateCartItem(key, nextQuantity);
3442        pushCartQuantityEvent("remove_from_cart", item, delta, itemIndex);
3443      } else {
3444        updateCartItem(key, 0);
3445        pushCartQuantityEvent("remove_from_cart", item, item.quantity, itemIndex);
3446      }
3447    });
3448
3449    checkout.addEventListener("click", function (event) {
3450      var items = readCart();
3451      var fingerprint = cartFingerprint(items);
3452      var handoff = preparedCheckout;
3453      var analyticsItems;
3454      var value;
3455      var currency;
3456
3457      event.preventDefault();
3458
3459      if (busy || !items.length) {
3460        return;
3461      }
3462
3463      if (!handoff || handoff.fingerprint !== fingerprint || !checkout.href) {
3464        prepareCheckout(items);
3465        return;
3466      }
3467
3468      // Do not stop propagation and do not replace this href. Google's linker
3469      // listens to the trusted click at document level and decorates the final
3470      // Shopify Checkout URL before the deferred navigation reads it.
3471      busy = true;
3472      setHidden(errorBox, true);
3473      checkout.setAttribute("aria-disabled", "true");
3474      checkout.setAttribute("aria-busy", "true");
3475      analyticsItems = items.map(function (item, index) {
3476        return analyticsItemWithCurrentPrice(item, index);
3477      });
3478      value = handoff.totalPrice ? handoff.totalPrice.amount : items.reduce(function (total, item) {
3479        return total + (item.price ? item.price.amount * item.quantity : 0);
3480      }, 0);
3481      currency = handoff.totalPrice ? handoff.totalPrice.currencyCode : ((items[0].price && items[0].price.currencyCode) || "JPY");
3482
3483      if (handoff.cartId && handoff.cartToken) {
3484        rememberPendingCheckout(handoff.cartId, handoff.cartToken, handoff.items, handoff.discountCode);
3485      }
3486      // Shopify Pixel owns the standard begin_checkout event when the hosted
3487      // Checkout actually starts. Keep this pre-navigation intent distinct so
3488      // one checkout is not counted once on WordPress and again on Shopify.
3489      sendAnalyticsEvent("checkout_click", {
3490        currency: currency,
3491        value: Number(value.toFixed(2)),
3492        items: analyticsItems
3493      }, function () {
3494        navigateToPreparedCheckout(checkout, handoff.checkoutUrl);
3495      });
3496    });
3497
3498    checkout.addEventListener("keydown", function (event) {
3499      if (
3500        (event.key === "Enter" || event.key === " ") &&
3501        checkout.getAttribute("aria-disabled") === "true" &&
3502        !busy &&
3503        readCart().length
3504      ) {
3505        event.preventDefault();
3506        prepareCheckout(readCart());
3507      }
3508    });
3509
3510    document.addEventListener("clc:cart-changed", function () {
3511      invalidatePreparedCheckout();
3512      render();
3513      if (!cart.hidden) {
3514        prepareCheckout(readCart());
3515      }
3516    });
3517    document.addEventListener("clc:panel-opened", function (event) {
3518      var detail = event.detail || {};
3519      if (detail.panelId === cart.id) {
3520        pushViewCartOnce(readCart());
3521        // Reset any prior `_gl` decoration to the validated raw Checkout URL.
3522        // The next trusted click then receives a fresh linker value.
3523        prepareCheckout(readCart());
3524      }
3525    });
3526    window.addEventListener("pageshow", function (event) {
3527      if (!event.persisted) {
3528        return;
3529      }
3530      invalidatePreparedCheckout();
3531      render();
3532      if (!cart.hidden) {
3533        prepareCheckout(readCart());
3534      }
3535    });
3536    render();
3537  }
3538
3539  function initQuiz(quiz) {
3540    var form = quiz.querySelector(".clc-quiz-form");
3541    var steps = toArray(quiz.querySelectorAll("[data-clc-quiz-step]"));
3542    var progress = quiz.querySelector("[data-clc-quiz-progress]");
3543    var progressTrack = quiz.querySelector("[data-clc-quiz-progress-track]");
3544    var progressBar = quiz.querySelector("[data-clc-quiz-progress-bar]");
3545    var back = quiz.querySelector("[data-clc-quiz-back]");
3546    var status = quiz.querySelector("[data-clc-quiz-status]");
3547    var result = quiz.querySelector("[data-clc-quiz-result]");
3548    var resultHeading = quiz.querySelector("[data-clc-quiz-result-heading]");
3549    var resultProfile = quiz.querySelector("[data-clc-quiz-result-profile]");
3550    var profileData = quiz.querySelector("[data-clc-quiz-profiles]");
3551    var productCards = toArray(quiz.querySelectorAll("[data-clc-quiz-product]"));
3552    var reset = quiz.querySelector("[data-clc-quiz-reset]");
3553    var current = 0;
3554    var advanceTimer = 0;
3555    var profiles = {};
3556
3557    if (!form || !steps.length || !result || quiz.getAttribute("data-clc-quiz-ready") === "true") {
3558      return;
3559    }
3560    quiz.setAttribute("data-clc-quiz-ready", "true");
3561
3562    try {
3563      profiles = JSON.parse((profileData && profileData.textContent) || "{}");
3564    } catch (error) {
3565      profiles = {};
3566    }
3567
3568    function progressPercent(index) {
3569      return Math.round(((index + 1) / steps.length) * 100);
3570    }
3571
3572    function showStep(index, focusQuestion) {
3573      var percent;
3574
3575      window.clearTimeout(advanceTimer);
3576      current = Math.max(0, Math.min(steps.length - 1, index));
3577      steps.forEach(function (step, stepIndex) { setHidden(step, stepIndex !== current); });
3578      setText(progress, String(current + 1));
3579      percent = progressPercent(current);
3580      if (progressTrack) {
3581        progressTrack.setAttribute("aria-valuenow", String(percent));
3582      }
3583      if (progressBar) {
3584        progressBar.style.width = String(percent) + "%";
3585      }
3586      if (back) {
3587        setHidden(back, current === 0);
3588      }
3589      setHidden(form, false);
3590      setHidden(result, true);
3591      steps[current].classList.remove("is-entering");
3592      window.requestAnimationFrame(function () {
3593        steps[current].classList.add("is-entering");
3594      });
3595      if (focusQuestion) {
3596        window.setTimeout(function () {
3597          var legend = steps[current] && steps[current].querySelector("legend");
3598          if (legend) {
3599            legend.focus({ preventScroll: true });
3600          }
3601        }, 0);
3602      }
3603    }
3604
3605    function selectedInputs() {
3606      return toArray(form.querySelectorAll("input:checked[data-quiz-scores]"));
3607    }
3608
3609    function calculateRanking() {
3610      var scores = Object.create(null);
3611      selectedInputs().forEach(function (input) {
3612        String(input.getAttribute("data-quiz-scores") || "").split(",").forEach(function (pair) {
3613          var parts = pair.split(":");
3614          var handle = parts[0];
3615          var score = Number(parts[1]) || 0;
3616          scores[handle] = (scores[handle] || 0) + score;
3617        });
3618      });
3619
3620      return productCards.map(function (card, index) {
3621        var handle = card.getAttribute("data-clc-quiz-product") || "";
3622        return { card: card, handle: handle, score: scores[handle] || 0, index: index };
3623      }).sort(function (a, b) {
3624        return b.score - a.score || a.index - b.index;
3625      });
3626    }
3627
3628    function showResults() {
3629      var ranked = calculateRanking().slice(0, 3);
3630      var taste = form.querySelector('[data-quiz-question="taste"]:checked');
3631      var roast = form.querySelector('[data-quiz-question="roast"]:checked');
3632      var profileKey = String((taste && taste.value) || "") + "_" + String((roast && roast.value) || "");
3633      var resultItems = [];
3634
3635      productCards.forEach(function (card) {
3636        setHidden(card, true);
3637        card.classList.remove("is-recommended");
3638        card.style.removeProperty("order");
3639      });
3640      ranked.forEach(function (item, index) {
3641        var image = item.card.querySelector("img");
3642        setHidden(item.card, false);
3643        item.card.classList.add("is-recommended");
3644        item.card.style.order = String(index + 1);
3645        if (image) {
3646          image.loading = "eager";
3647          image.fetchPriority = index === 0 ? "high" : "auto";
3648        }
3649        resultItems.push({
3650          item_id: item.card.getAttribute("data-product-id") || item.handle,
3651          item_name: item.card.getAttribute("data-product-title") || item.handle,
3652          price: Number(item.card.getAttribute("data-price-amount")) || 0,
3653          index: index + 1
3654        });
3655      });
3656      setText(resultProfile, profiles[profileKey] || "あなたにぴったりのコーヒーを見つけました。");
3657      setHidden(form, true);
3658      setHidden(result, false);
3659      sendAnalyticsEvent("view_item_list", {
3660        item_list_id: "coffee_finder_results",
3661        item_list_name: "コーヒー診断結果",
3662        items: resultItems
3663      });
3664      sendAnalyticsEvent("select_content", {
3665        content_type: "coffee_finder",
3666        item_id: ranked.length ? ranked[0].handle : ""
3667      });
3668      if (resultHeading) {
3669        resultHeading.focus({ preventScroll: true });
3670      }
3671    }
3672
3673    function handleAnswer(input) {
3674      var questionId = input.getAttribute("data-quiz-question") || "";
3675      var selectedLabel = input.closest("label");
3676
3677      window.clearTimeout(advanceTimer);
3678      if (selectedLabel) {
3679        selectedLabel.classList.add("is-selected");
3680      }
3681      setText(status, current < steps.length - 1 ? "回答を選択しました。次の質問へ進みます。" : "回答を選択しました。診断結果を表示します。");
3682      sendAnalyticsEvent("select_content", {
3683        content_type: "coffee_finder_answer",
3684        item_id: questionId + ":" + String(input.value || ""),
3685        index: current + 1
3686      });
3687      advanceTimer = window.setTimeout(function () {
3688        if (current < steps.length - 1) {
3689          showStep(current + 1, true);
3690        } else {
3691          showResults();
3692        }
3693      }, 320);
3694    }
3695
3696    form.addEventListener("change", function (event) {
3697      var input = event.target.closest('input[type="radio"][data-quiz-scores]');
3698      if (input && !input.disabled) {
3699        handleAnswer(input);
3700      }
3701    });
3702    form.addEventListener("submit", function (event) {
3703      event.preventDefault();
3704    });
3705    if (back) {
3706      back.addEventListener("click", function () {
3707        var target = Math.max(0, current - 1);
3708        steps.slice(target).forEach(function (step) {
3709          toArray(step.querySelectorAll('input[type="radio"]')).forEach(function (input) {
3710            input.checked = false;
3711          });
3712          toArray(step.querySelectorAll("label.is-selected")).forEach(function (label) {
3713            label.classList.remove("is-selected");
3714          });
3715        });
3716        showStep(target, true);
3717      });
3718    }
3719    result.addEventListener("click", function (event) {
3720      var addButton = event.target.closest("[data-clc-quiz-add]");
3721      var productLink = event.target.closest("[data-clc-quiz-product-link]");
3722      var card = event.target.closest("[data-clc-quiz-product]");
3723
3724      if (addButton && card) {
3725        var added = addCartItem({
3726          productHandle: card.getAttribute("data-product-handle"),
3727          productId: card.getAttribute("data-product-id"),
3728          title: card.getAttribute("data-product-title"),
3729          variantId: card.getAttribute("data-variant-id"),
3730          variantTitle: "",
3731          sellingPlanId: "",
3732          sellingPlanName: "",
3733          quantity: 1,
3734          price: {
3735            amount: Number(card.getAttribute("data-price-amount")) || 0,
3736            currencyCode: card.getAttribute("data-price-currency") || "JPY"
3737          },
3738          imageUrl: card.getAttribute("data-image-url"),
3739          imageAlt: card.getAttribute("data-product-title")
3740        });
3741
3742        if (added) {
3743          setText(addButton, labels.addedToCart || "カートに追加しました");
3744          sendAnalyticsEvent("add_to_cart", {
3745            currency: card.getAttribute("data-price-currency") || "JPY",
3746            value: (Number(card.getAttribute("data-price-amount")) || 0) * added,
3747            items: [{
3748              item_id: card.getAttribute("data-product-id") || card.getAttribute("data-product-handle"),
3749              item_name: card.getAttribute("data-product-title"),
3750              price: Number(card.getAttribute("data-price-amount")) || 0,
3751              quantity: added
3752            }]
3753          });
3754          openUtilityPanel("clc-cart-panel", addButton);
3755          window.setTimeout(function () {
3756            setText(addButton, labels.addToCart || "カートに追加");
3757          }, 1800);
3758        }
3759      } else if (productLink && card) {
3760        sendAnalyticsEvent("select_item", {
3761          item_list_id: "coffee_finder_results",
3762          item_list_name: "コーヒー診断結果",
3763          items: [{
3764            item_id: card.getAttribute("data-product-id") || card.getAttribute("data-product-handle"),
3765            item_name: card.getAttribute("data-product-title"),
3766            price: Number(card.getAttribute("data-price-amount")) || 0,
3767            quantity: 1
3768          }]
3769        });
3770      }
3771    });
3772    if (reset) {
3773      reset.addEventListener("click", function () {
3774        window.clearTimeout(advanceTimer);
3775        form.reset();
3776        toArray(form.querySelectorAll("label.is-selected")).forEach(function (label) {
3777          label.classList.remove("is-selected");
3778        });
3779        productCards.forEach(function (card) { setHidden(card, true); });
3780        setHidden(result, true);
3781        setHidden(form, false);
3782        showStep(0, true);
3783      });
3784    }
3785    showStep(0, false);
3786  }
3787
3788  function initNavigation(storefront, index) {
3789    var toggle = storefront.querySelector(".clc-nav-toggle");
3790    var panel = storefront.querySelector(".clc-nav-panel");
3791    var disclosures;
3792
3793    if (!toggle || !panel || toggle.getAttribute("data-clc-initialized") === "true") {
3794      return;
3795    }
3796
3797    toggle.setAttribute("data-clc-initialized", "true");
3798
3799    if (!panel.id) {
3800      panel.id = "clc-nav-panel-" + String(index + 1);
3801    }
3802
3803    toggle.setAttribute("aria-controls", panel.id);
3804
3805    if (!toggle.hasAttribute("aria-expanded")) {
3806      toggle.setAttribute("aria-expanded", "false");
3807    }
3808
3809    disclosures = toArray(panel.querySelectorAll(".clc-nav-disclosure"));
3810
3811    var media = window.matchMedia ? window.matchMedia("(max-width: 1439px)") : null;
3812
3813    function isMobile() {
3814      return media ? media.matches : window.innerWidth <= 1439;
3815    }
3816
3817    function closeDisclosures(except) {
3818      disclosures.forEach(function (disclosure) {
3819        if (disclosure !== except && disclosure.open) {
3820          disclosure.open = false;
3821        }
3822      });
3823    }
3824
3825    function setOpen(open) {
3826      var expanded = Boolean(open) && isMobile();
3827      toggle.setAttribute("aria-expanded", expanded ? "true" : "false");
3828      toggle.setAttribute("aria-label", expanded ?
3829        (labels.closeMenu || "メニューを閉じる") :
3830        (labels.openMenu || "メニューを開く"));
3831      panel.classList.toggle("is-open", expanded);
3832      setHidden(panel, isMobile() && !expanded);
3833      document.body.classList.toggle("clc-navigation-open", expanded);
3834      if (!expanded) {
3835        closeDisclosures(null);
3836      }
3837    }
3838
3839    function synchronizeViewport() {
3840      if (isMobile()) {
3841        setOpen(toggle.getAttribute("aria-expanded") === "true");
3842      } else {
3843        toggle.setAttribute("aria-expanded", "false");
3844        panel.classList.remove("is-open");
3845        panel.hidden = false;
3846        document.body.classList.remove("clc-navigation-open");
3847        closeDisclosures(null);
3848      }
3849    }
3850
3851    disclosures.forEach(function (disclosure) {
3852      var summary = disclosure.querySelector(":scope > summary");
3853
3854      if (!summary) {
3855        return;
3856      }
3857
3858      disclosure.addEventListener("toggle", function () {
3859        if (disclosure.open) {
3860          closeDisclosures(disclosure);
3861        }
3862      });
3863    });
3864
3865    toggle.addEventListener("click", function () {
3866      setOpen(toggle.getAttribute("aria-expanded") !== "true");
3867    });
3868
3869    panel.addEventListener("click", function (event) {
3870      if (event.target.closest("a, [data-clc-panel-open]")) {
3871        closeDisclosures(null);
3872        if (isMobile()) {
3873          setOpen(false);
3874        }
3875      }
3876    });
3877
3878    storefront.addEventListener("keydown", function (event) {
3879      var openDisclosure;
3880
3881      if (event.key !== "Escape") {
3882        return;
3883      }
3884
3885      openDisclosure = panel.querySelector(".clc-nav-disclosure[open]");
3886      if (openDisclosure) {
3887        event.preventDefault();
3888        openDisclosure.open = false;
3889        if (openDisclosure.querySelector(":scope > summary")) {
3890          openDisclosure.querySelector(":scope > summary").focus();
3891        }
3892        return;
3893      }
3894
3895      if (toggle.getAttribute("aria-expanded") === "true") {
3896        event.preventDefault();
3897        setOpen(false);
3898        toggle.focus();
3899      }
3900    });
3901
3902    storefront.addEventListener("focusin", function (event) {
3903      if (
3904        isMobile() &&
3905        toggle.getAttribute("aria-expanded") === "true" &&
3906        event.target !== toggle &&
3907        !panel.contains(event.target)
3908      ) {
3909        setOpen(false);
3910      }
3911    });
3912
3913    document.addEventListener("pointerdown", function (event) {
3914      if (panel.contains(event.target) || toggle.contains(event.target)) {
3915        return;
3916      }
3917
3918      closeDisclosures(null);
3919      if (isMobile() && toggle.getAttribute("aria-expanded") === "true") {
3920        setOpen(false);
3921      }
3922    });
3923
3924    if (media && typeof media.addEventListener === "function") {
3925      media.addEventListener("change", synchronizeViewport);
3926    } else if (media && typeof media.addListener === "function") {
3927      media.addListener(synchronizeViewport);
3928    } else {
3929      window.addEventListener("resize", synchronizeViewport);
3930    }
3931
3932    synchronizeViewport();
3933  }
3934
3935  function initContactFormA11y(storefront) {
3936    toArray(storefront.querySelectorAll(".clc-contact-form .wpcf7-checkbox, .clc-contact-form .wpcf7-radio")).forEach(function (group, index) {
3937      var paragraph;
3938      var groupLabel;
3939      var labelId;
3940
3941      if (group.getAttribute("data-clc-group-ready") === "true") {
3942        return;
3943      }
3944      group.setAttribute("data-clc-group-ready", "true");
3945      paragraph = group.closest("p");
3946      groupLabel = paragraph ? paragraph.querySelector(":scope > label") : null;
3947      if (!groupLabel || !String(groupLabel.textContent || "").trim()) {
3948        return;
3949      }
3950
3951      labelId = groupLabel.id || "clc-contact-group-label-" + String(index + 1);
3952      groupLabel.id = labelId;
3953      group.setAttribute("role", "group");
3954      group.setAttribute("aria-labelledby", labelId);
3955      if (group.classList.contains("wpcf7-validates-as-required")) {
3956        group.setAttribute("aria-required", "true");
3957      }
3958    });
3959  }
3960
3961  function initialize() {
3962    var storefronts = toArray(document.querySelectorAll(".clc-storefront"));
3963
3964    storefronts.forEach(function (storefront, index) {
3965      allIncluding(storefront, ".clc-product").forEach(initProduct);
3966      allIncluding(storefront, ".clc-collection").forEach(initCollection);
3967      allIncluding(storefront, ".clc-rich-text").forEach(initRichText);
3968      allIncluding(storefront, "[data-clc-horizontal-scroll]").forEach(initHorizontalScroll);
3969      allIncluding(storefront, "[data-clc-home-hero]").forEach(initHomeHero);
3970      allIncluding(storefront, "[data-clc-home-testimonials]").forEach(initHomeTestimonials);
3971      allIncluding(storefront, "[data-clc-weekly-countdown]").forEach(initWeeklyCountdown);
3972      allIncluding(storefront, "[data-clc-home-product-list]").forEach(initHomeProductList);
3973      allIncluding(storefront, "[data-clc-cart]").forEach(initCart);
3974      allIncluding(storefront, "[data-clc-quiz]").forEach(initQuiz);
3975      initContactFormA11y(storefront);
3976      initUtilityPanels(storefront);
3977      allIncluding(storefront, "[data-clc-cart-reminder]").forEach(initCartReminder);
3978      initNavigation(storefront, index);
3979    });
3980    reconcilePendingCheckouts();
3981  }
3982
3983  window.addEventListener("pageshow", function (event) {
3984    if (event.persisted) {
3985      reconcilePendingCheckouts(true);
3986    }
3987  });
3988  window.addEventListener("storage", function (event) {
3989    if (event.storageArea !== window.localStorage) {
3990      return;
3991    }
3992    if (event.key === cartStorageKey) {
3993      cartStorageAvailable = true;
3994      document.dispatchEvent(new CustomEvent("clc:cart-changed", {
3995        detail: { items: readCart(), source: "storage" }
3996      }));
3997    }
3998    if (event.key === pendingCheckoutStorageKey) {
3999      pendingCheckoutStorageAvailable = true;
4000      reconcilePendingCheckouts(true);
4001    }
4002  });
4003  ready(initialize);
4004}());

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.