PageSourceSearch

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

js coffee-labo.co.jp collected 2026-09-24 22:21:44 UTC 132,242 bytes, 3,991 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      if (availability) {
1579        availability.classList.toggle("is-available", available);
1580        availability.classList.toggle("is-unavailable", !available);
1581        setText(availability, available ?
1582          (labels.inStock || labels.available || "在庫あり") :
1583          inactiveLabel);
1584      }
1585
1586      if (checkoutButton) {
1587        checkoutButton.disabled = busy || !available;
1588
1589        if (!busy) {
1590          setText(checkoutButton, available ?
1591            (checkoutButton.dataset.defaultLabel || labels.checkout || "購入手続きへ") :
1592            inactiveLabel);
1593        }
1594
1595        if (currentVariant && currentVariant.id != null) {
1596          checkoutButton.setAttribute("data-variant-id", String(currentVariant.id));
1597        } else {
1598          checkoutButton.removeAttribute("data-variant-id");
1599        }
1600      }
1601
1602      updateVariantImage(currentVariant);
1603      synchronizeVariantUrl(product, currentVariant, productElement);
1604
1605      if (hasRendered && planSync.selectionCleared) {
1606        announceProduct(labels.planChanged || "選択したコースでは購入方法が変わるため、購入方法を選び直してください。");
1607      }
1608
1609      hasRendered = true;
1610    }
1611
1612    function selectVariantFromInputs(pushEvent) {
1613      var matched = findSelectedVariant(variants, optionInputs);
1614
1615      showError("");
1616      announceProduct("");
1617      currentVariant = matched;
1618      render();
1619
1620      if (pushEvent && currentVariant) {
1621        pushEcommerce("select_variant", product, currentVariant, currentPricing, 1, selectedPlan());
1622      }
1623    }
1624
1625    optionInputs.forEach(function (input) {
1626      input.addEventListener("change", function () {
1627        selectVariantFromInputs(true);
1628      });
1629    });
1630
1631    planInputs.forEach(function (input) {
1632      input.addEventListener("change", function () {
1633        if (!input.checked || input.disabled) {
1634          return;
1635        }
1636
1637        showError("");
1638        announceProduct("");
1639        render();
1640        pushEcommerce("select_subscription_plan", product, currentVariant, currentPricing, 1, selectedPlan());
1641      });
1642    });
1643
1644    if (purchaseForm && checkoutButton) {
1645      purchaseForm.addEventListener("submit", function (event) {
1646        var quantity;
1647        var image;
1648        var allocation;
1649        var added;
1650
1651        event.preventDefault();
1652
1653        if (busy || !currentVariant || !isVariantAvailable(currentVariant)) {
1654          return;
1655        }
1656
1657        if (product.requiresSellingPlan && !selectedPlan()) {
1658          showError(labels.selectPlan || "購入方法を選択してください。");
1659          return;
1660        }
1661
1662        quantity = getQuantity();
1663
1664        if (!Number.isFinite(quantity) || quantity < 1) {
1665          showError(labels.quantityError || "数量を1以上にしてください。");
1666          return;
1667        }
1668
1669        image = productImage(currentVariant) || product.featuredImage || {};
1670        allocation = currentPricing && currentPricing.allocation;
1671        added = addCartItem({
1672          productHandle: String(product.handle || productElement.getAttribute("data-product-handle") || ""),
1673          productId: String(product.id || product.handle || ""),
1674          title: String(product.title || "商品"),
1675          variantId: String(currentVariant.id),
1676          variantTitle: String(currentVariant.title || ""),
1677          sellingPlanId: selectedPlan() || "",
1678          sellingPlanName: String(allocation && allocation.sellingPlan && allocation.sellingPlan.name || ""),
1679          quantity: quantity,
1680          price: currentPricing && currentPricing.current,
1681          imageUrl: String(image.url || ""),
1682          imageAlt: String(image.altText || product.title || "")
1683        });
1684
1685        if (!added) {
1686          showError(labels.cartFull || "カートに追加できる商品数の上限に達しました。");
1687          return;
1688        }
1689
1690        showError("");
1691        pushEcommerce("add_to_cart", product, currentVariant, currentPricing, added, selectedPlan());
1692        announceProduct(labels.addedToCart || "カートに追加しました。");
1693        setText(checkoutButton, labels.addedToCart || "カートに追加しました");
1694        openUtilityPanel("clc-cart-panel", checkoutButton);
1695
1696        window.setTimeout(function () {
1697          render();
1698        }, 900);
1699      });
1700    }
1701
1702    showError("");
1703    render();
1704    pushViewItemOnce(product, currentVariant, currentPricing);
1705  }
1706
1707  function initRichText(root) {
1708    if (!root || root.getAttribute("data-clc-rich-text-ready") === "true") {
1709      return;
1710    }
1711
1712    root.setAttribute("data-clc-rich-text-ready", "true");
1713
1714    toArray(root.querySelectorAll("table")).forEach(function (table, tableIndex) {
1715      var rows = toArray(table.rows || []);
1716      var caption = table.querySelector("caption");
1717      var heading = table.previousElementSibling;
1718      var headingText = "";
1719      var consistentShape;
1720      var columnCount;
1721      var firstRowIsHeader;
1722
1723      while (heading && !/^H[2-4]$/.test(heading.tagName)) {
1724        heading = heading.previousElementSibling;
1725      }
1726      if (heading) {
1727        headingText = String(heading.textContent || "").trim();
1728      }
1729      if (!caption) {
1730        caption = document.createElement("caption");
1731        caption.className = "clc-screen-reader-text";
1732        caption.textContent = headingText || (labels.tableScroll || "商品・サービス情報表") + " " + String(tableIndex + 1);
1733        table.insertBefore(caption, table.firstChild);
1734      }
1735
1736      columnCount = rows.length && rows[0].cells ? rows[0].cells.length : 0;
1737      consistentShape = columnCount > 0 && rows.every(function (row) {
1738        return row.cells && row.cells.length === columnCount;
1739      });
1740      if (!consistentShape || table.querySelector("th")) {
1741        return;
1742      }
1743
1744      function promoteCell(cell, scope) {
1745        var header = document.createElement("th");
1746        toArray(cell.attributes || []).forEach(function (attribute) {
1747          header.setAttribute(attribute.name, attribute.value);
1748        });
1749        header.setAttribute("scope", scope);
1750        while (cell.firstChild) {
1751          header.appendChild(cell.firstChild);
1752        }
1753        cell.parentNode.replaceChild(header, cell);
1754      }
1755
1756      firstRowIsHeader = Boolean(table.tHead) ||
1757        /background(?:-color)?\s*:/i.test(rows[0].getAttribute("style") || "") ||
1758        toArray(rows[0].cells).every(function (cell) {
1759          return Boolean(cell.querySelector("strong, b"));
1760        });
1761      if (firstRowIsHeader) {
1762        toArray(rows[0].cells).forEach(function (cell) {
1763          promoteCell(cell, "col");
1764        });
1765      }
1766      rows.slice(firstRowIsHeader ? 1 : 0).forEach(function (row) {
1767        if (row.cells && row.cells[0]) {
1768          promoteCell(row.cells[0], "row");
1769        }
1770      });
1771    });
1772
1773    var wrappers = toArray(root.querySelectorAll("table")).map(function (table) {
1774      var parent = table.parentElement;
1775      var wrapper;
1776
1777      if (parent && parent.classList.contains("clc-table-scroll")) {
1778        return parent;
1779      }
1780
1781      wrapper = document.createElement("div");
1782      wrapper.className = "clc-table-scroll";
1783      wrapper.setAttribute("data-clc-table-scroll", "true");
1784      table.parentNode.insertBefore(wrapper, table);
1785      wrapper.appendChild(table);
1786      return wrapper;
1787    });
1788
1789    function synchronizeTableAccess() {
1790      wrappers.forEach(function (wrapper) {
1791        var table = wrapper.querySelector("table");
1792        var caption = table && table.querySelector("caption");
1793        var isScrollable = wrapper.scrollWidth > wrapper.clientWidth + 1;
1794
1795        if (isScrollable) {
1796          wrapper.tabIndex = 0;
1797          wrapper.setAttribute("role", "region");
1798          wrapper.setAttribute(
1799            "aria-label",
1800            caption && caption.textContent.trim() ?
1801              caption.textContent.trim() :
1802              (labels.tableScroll || "商品情報表。横にスクロールできます。")
1803          );
1804        } else {
1805          wrapper.removeAttribute("tabindex");
1806          wrapper.removeAttribute("role");
1807          wrapper.removeAttribute("aria-label");
1808        }
1809      });
1810    }
1811
1812    synchronizeTableAccess();
1813
1814    if (typeof window.requestAnimationFrame === "function") {
1815      window.requestAnimationFrame(synchronizeTableAccess);
1816    }
1817
1818    if (wrappers.length) {
1819      window.addEventListener("resize", synchronizeTableAccess, { passive: true });
1820    }
1821  }
1822
1823  function initHorizontalScroll(root) {
1824    if (!root || root.getAttribute("data-clc-horizontal-scroll-ready") === "true") {
1825      return;
1826    }
1827
1828    root.setAttribute("data-clc-horizontal-scroll-ready", "true");
1829
1830    function synchronizeAccess() {
1831      var isScrollable = root.scrollWidth > root.clientWidth + 1;
1832
1833      if (isScrollable) {
1834        root.tabIndex = 0;
1835        root.setAttribute("role", "region");
1836        root.setAttribute(
1837          "aria-label",
1838          root.getAttribute("data-clc-scroll-label") || labels.horizontalScroll || "横にスクロールできます。"
1839        );
1840      } else {
1841        root.removeAttribute("tabindex");
1842        root.removeAttribute("role");
1843        root.removeAttribute("aria-label");
1844      }
1845    }
1846
1847    synchronizeAccess();
1848    if (typeof window.requestAnimationFrame === "function") {
1849      window.requestAnimationFrame(synchronizeAccess);
1850    }
1851    window.addEventListener("resize", synchronizeAccess, { passive: true });
1852  }
1853
1854  function controlFilterName(control) {
1855    var owner = control.hasAttribute("data-clc-filter") ? control : control.closest("[data-clc-filter]");
1856    return owner ? owner.getAttribute("data-clc-filter") : "";
1857  }
1858
1859  function filterControls(collection) {
1860    var controls = [];
1861
1862    toArray(collection.querySelectorAll("[data-clc-filter]")).forEach(function (element) {
1863      if (element.matches("input[type=\"checkbox\"], input[type=\"radio\"]")) {
1864        controls.push(element);
1865        return;
1866      }
1867
1868      toArray(element.querySelectorAll("input[type=\"checkbox\"], input[type=\"radio\"]")).forEach(function (input) {
1869        if (controls.indexOf(input) === -1) {
1870          controls.push(input);
1871        }
1872      });
1873    });
1874
1875    return controls;
1876  }
1877
1878  function numericCardValue(card, field) {
1879    var value = card.getAttribute("data-" + field) || "";
1880    var parsed = Number(String(value).replace(/[^0-9.-]/g, ""));
1881    return Number.isFinite(parsed) ? parsed : 0;
1882  }
1883
1884  function cardFieldValue(card, field) {
1885    var normalizedField = field === "availability" ? "available" : field;
1886    normalizedField = normalizedField === "type" ? "product-type" : normalizedField;
1887    var value = card.getAttribute("data-" + normalizedField);
1888
1889    if (value == null && normalizedField === "title") {
1890      var title = card.querySelector(".clc-card-title");
1891      value = title ? title.textContent : "";
1892    }
1893
1894    return String(value == null ? "" : value);
1895  }
1896
1897  function matchesPriceFilter(card, control) {
1898    var owner = control.hasAttribute("data-clc-filter") ? control : control.closest("[data-clc-filter]");
1899    var price = numericCardValue(card, "price");
1900    var minimum = Number(control.getAttribute("data-min") || (owner && owner.getAttribute("data-min")));
1901    var maximum = Number(control.getAttribute("data-max") || (owner && owner.getAttribute("data-max")));
1902    var value = normalize(control.value);
1903    var range;
1904
1905    if (!Number.isFinite(minimum)) {
1906      minimum = -Infinity;
1907    }
1908
1909    if (!Number.isFinite(maximum)) {
1910      maximum = Infinity;
1911    }
1912
1913    if (minimum !== -Infinity || maximum !== Infinity) {
1914      return price >= minimum && price <= maximum;
1915    }
1916
1917    range = value.match(/^(?:Â¥|ï¿¥)?([0-9.]*)\s*[-:]\s*(?:Â¥|ï¿¥)?([0-9.]*)$/);
1918    if (range) {
1919      minimum = range[1] ? Number(range[1]) : -Infinity;
1920      maximum = range[2] ? Number(range[2]) : Infinity;
1921      return price >= minimum && price <= maximum;
1922    }
1923
1924    if (value.indexOf("under-") === 0) {
1925      return price < Number(value.replace("under-", ""));
1926    }
1927
1928    if (value.indexOf("over-") === 0) {
1929      return price >= Number(value.replace("over-", ""));
1930    }
1931
1932    return normalize(cardFieldValue(card, "price")) === value;
1933  }
1934
1935  function cardMatchesControl(card, field, control) {
1936    var expected = normalize(control.value);
1937    var actual;
1938
1939    if (field === "price") {
1940      return matchesPriceFilter(card, control);
1941    }
1942
1943    if (field === "available" || field === "availability") {
1944      actual = normalize(cardFieldValue(card, "available"));
1945
1946      if (["true", "1", "yes", "available", "in-stock", "instock"].indexOf(expected) !== -1) {
1947        return ["true", "1", "yes", "available", "in-stock", "instock"].indexOf(actual) !== -1;
1948      }
1949
1950      if (["false", "0", "no", "sold-out", "soldout", "unavailable"].indexOf(expected) !== -1) {
1951        return ["false", "0", "no", "sold-out", "soldout", "unavailable"].indexOf(actual) !== -1;
1952      }
1953    }
1954
1955    actual = normalize(cardFieldValue(card, field));
1956    return actual === expected;
1957  }
1958
1959  function initCollection(collection) {
1960    if (collection.getAttribute("data-clc-initialized") === "true") {
1961      return;
1962    }
1963
1964    collection.setAttribute("data-clc-initialized", "true");
1965
1966    var cards = toArray(collection.querySelectorAll(".clc-product-card"));
1967    var controls = filterControls(collection);
1968    var sortControl = collection.querySelector("[data-clc-sort]");
1969    var resultCount = collection.querySelector("[data-clc-result-count]");
1970    var collectionStatus = collection.querySelector("[data-clc-collection-status]");
1971    var emptyState = collection.querySelector("[data-clc-empty]");
1972    var grid = collection.querySelector(".clc-product-grid") || (cards[0] && cards[0].parentElement);
1973    var filterToggle = collection.querySelector("[data-clc-filter-toggle]");
1974    var filterPanel = collection.querySelector("[data-clc-filter-panel]");
1975    var filterClose = collection.querySelector("[data-clc-filter-close]");
1976    var filterClear = collection.querySelector("[data-clc-filter-clear]");
1977    var filterCount = collection.querySelector("[data-clc-filter-count]");
1978    var filterMedia = window.matchMedia ? window.matchMedia("(max-width: 767px)") : null;
1979    var searchSummary = collection.querySelector("[data-clc-search-query]");
1980    var searchQuery = normalize(searchSummary ? searchSummary.getAttribute("data-clc-search-query") : "");
1981    var serverSearch = collection.getAttribute("data-clc-search-mode") === "server";
1982    var visibleCount = cards.length;
1983    var collator = typeof Intl !== "undefined" && Intl.Collator ?
1984      new Intl.Collator(locale, { sensitivity: "base", numeric: true }) : null;
1985
1986    cards.forEach(function (card, index) {
1987      card.setAttribute("data-clc-original-index", String(index));
1988    });
1989
1990    function isMobileFilter() {
1991      return filterMedia ? filterMedia.matches : window.innerWidth <= 767;
1992    }
1993
1994    function announceCollection(message) {
1995      setText(collectionStatus, message || "");
1996    }
1997
1998    function setFilterOpen(open, returnFocus) {
1999      var expanded = Boolean(open) && isMobileFilter();
2000
2001      if (!filterPanel || !filterToggle) {
2002        return;
2003      }
2004
2005      filterToggle.setAttribute("aria-expanded", expanded ? "true" : "false");
2006      filterPanel.classList.toggle("is-open", expanded);
2007      filterPanel.hidden = isMobileFilter() && !expanded;
2008
2009      if (expanded) {
2010        (filterClose || filterPanel).focus();
2011      } else if (returnFocus) {
2012        filterToggle.focus();
2013      }
2014    }
2015
2016    function synchronizeFilterViewport() {
2017      if (!filterPanel || !filterToggle) {
2018        return;
2019      }
2020
2021      if (isMobileFilter()) {
2022        setFilterOpen(filterToggle.getAttribute("aria-expanded") === "true", false);
2023      } else {
2024        filterToggle.setAttribute("aria-expanded", "false");
2025        filterPanel.classList.remove("is-open");
2026        filterPanel.hidden = false;
2027      }
2028    }
2029
2030    function activeGroups() {
2031      var groups = Object.create(null);
2032
2033      controls.forEach(function (control) {
2034        var field = controlFilterName(control);
2035
2036        if (!field || !control.checked) {
2037          return;
2038        }
2039
2040        if (!groups[field]) {
2041          groups[field] = [];
2042        }
2043
2044        groups[field].push(control);
2045      });
2046
2047      return groups;
2048    }
2049
2050    function applyFilters(shouldAnnounce) {
2051      var groups = activeGroups();
2052      var groupNames = Object.keys(groups);
2053      var visible = 0;
2054      var activeCount = controls.filter(function (control) { return control.checked; }).length;
2055
2056      cards.forEach(function (card) {
2057        var matchesSearch = serverSearch || !searchQuery || normalize([
2058          cardFieldValue(card, "title"),
2059          cardFieldValue(card, "type"),
2060          card.getAttribute("data-handle") || ""
2061        ].join(" ")).indexOf(searchQuery) !== -1;
2062        var matches = matchesSearch && groupNames.every(function (field) {
2063          return groups[field].some(function (control) {
2064            return cardMatchesControl(card, field, control);
2065          });
2066        });
2067
2068        card.hidden = !matches;
2069        card.setAttribute("aria-hidden", matches ? "false" : "true");
2070
2071        if (matches) {
2072          visible += 1;
2073        }
2074      });
2075
2076      if (resultCount) {
2077        setText(resultCount, renderTemplate(labels.resultCount || "{count}件の商品", { count: visible }));
2078      }
2079
2080      visibleCount = visible;
2081      setText(filterCount, String(activeCount));
2082      setHidden(filterCount, activeCount === 0);
2083      setHidden(filterClear, activeCount === 0);
2084
2085      setHidden(emptyState, visible !== 0);
2086
2087      if (shouldAnnounce) {
2088        announceCollection(renderTemplate(labels.filtered || "絞り込み結果は{count}商品です。", { count: visible }));
2089      }
2090    }
2091
2092    function compareText(left, right, field) {
2093      var first = cardFieldValue(left, field);
2094      var second = cardFieldValue(right, field);
2095
2096      if (collator) {
2097        return collator.compare(first, second);
2098      }
2099
2100      return first < second ? -1 : (first > second ? 1 : 0);
2101    }
2102
2103    function applySort(shouldAnnounce) {
2104      var value = normalize(sortControl ? sortControl.value : "featured");
2105      var ordered = cards.slice();
2106      var selectedOption = sortControl && sortControl.options ? sortControl.options[sortControl.selectedIndex] : null;
2107
2108      ordered.sort(function (left, right) {
2109        if (["price-ascending", "price_asc", "price-asc", "price-low-high"].indexOf(value) !== -1) {
2110          return numericCardValue(left, "price") - numericCardValue(right, "price");
2111        }
2112
2113        if (["price-descending", "price_desc", "price-desc", "price-high-low"].indexOf(value) !== -1) {
2114          return numericCardValue(right, "price") - numericCardValue(left, "price");
2115        }
2116
2117        if (["title-ascending", "title_asc", "title-asc", "a-z"].indexOf(value) !== -1) {
2118          return compareText(left, right, "title");
2119        }
2120
2121        if (["title-descending", "title_desc", "title-desc", "z-a"].indexOf(value) !== -1) {
2122          return compareText(right, left, "title");
2123        }
2124
2125        return Number(left.getAttribute("data-clc-original-index")) -
2126          Number(right.getAttribute("data-clc-original-index"));
2127      });
2128
2129      if (grid) {
2130        ordered.forEach(function (card) {
2131          grid.appendChild(card);
2132        });
2133      }
2134
2135      if (shouldAnnounce) {
2136        announceCollection(renderTemplate(labels.sorted || "{sort}に並べ替えました。{count}商品あります。", {
2137          sort: selectedOption ? selectedOption.textContent.trim() : "おすすめ順",
2138          count: visibleCount
2139        }));
2140      }
2141    }
2142
2143    function listName() {
2144      var listTitle = collection.querySelector(".clc-collection-title");
2145      return listTitle ? listTitle.textContent.trim() : "商品一覧";
2146    }
2147
2148    function cardItem(card) {
2149      var link = card.querySelector("[data-clc-product-link], .clc-card-link");
2150      var itemId = card.getAttribute("data-product-id") || card.getAttribute("data-handle") || "";
2151      var itemName = cardFieldValue(card, "title");
2152      var displayedCards = grid ? toArray(grid.querySelectorAll(".clc-product-card:not([hidden])")) : cards;
2153      var displayedIndex = displayedCards.indexOf(card);
2154
2155      if (!itemId && link) {
2156        try {
2157          var pathParts = new URL(link.href, window.location.href).pathname.split("/").filter(
2157Boolean);
2158          itemId = decodeURIComponent(pathParts[pathParts.length - 1] || "");
2159        } catch (error) {
2160          itemId = link.getAttribute("href") || itemName;
2161        }
2162      }
2163
2164      return {
2165        item_id: String(itemId || itemName),
2166        item_name: itemName,
2167        item_category: cardFieldValue(card, "type"),
2168        item_list_name: listName(),
2169        price: numericCardValue(card, "price"),
2170        index: displayedIndex >= 0 ? displayedIndex + 1 : 1
2171      };
2172    }
2173
2174    function pushCardSelection(card) {
2175
2176      sendAnalyticsEvent("select_item", {
2177        item_list_name: listName(),
2178        items: [cardItem(card)]
2179      });
2180    }
2181
2182    function pushViewItemListOnce() {
2183      var key;
2184      var viewed;
2185
2186      if (!cards.length) {
2187        return;
2188      }
2189
2190      key = String(window.location.pathname || "") + "::" + listName();
2191      viewed = window.__CLCCommerceViewedLists;
2192
2193      if (!viewed || typeof viewed.has !== "function" || typeof viewed.add !== "function") {
2194        viewed = new Set();
2195        window.__CLCCommerceViewedLists = viewed;
2196      }
2197
2198      if (viewed.has(key)) {
2199        return;
2200      }
2201
2202      viewed.add(key);
2203      sendAnalyticsEvent("view_item_list", {
2204        item_list_name: listName(),
2205        items: cards.map(cardItem)
2206      });
2207    }
2208
2209    controls.forEach(function (control) {
2210      control.addEventListener("change", function () {
2211        applyFilters(true);
2212      });
2213    });
2214
2215    if (sortControl) {
2216      sortControl.addEventListener("change", function () {
2217        applySort(true);
2218      });
2219    }
2220
2221    if (filterToggle && filterPanel) {
2222      filterToggle.addEventListener("click", function () {
2223        setFilterOpen(filterToggle.getAttribute("aria-expanded") !== "true", false);
2224      });
2225
2226      if (filterClose) {
2227        filterClose.addEventListener("click", function () {
2228          setFilterOpen(false, true);
2229        });
2230      }
2231
2232      collection.addEventListener("keydown", function (event) {
2233        if (event.key === "Escape" && filterToggle.getAttribute("aria-expanded") === "true") {
2234          setFilterOpen(false, true);
2235        }
2236      });
2237
2238      if (filterMedia && typeof filterMedia.addEventListener === "function") {
2239        filterMedia.addEventListener("change", synchronizeFilterViewport);
2240      } else if (filterMedia && typeof filterMedia.addListener === "function") {
2241        filterMedia.addListener(synchronizeFilterViewport);
2242      } else {
2243        window.addEventListener("resize", synchronizeFilterViewport, { passive: true });
2244      }
2245    }
2246
2247    if (filterClear) {
2248      filterClear.addEventListener("click", function () {
2249        controls.forEach(function (control) {
2250          control.checked = false;
2251        });
2252        applyFilters(true);
2253      });
2254    }
2255
2256    cards.forEach(function (card) {
2257      card.addEventListener("click", function (event) {
2258        if (event.target.closest("[data-clc-product-link], .clc-card-link")) {
2259          pushCardSelection(card);
2260        }
2261      });
2262    });
2263
2264    synchronizeFilterViewport();
2265    applySort(false);
2266    applyFilters(false);
2267    pushViewItemListOnce();
2268  }
2269
2270  function initHomeHero(hero) {
2271    if (hero.getAttribute("data-clc-initialized") === "true") {
2272      return;
2273    }
2274
2275    var viewport = hero.querySelector("[data-clc-home-hero-viewport]");
2276    var slides = toArray(hero.querySelectorAll("[data-clc-home-slide]"));
2277    var dots = toArray(hero.querySelectorAll("[data-clc-home-hero-dot]"));
2278    var previous = hero.querySelector("[data-clc-home-hero-previous]");
2279    var next = hero.querySelector("[data-clc-home-hero-next]");
2280    var status = hero.querySelector("[data-clc-home-hero-status]");
2281    var reducedMotion = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
2282    var currentIndex = 0;
2283    var scrollTimer = 0;
2284
2285    if (!viewport || !slides.length) {
2286      return;
2287    }
2288
2289    hero.setAttribute("data-clc-initialized", "true");
2290
2291    function nearestIndex() {
2292      var left = viewport.scrollLeft;
2293      var bestIndex = 0;
2294      var bestDistance = Infinity;
2295
2296      slides.forEach(function (slide, index) {
2297        var distance = Math.abs(slide.offsetLeft - left);
2298        if (distance < bestDistance) {
2299          bestDistance = distance;
2300          bestIndex = index;
2301        }
2302      });
2303
2304      return bestIndex;
2305    }
2306
2307    function updateState(index, announce) {
2308      currentIndex = Math.max(0, Math.min(slides.length - 1, index));
2309      slides.forEach(function (slide, slideIndex) {
2310        var active = slideIndex === currentIndex;
2311
2312        if (!active && slide.contains(document.activeElement)) {
2313          try {
2314            viewport.focus({ preventScroll: true });
2315          } catch (error) {
2316            viewport.focus();
2317          }
2318        }
2319        slide.setAttribute("aria-hidden", active ? "false" : "true");
2320        if (active) {
2321          slide.removeAttribute("inert");
2322        } else {
2323          slide.setAttribute("inert", "");
2324        }
2325      });
2326      dots.forEach(function (dot, dotIndex) {
2327        dot.setAttribute("aria-pressed", dotIndex === currentIndex ? "true" : "false");
2328      });
2329      if (previous) {
2330        previous.disabled = currentIndex === 0;
2331      }
2332      if (next) {
2333        next.disabled = currentIndex === slides.length - 1;
2334      }
2335      if (announce) {
2336        setText(status, String(currentIndex + 1) + " / " + String(slides.length));
2337      }
2338    }
2339
2340    function showSlide(index, announce) {
2341      var targetIndex = Math.max(0, Math.min(slides.length - 1, index));
2342      var target = slides[targetIndex];
2343
2344      updateState(targetIndex, announce);
2345      if (typeof viewport.scrollTo === "function") {
2346        viewport.scrollTo({
2347          left: target.offsetLeft,
2348          behavior: reducedMotion ? "auto" : "smooth"
2349        });
2350      } else {
2351        viewport.scrollLeft = target.offsetLeft;
2352      }
2353    }
2354
2355    dots.forEach(function (dot) {
2356      dot.addEventListener("click", function () {
2357        showSlide(Number(dot.getAttribute("data-clc-home-hero-dot")) || 0, true);
2358      });
2359    });
2360
2361    if (previous) {
2362      previous.addEventListener("click", function () {
2363        showSlide(currentIndex - 1, true);
2364      });
2365    }
2366
2367    if (next) {
2368      next.addEventListener("click", function () {
2369        showSlide(currentIndex + 1, true);
2370      });
2371    }
2372
2373    viewport.addEventListener("keydown", function (event) {
2374      if (event.key === "ArrowLeft" || event.key === "ArrowRight") {
2375        event.preventDefault();
2376        showSlide(currentIndex + (event.key === "ArrowRight" ? 1 : -1), true);
2377      }
2378    });
2379
2380    viewport.addEventListener("scroll", function () {
2381      window.clearTimeout(scrollTimer);
2382      scrollTimer = window.setTimeout(function () {
2383        var nextIndex = nearestIndex();
2384        updateState(nextIndex, nextIndex !== currentIndex);
2385      }, 120);
2386    }, { passive: true });
2387
2388    window.addEventListener("resize", function () {
2389      showSlide(currentIndex, false);
2390    }, { passive: true });
2391
2392    updateState(0, false);
2393  }
2394
2395  function initHomeTestimonials(testimonials) {
2396    if (testimonials.getAttribute("data-clc-initialized") === "true") {
2397      return;
2398    }
2399
2400    var slides = toArray(testimonials.querySelectorAll("[data-clc-home-testimonial-slide]"));
2401    var controls = toArray(testimonials.querySelectorAll("[data-clc-home-testimonial-control]"));
2402    var stage = testimonials.querySelector(".clc-home-testimonial-stage");
2403    var status = testimonials.querySelector("[data-clc-home-testimonial-status]");
2404    var reducedMotion = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
2405    var currentIndex = 0;
2406    var autoplayTimer = 0;
2407    var touchStartX = null;
2408
2409    if (!slides.length || !controls.length) {
2410      return;
2411    }
2412
2413    testimonials.setAttribute("data-clc-initialized", "true");
2414
2415    function normalizedIndex(index) {
2416      return (index + slides.length) % slides.length;
2417    }
2418
2419    function updateSlide(index, announce) {
2420      currentIndex = normalizedIndex(index);
2421
2422      slides.forEach(function (slide, slideIndex) {
2423        var selected = slideIndex === currentIndex;
2424        setHidden(slide, !selected);
2425        slide.classList.toggle("is-active", selected);
2426      });
2427
2428      controls.forEach(function (control) {
2429        var selected = Number(control.getAttribute("data-clc-home-testimonial-control")) === currentIndex;
2430        control.setAttribute("aria-current", selected ? "true" : "false");
2431      });
2432
2433      if (announce) {
2434        setText(status, String(currentIndex + 1) + " / " + String(slides.length));
2435      }
2436    }
2437
2438    function stopAutoplay() {
2439      if (autoplayTimer) {
2440        window.clearInterval(autoplayTimer);
2441        autoplayTimer = 0;
2442      }
2443    }
2444
2445    function startAutoplay() {
2446      stopAutoplay();
2447      if (reducedMotion || document.hidden) {
2448        return;
2449      }
2450      autoplayTimer = window.setInterval(function () {
2451        updateSlide(currentIndex + 1, false);
2452      }, 5000);
2453    }
2454
2455    function activateControl(control, announce) {
2456      updateSlide(Number(control.getAttribute("data-clc-home-testimonial-control")) || 0, announce);
2457      startAutoplay();
2458    }
2459
2460    controls.forEach(function (control) {
2461      control.addEventListener("click", function () {
2462        activateControl(control, true);
2463      });
2464
2465      control.addEventListener("keydown", function (event) {
2466        var group;
2467        var groupControls;
2468        var groupIndex;
2469        var nextIndex;
2470
2471        if (["ArrowLeft", "ArrowRight", "Home", "End"].indexOf(event.key) === -1) {
2472          return;
2473        }
2474
2475        group = control.parentElement;
2476        groupControls = toArray(group.querySelectorAll("[data-clc-home-testimonial-control]"));
2477        groupIndex = groupControls.indexOf(control);
2478        nextIndex = groupIndex;
2479
2480        if (event.key === "Home") {
2481          nextIndex = 0;
2482        } else if (event.key === "End") {
2483          nextIndex = groupControls.length - 1;
2484        } else {
2485          nextIndex = (groupIndex + (event.key === "ArrowRight" ? 1 : -1) + groupControls.length) % groupControls.length;
2486        }
2487
2488        event.preventDefault();
2489        groupControls[nextIndex].focus();
2490        activateControl(groupControls[nextIndex], true);
2491      });
2492    });
2493
2494    testimonials.addEventListener("pointerenter", stopAutoplay);
2495    testimonials.addEventListener("pointerleave", startAutoplay);
2496    testimonials.addEventListener("focusin", stopAutoplay);
2497    testimonials.addEventListener("focusout", function () {
2498      window.setTimeout(function () {
2499        if (!testimonials.contains(document.activeElement)) {
2500          startAutoplay();
2501        }
2502      }, 0);
2503    });
2504
2505    if (stage) {
2506      stage.addEventListener("touchstart", function (event) {
2507        touchStartX = event.touches && event.touches[0] ? event.touches[0].clientX : null;
2508        stopAutoplay();
2509      }, { passive: true });
2510
2511      stage.addEventListener("touchend", function (event) {
2512        var touchEndX = event.changedTouches && event.changedTouches[0] ? event.changedTouches[0].clientX : null;
2513        var distance = touchStartX !== null && touchEndX !== null ? touchEndX - touchStartX : 0;
2514
2515        if (Math.abs(distance) >= 40) {
2516          updateSlide(currentIndex + (distance < 0 ? 1 : -1), true);
2517        }
2518        touchStartX = null;
2519        startAutoplay();
2520      }, { passive: true });
2521    }
2522
2523    document.addEventListener("visibilitychange", function () {
2524      if (document.hidden) {
2525        stopAutoplay();
2526      } else {
2527        startAutoplay();
2528      }
2529    });
2530
2531    updateSlide(0, false);
2532    startAutoplay();
2533  }
2534
2535  function initWeeklyCountdown(countdown) {
2536    if (countdown.getAttribute("data-clc-initialized") === "true") {
2537      return;
2538    }
2539
2540    var days = countdown.querySelector("[data-clc-countdown-days]");
2541    var hours = countdown.querySelector("[data-clc-countdown-hours]");
2542    var minutes = countdown.querySelector("[data-clc-countdown-minutes]");
2543    var seconds = countdown.querySelector("[data-clc-countdown-seconds]");
2544    var timer = 0;
2545    var tokyoOffset = 9 * 60 * 60 * 1000;
2546
2547    if (!days || !hours || !minutes || !seconds) {
2548      return;
2549    }
2550
2551    countdown.setAttribute("data-clc-initialized", "true");
2552
2553    function pad(value) {
2554      return String(Math.max(0, value)).padStart(2, "0");
2555    }
2556
2557    function nextDeadline(now) {
2558      var tokyoNow = new Date(now.getTime() + tokyoOffset);
2559      var weekday = tokyoNow.getUTCDay();
2560      var daysAhead = (1 - weekday + 7) % 7;
2561      var tokyoMidnight = Date.UTC(tokyoNow.getUTCFullYear(), tokyoNow.getUTCMonth(), tokyoNow.getUTCDate());
2562      var deadline = tokyoMidnight + (daysAhead * 86400000) - tokyoOffset;
2563
2564      if (deadline <= now.getTime()) {
2565        deadline += 7 * 86400000;
2566      }
2567      return deadline;
2568    }
2569
2570    function updateCountdown() {
2571      var now = new Date();
2572      var remaining = Math.max(0, Math.floor((nextDeadline(now) - now.getTime()) / 1000));
2573      var remainingDays = Math.floor(remaining / 86400);
2574      var remainingHours = Math.floor((remaining % 86400) / 3600);
2575      var remainingMinutes = Math.floor((remaining % 3600) / 60);
2576      var remainingSeconds = remaining % 60;
2577
2578      setText(days, pad(remainingDays));
2579      setText(hours, pad(remainingHours));
2580      setText(minutes, pad(remainingMinutes));
2581      setText(seconds, pad(remainingSeconds));
2582      countdown.setAttribute(
2583        "aria-label",
2584        "次の月曜日0時まで" + String(remainingDays) + "日" + String(remainingHours) + "時間" + String(remainingMinutes) + "分"
2585      );
2586    }
2587
2588    function startCountdown() {
2589      if (timer) {
2590        window.clearInterval(timer);
2591      }
2592      updateCountdown();
2593      timer = window.setInterval(updateCountdown, 1000);
2594    }
2595
2596    startCountdown();
2597
2598    document.addEventListener("visibilitychange", function () {
2599      if (!document.hidden) {
2600        updateCountdown();
2601      }
2602    });
2603
2604    window.addEventListener("pagehide", function () {
2605      window.clearInterval(timer);
2606      timer = 0;
2607    });
2608    window.addEventListener("pageshow", function (event) {
2609      if (event.persisted) {
2610        startCountdown();
2611      }
2612    });
2613  }
2614
2615  function initHomeProductList(list) {
2616    if (list.getAttribute("data-clc-initialized") === "true") {
2617      return;
2618    }
2619
2620    var cards = toArray(list.querySelectorAll(".clc-product-card"));
2621    var listName = list.getAttribute("data-list-name") || "注目の商品";
2622    var viewed = window.__CLCCommerceViewedLists;
2623    var viewKey = String(window.location.pathname || "") + "::" + listName;
2624
2625    list.setAttribute("data-clc-initialized", "true");
2626
2627    function cardItem(card, index) {
2628      var itemId = card.getAttribute("data-product-id") || card.getAttribute("data-handle") || "";
2629      return {
2630        item_id: String(itemId || cardFieldValue(card, "title")),
2631        item_name: cardFieldValue(card, "title"),
2632        item_category: cardFieldValue(card, "type"),
2633        item_list_name: listName,
2634        price: numericCardValue(card, "price"),
2635        index: index + 1
2636      };
2637    }
2638
2639    cards.forEach(function (card, index) {
2640      card.addEventListener("click", function (event) {
2641        if (event.target.closest("[data-clc-product-link], .clc-card-link")) {
2642          sendAnalyticsEvent("select_item", {
2643            item_list_name: listName,
2644            items: [cardItem(card, index)]
2645          });
2646        }
2647      });
2648    });
2649
2650    if (!cards.length) {
2651      return;
2652    }
2653
2654    if (!viewed || typeof viewed.has !== "function" || typeof viewed.add !== "function") {
2655      viewed = new Set();
2656      window.__CLCCommerceViewedLists = viewed;
2657    }
2658
2659    if (!viewed.has(viewKey)) {
2660      viewed.add(viewKey);
2661      sendAnalyticsEvent("view_item_list", {
2662        item_list_name: listName,
2663        items: cards.map(cardItem)
2664      });
2665    }
2666  }
2667
2668  function focusableElements(panel) {
2669    return toArray(panel.querySelectorAll(
2670      "a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex='-1'])"
2671    )).filter(function (element) {
2672      return !element.hidden &&
2673        !element.closest("[hidden]") &&
2674        element.getAttribute("aria-hidden") !== "true" &&
2675        !element.classList.contains("clc-panel-backdrop");
2676    });
2677  }
2678
2679  function initUtilityPanels(storefront) {
2680    var panels = toArray(storefront.querySelectorAll("[data-clc-panel]"));
2681    var triggers = toArray(storefront.querySelectorAll("[data-clc-panel-open]"));
2682    var activePanel = null;
2683    var activeTrigger = null;
2684    var inertedBackground = [];
2685
2686    if (!panels.length || storefront.getAttribute("data-clc-panels-ready") === "true") {
2687      return;
2688    }
2689    storefront.setAttribute("data-clc-panels-ready", "true");
2690
2691    function setTriggerState(panelId, expanded) {
2692      triggers.forEach(function (trigger) {
2693        if (trigger.getAttribute("data-clc-panel-open") !== panelId) {
2694          return;
2695        }
2696        trigger.setAttribute("aria-controls", panelId);
2697        trigger.setAttribute("aria-expanded", expanded ? "true" : "false");
2698      });
2699    }
2700
2701    function restoreBackground() {
2702      inertedBackground.forEach(function (state) {
2703        if (state.hadInert) {
2704          state.element.setAttribute("inert", "");
2705        } else {
2706          state.element.removeAttribute("inert");
2707        }
2708        if (state.ariaHidden == null) {
2709          state.element.removeAttribute("aria-hidden");
2710        } else {
2711          state.element.setAttribute("aria-hidden", state.ariaHidden);
2712        }
2713      });
2714      inertedBackground = [];
2715    }
2716
2717    function isolatePanel(panel) {
2718      restoreBackground();
2719      toArray(storefront.children).forEach(function (element) {
2720        if (element === panel) {
2721          return;
2722        }
2723        inertedBackground.push({
2724          element: element,
2725          hadInert: element.hasAttribute("inert"),
2726          ariaHidden: element.getAttribute("aria-hidden")
2727        });
2728        element.setAttribute("inert", "");
2729        element.setAttribute("aria-hidden", "true");
2730      });
2731    }
2732
2733    function closePanel(returnFocus) {
2734      var trigger = activeTrigger;
2735      var panelId;
2736
2737      if (!activePanel) {
2738        return;
2739      }
2740      panelId = activePanel.id;
2741      activePanel.classList.remove("is-open");
2742      activePanel.hidden = true;
2743      activePanel = null;
2744      activeTrigger = null;
2745      document.body.classList.remove("clc-overlay-open");
2746      restoreBackground();
2747      setTriggerState(panelId, false);
2748      if (returnFocus && trigger && document.contains(trigger)) {
2749        trigger.focus();
2750      }
2751    }
2752
2753    function showPanel(panelId, trigger) {
2754      var panel = storefront.querySelector("#" + String(panelId || ""));
2755      var focusable;
2756
2757      if (!panel) {
2758        return;
2759      }
2760      if (activePanel && activePanel !== panel) {
2761        closePanel(false);
2762      }
2763      activePanel = panel;
2764      activeTrigger = trigger || null;
2765      panel.hidden = false;
2766      setTriggerState(panel.id, true);
2767      isolatePanel(panel);
2768      document.body.classList.add("clc-overlay-open");
2769      document.dispatchEvent(new CustomEvent("clc:panel-opened", {
2770        detail: { panelId: panel.id, panel: panel, trigger: activeTrigger }
2771      }));
2772      window.requestAnimationFrame(function () {
2773        panel.classList.add("is-open");
2774        focusable = focusableElements(panel);
2775        if (focusable.length) {
2776          focusable[0].focus();
2777        }
2778      });
2779    }
2780
2781    triggers.forEach(function (trigger) {
2782      var panelId = trigger.getAttribute("data-clc-panel-open");
2783      trigger.setAttribute("aria-controls", panelId);
2784      trigger.setAttribute("aria-expanded", "false");
2785      trigger.addEventListener("click", function () {
2786        showPanel(panelId, trigger);
2787      });
2788    });
2789
2790    panels.forEach(function (panel) {
2791      toArray(panel.querySelectorAll("[data-clc-panel-close]")).forEach(function (button) {
2792        button.addEventListener("click", function () {
2793          closePanel(true);
2794        });
2795      });
2796
2797      panel.addEventListener("keydown", function (event) {
2798        var focusable;
2799        var first;
2800        var last;
2801
2802        if (event.key === "Escape") {
2803          event.preventDefault();
2804          closePanel(true);
2805          return;
2806        }
2807        if (event.key !== "Tab") {
2808          return;
2809        }
2810        focusable = focusableElements(panel);
2811        if (!focusable.length) {
2812          return;
2813        }
2814        first = focusable[0];
2815        last = focusable[focusable.length - 1];
2816        if (event.shiftKey && document.activeElement === first) {
2817          event.preventDefault();
2818          last.focus();
2819        } else if (!event.shiftKey && document.activeElement === last) {
2820          event.preventDefault();
2821          first.focus();
2822        }
2823      });
2824    });
2825
2826    document.addEventListener("keydown", function (event) {
2827      if (event.key === "Escape" && activePanel) {
2828        event.preventDefault();
2829        closePanel(true);
2830      }
2831    });
2832
2833    document.addEventListener("clc:open-panel", function (event) {
2834      var detail = event.detail || {};
2835      showPanel(detail.panelId, detail.trigger);
2836    });
2837  }
2838
2839  function initCartReminder(reminder) {
2840    var storefront = reminder.closest(".clc-storefront");
2841    var image = reminder.querySelector("[data-clc-cart-reminder-image]");
2842    var media = reminder.querySelector(".clc-cart-reminder-media");
2843    var name = reminder.querySelector("[data-clc-cart-reminder-name]");
2844    var meta = reminder.querySelector("[data-clc-cart-reminder-meta]");
2845    var price = reminder.querySelector("[data-clc-cart-reminder-price]");
2846    var quantity = reminder.querySelector("[data-clc-cart-reminder-quantity]");
2847    var subtotal = reminder.querySelector("[data-clc-cart-reminder-subtotal]");
2848    var extra = reminder.querySelector("[data-clc-cart-reminder-extra]");
2849    var openCart = reminder.querySelector("[data-clc-cart-reminder-open-cart]");
2850    var initialItems = readCart();
2851    var seenKey = "clc_cart_reminder_seen_v1";
2852    var seenInMemory = false;
2853    var timer = null;
2854
2855    if (
2856      !storefront ||
2857      !name ||
2858      !openCart ||
2859      !initialItems.length ||
2860      reminder.getAttribute("data-clc-cart-reminder-ready") === "true"
2861    ) {
2862      return;
2863    }
2864    reminder.setAttribute("data-clc-cart-reminder-ready", "true");
2865
2866    function hasBeenSeen() {
2867      if (seenInMemory) {
2868        return true;
2869      }
2870      try {
2871        return window.sessionStorage.getItem(seenKey) === "1";
2872      } catch (error) {
2873        return false;
2874      }
2875    }
2876
2877    function markSeen() {
2878      seenInMemory = true;
2879      try {
2880        window.sessionStorage.setItem(seenKey, "1");
2881      } catch (error) {
2882        // A session-only in-memory flag still prevents repeated interruption.
2883      }
2884    }
2885
2886    function render() {
2887      var items = readCart();
2888      var item = items[0];
2889      var details = [];
2890      var total = 0;
2891      var currency = "JPY";
2892
2893      if (!item) {
2894        return false;
2895      }
2896
2897      setText(name, item.title);
2898      if (item.variantTitle && normalize(item.variantTitle) !== "default title") {
2899        details.push(item.variantTitle);
2900      }
2901      if (item.sellingPlanName) {
2902        details.push(item.sellingPlanName);
2903      }
2904      setText(meta, details.join(" / "));
2905      setHidden(meta, details.length === 0);
2906      setText(price, item.price ? formatMoney(item.price) : "—");
2907      setText(quantity, item.quantity);
2908
2909      if (image && media) {
2910        /* imageUrl は localStorage 復元値を含むため Shopify CDN の https URL のみ許可
2911           (responsiveShopifyImageUrl がホスト検証を兼ねる。2026-08-30総点検 L-8) */
2912        var safeImageSrc = item.imageUrl ? responsiveShopifyImageUrl(item.imageUrl, 192) : "";
2913        if (safeImageSrc) {
2914          image.src = safeImageSrc;
2915          image.alt = item.imageAlt || item.title;
2916          setHidden(image, false);
2917          setHidden(media, false);
2918        } else {
2919          image.removeAttribute("src");
2920          image.alt = "";
2921          setHidden(image, true);
2922          setHidden(media, true);
2923        }
2924      }
2925
2926      items.forEach(function (cartItem) {
2927        if (cartItem.price) {
2928          total += cartItem.price.amount * cartItem.quantity;
2929          currency = cartItem.price.currencyCode || currency;
2930        }
2931      });
2932      setText(subtotal, formatMoney({ amount: total, currencyCode: currency }));
2933      setText(extra, items.length > 1 ? "ほか" + String(items.length - 1) + "商品がカートに入っています。" : "");
2934      setHidden(extra, items.length <= 1);
2935      return true;
2936    }
2937
2938    function showReminder() {
2939      var visiblePanel;
2940
2941      timer = null;
2942      if (hasBeenSeen() || !render()) {
2943        return;
2944      }
2945      if (document.visibilityState === "hidden") {
2946        document.addEventListener("visibilitychange", function onVisibilityChange() {
2947          if (document.visibilityState !== "visible") {
2948            return;
2949          }
2950          document.removeEventListener("visibilitychange", onVisibilityChange);
2951          timer = window.setTimeout(showReminder, 1200);
2952        });
2953        return;
2954      }
2955
2956      visiblePanel = storefront.querySelector("[data-clc-panel]:not([hidden])");
2957      if (visiblePanel && visiblePanel !== reminder) {
2958        return;
2959      }
2960      markSeen();
2961      openUtilityPanel(reminder.id, null);
2962    }
2963
2964    openCart.addEventListener("click", function () {
2965      var cartTrigger = storefront.querySelector('[data-clc-panel-open="clc-cart-panel"]');
2966      var close = reminder.querySelector("[data-clc-panel-close]");
2967
2968      markSeen();
2969      if (close) {
2970        close.click();
2971      }
2972      window.setTimeout(function () {
2973        openUtilityPanel("clc-cart-panel", cartTrigger || null);
2974      }, 0);
2975    });
2976
2977    document.addEventListener("clc:panel-opened", function (event) {
2978      var detail = event.detail || {};
2979      if (detail.panelId === "clc-cart-panel") {
2980        markSeen();
2981      }
2982    });
2983
2984    document.addEventListener("clc:cart-changed", function () {
2985      if (readCart().length) {
2986        if (!reminder.hidden) {
2987          render();
2988        }
2989        return;
2990      }
2991      if (!reminder.hidden) {
2992        var close = reminder.querySelector("[data-clc-panel-close]");
2993        if (close) {
2994          close.click();
2995        }
2996      }
2997      if (timer) {
2998        window.clearTimeout(timer);
2999        timer = null;
3000      }
3001    });
3002
3003    timer = window.setTimeout(showReminder, 12000);
3004  }
3005
3006  function initCart(cart) {
3007    var list = cart.querySelector("[data-clc-cart-items]");
3008    var empty = cart.querySelector("[data-clc-cart-empty]");
3009    var summary = cart.querySelector("[data-clc-cart-summary]");
3010    var subtotal = cart.querySelector("[data-clc-cart-subtotal]");
3011    var checkout = cart.querySelector("[data-clc-cart-checkout]");
3012    var errorBox = cart.querySelector("[data-clc-cart-error]");
3013    var statusBox = cart.querySelector("[data-clc-cart-status]");
3014    var cartHeading = cart.querySelector("#clc-cart-title");
3015    var discountNotice = cart.querySelector("[data-clc-cart-discount]");
3016    var discountCodeText = cart.querySelector("[data-clc-cart-discount-code]");
3017    var busy = false;
3018    var preparedCheckout = null;
3019    var prepareController = null;
3020    var prepareSequence = 0;
3021    var lastViewedFingerprint = "";
3022    var lastRenderedItemsFingerprint = "";
3023    var pendingCartFocus = null;
3024
3025    if (!list || !checkout || cart.getAttribute("data-clc-cart-ready") === "true") {
3026      return;
3027    }
3028    cart.setAttribute("data-clc-cart-ready", "true");
3029
3030    function updateCounts(items) {
3031      var count = items.reduce(function (total, item) { return total + item.quantity; }, 0);
3032      toArray(document.querySelectorAll("[data-clc-cart-count]")).forEach(function (badge) {
3033        setText(badge, String(count));
3034        setHidden(badge, count === 0);
3035      });
3036    }
3037
3038    function makeText(tag, className, value) {
3039      var element = document.createElement(tag);
3040      element.className = className;
3041      setText(element, value);
3042      return element;
3043    }
3044
3045    function cartFingerprint(items, discountCode) {
3046      return JSON.stringify({
3047        items: items.map(function (item) {
3048          return [item.entryId || "", item.key, item.productHandle, item.variantId, item.sellingPlanId || "", item.quantity];
3049        }),
3050        discountCode: normalizeDiscountCode(discountCode == null ? activeDiscountCode() : discountCode)
3051      });
3052    }
3053
3054    function checkoutLines(items) {
3055      return items.map(function (item) {
3056        return {
3057          product_handle: item.productHandle,
3058          variant_id: item.variantId,
3059          selling_plan_id: item.sellingPlanId || "",
3060          quantity: item.quantity
3061        };
3062      });
3063    }
3064
3065    function pushViewCartOnce(items) {
3066      var fingerprint;
3067      var value;
3068      var currency;
3069
3070      if (!items.length) {
3071        return;
3072      }
3073      fingerprint = cartFingerprint(items);
3074      if (fingerprint === lastViewedFingerprint) {
3075        return;
3076      }
3077
3078      lastViewedFingerprint = fingerprint;
3079      value = items.reduce(function (total, item) {
3080        return total + (item.price ? item.price.amount * item.quantity : 0);
3081      }, 0);
3082      currency = items.find(function (item) { return item.price && item.price.currencyCode; });
3083      sendAnalyticsEvent("view_cart", {
3084        currency: currency && currency.price.currencyCode || settings.currency || "JPY",
3085        value: Number(value.toFixed(2)),
3086        items: items.map(function (item, index) {
3087          return cartAnalyticsItem(item, index);
3088        })
3089      });
3090    }
3091
3092    function invalidatePreparedCheckout() {
3093      prepareSequence += 1;
3094      if (prepareController && typeof prepareController.abort === "function") {
3095        prepareController.abort();
3096      }
3097      prepareController = null;
3098      preparedCheckout = null;
3099      busy = false;
3100    }
3101
3102    function itemsRenderFingerprint(items) {
3103      return JSON.stringify(items.map(function (item) {
3104        return [item.entryId, item.key, item.title, item.variantTitle, item.sellingPlanName, item.quantity, item.imageUrl, item.price];
3105      }));
3106    }
3107
3108    function restoreCartFocus(items) {
3109      var intent = pendingCartFocus;
3110      var targetRow;
3111      var target;
3112
3113      pendingCartFocus = null;
3114      if (!intent) {
3115        return;
3116      }
3117      targetRow = toArray(list.querySelectorAll("[data-cart-entry-id]")).find(function (row) {
3118        return row.getAttribute("data-cart-entry-id") === intent.entryId;
3119      });
3120      if (!targetRow && items.length) {
3121        targetRow = list.children[Math.min(intent.index, items.length - 1)] || null;
3122      }
3123      if (targetRow) {
3124        target = targetRow.querySelector('[data-cart-action="' + intent.action + '"]') || targetRow.querySelector("[data-cart-action]");
3125      }
3126      if (!target && cartHeading) {
3127        target = cartHeading;
3128      }
3129      if (target && typeof target.focus === "function") {
3130        target.focus({ preventScroll: true });
3131      }
3132      if (statusBox && intent.message) {
3133        setText(statusBox, "");
3134        if (typeof window.requestAnimationFrame === "function") {
3135          window.requestAnimationFrame(function () { setText(statusBox, intent.message); });
3136        } else {
3137          window.setTimeout(function () { setText(statusBox, intent.message); }, 0);
3138        }
3139      }
3140    }
3141
3142    function preparedLineForItem(item) {
3143      var lines = preparedCheckout && Array.isArray(preparedCheckout.currentLines) ? preparedCheckout.currentLines : [];
3144      return lines.find(function (line) {
3145        return line.variantId === item.variantId && line.sellingPlanId === (item.sellingPlanId || "");
3146      }) || null;
3147    }
3148
3149    function analyticsItemWithCurrentPrice(item, index) {
3150      var analyticsItem = cartAnalyticsItem(item, index);
3151      var currentLine = preparedLineForItem(item);
3152      if (currentLine && currentLine.price) {
3153        analyticsItem.price = currentLine.price.amount;
3154      }
3155      return analyticsItem;
3156    }
3157
3158    function render() {
3159      var items = readCart();
3160      var total = 0;
3161      var currency = "JPY";
3162      var renderFingerprint = itemsRenderFingerprint(items);
3163
3164      if (renderFingerprint !== lastRenderedItemsFingerprint) {
3165        while (list.firstChild) {
3166          list.removeChild(list.firstChild);
3167        }
3168        items.forEach(function (item) {
3169        var row = document.createElement("li");
3170        var media = document.createElement("div");
3171        var content = document.createElement("div");
3172        var details = [];
3173        var controls = document.createElement("div");
3174        var decrease = document.createElement("button");
3175        var quantity = makeText("span", "clc-cart-item-quantity", item.quantity);
3176        var increase = document.createElement("button");
3177        var remove = document.createElement("button");
3178
3179        row.className = "clc-cart-item";
3180        row.setAttribute("data-cart-key", item.key);
3181        row.setAttribute("data-cart-entry-id", item.entryId || "");
3182        media.className = "clc-cart-item-media";
3183        /* localStorage 復元値を含むため、ホスト検証を通らないURLは描画しない(L-8) */
3184        var safeRowImageSrc = item.imageUrl ? responsiveShopifyImageUrl(item.imageUrl, 192) : "";
3185        if (safeRowImageSrc) {
3186          var image = document.createElement("img");
3187          image.src = safeRowImageSrc;
3188          image.alt = item.imageAlt;
3189          image.width = 96;
3190          image.height = 96;
3191          image.loading = "lazy";
3192          media.appendChild(image);
3193        }
3194        content.className = "clc-cart-item-content";
3195        content.appendChild(makeText("h3", "clc-cart-item-title", item.title));
3196        if (item.variantTitle && normalize(item.variantTitle) !== "default title") {
3197          details.push(item.variantTitle);
3198        }
3199        if (item.sellingPlanName) {
3200          details.push(item.sellingPlanName);
3201        }
3202        if (details.length) {
3203          content.appendChild(makeText("p", "clc-cart-item-meta", details.join(" / ")));
3204        }
3205        content.appendChild(makeText("p", "clc-cart-item-price", item.price ? formatMoney(item.price) : "—"));
3206
3207        controls.className = "clc-cart-item-controls";
3208        decrease.type = "button";
3209        decrease.className = "clc-cart-item-step";
3210        decrease.setAttribute("data-cart-action", "decrease");
3211        decrease.setAttribute("aria-label", item.title + "の数量を減らす");
3212        setText(decrease, "−");
3213        increase.type = "button";
3214        increase.className = "clc-cart-item-step";
3215        increase.setAttribute("data-cart-action", "increase");
3216        increase.setAttribute("aria-label", item.title + "の数量を増やす");
3217        increase.disabled = item.quantity >= cartItemMaximum(item);
3218        setText(increase, "+");
3219        remove.type = "button";
3220        remove.className = "clc-cart-item-remove";
3221        remove.setAttribute("data-cart-action", "remove");
3222        remove.setAttribute("aria-label", item.title + "をカートから削除");
3223        setText(remove, labels.remove || "削除");
3224        controls.appendChild(decrease);
3225        controls.appendChild(quantity);
3226        controls.appendChild(increase);
3227        controls.appendChild(remove);
3228        content.appendChild(controls);
3229        row.appendChild(media);
3230        row.appendChild(content);
3231        list.appendChild(row);
3232
3233        });
3234        lastRenderedItemsFingerprint = renderFingerprint;
3235        restoreCartFocus(items);
3236      }
3237
3238      items.forEach(function (item, index) {
3239        var currentLine = preparedCheckout && preparedCheckout.fingerprint === cartFingerprint(items) ? preparedLineForItem(item) : null;
3240        var displayedPrice = currentLine && currentLine.price ? currentLine.price : item.price;
3241        var row = list.children[index];
3242        if (row) {
3243          // Preserve quantity controls and keyboard focus while applying the
3244          // same current Shopify prices used by the subtotal and Checkout event.
3245          setText(row.querySelector(".clc-cart-item-price"), displayedPrice ? formatMoney(displayedPrice) : "—");
3246        }
3247        if (item.price) {
3248          total += item.price.amount * item.quantity;
3249          currency = item.price.currencyCode || currency;
3250        }
3251      });
3252
3253      if (preparedCheckout && preparedCheckout.fingerprint === cartFingerprint(items) && preparedCheckout.totalPrice) {
3254        total = preparedCheckout.totalPrice.amount;
3255        currency = preparedCheckout.totalPrice.currencyCode;
3256      }
3257
3258      updateCounts(items);
3259      setHidden(empty, items.length > 0);
3260      setHidden(summary, items.length === 0);
3261      setText(subtotal, items.length ? formatMoney({ amount: total, currencyCode: currency }) : "—");
3262      var discountCode = activeDiscountCode();
3263      setText(discountCodeText, discountCode);
3264      setHidden(discountNotice, !discountCode || items.length === 0);
3265      var fingerprint = cartFingerprint(items);
3266      var checkoutReady = Boolean(
3267        preparedCheckout &&
3268        preparedCheckout.fingerprint === fingerprint &&
3269        preparedCheckout.checkoutUrl
3270      );
3271
3272      checkout.setAttribute("aria-disabled", busy || items.length === 0 || !checkoutReady ? "true" : "false");
3273      checkout.setAttribute("aria-busy", busy ? "true" : "false");
3274      if (!busy && checkoutReady) {
3275        // This must be the final Shopify Checkout URL before the user clicks.
3276        // Google's cross-domain linker decorates this real anchor; JavaScript
3277        // navigation to an asynchronously returned URL is not supported.
3278        checkout.href = preparedCheckout.checkoutUrl;
3279        checkout.removeAttribute("role"); // hrefがある間は本物のリンクとして通知する
3280      } else {
3281        checkout.removeAttribute("href");
3282        checkout.setAttribute("role", "button"); // href無しの<a>はロール不定になるため明示する
3283      }
3284      if (busy) {
3285        setText(checkout, labels.cartLoading || "購入画面を準備しています…");
3286      } else {
3287        setText(checkout, labels.checkout || "購入手続きへ");
3288      }
3289    }
3290
3291    function prepareCheckout(items) {
3292      var discountCode = activeDiscountCode();
3293      var fingerprint = cartFingerprint(items, discountCode);
3294      var requestBody = { lines: checkoutLines(items) };
3295      var requestId;
3296
3297      if (discountCode) {
3298        requestBody.discount_code = discountCode;
3299      }
3300
3301      if (!items.length || !settings.restUrl || !settings.nonce) {
3302        invalidatePreparedCheckout();
3303        render();
3304        return Promise.resolve(null);
3305      }
3306
3307      if (preparedCheckout && preparedCheckout.fingerprint === fingerprint) {
3308        busy = false;
3309        setHidden(errorBox, true);
3310        render();
3311        return Promise.resolve(preparedCheckout);
3312      }
3313
3314      requestId = ++prepareSequence;
3315      if (prepareController && typeof prepareController.abort === "function") {
3316        prepareController.abort();
3317      }
3318      prepareController = typeof window.AbortController === "function" ? new window.AbortController() : null;
3319      preparedCheckout = null;
3320      busy = true;
3321      setHidden(errorBox, true);
3322      render();
3323
3324      return window.fetch(settings.restUrl, {
3325        method: "POST",
3326        credentials: "same-origin",
3327        headers: {
3328          "Content-Type": "application/json",
3329          "X-WP-Nonce": settings.nonce
3330        },
3331        signal: prepareController ? prepareController.signal : undefined,
3332        body: JSON.stringify(requestBody)
3333      }).then(function (response) {
3334        return response.json().catch(function () { return {}; }).then(function (payload) {
3335          if (!response.ok) {
3336            throw new Error(payload.message || labels.checkoutError || "購入画面を準備できませんでした。");
3337          }
3338          return payload;
3339        });
3340      }).then(function (payload) {
3341        if (!payload.checkout_url) {
3342          throw new Error(labels.checkoutError || "購入画面を準備できませんでした。");
3343        }
3344        if (requestId !== prepareSequence || cartFingerprint(readCart()) !== fingerprint) {
3345          return null;
3346        }
3347
3348        var totalPrice = payload.cart_total && Number.isFinite(Number(payload.cart_total.amount)) ? {
3349          amount: Number(payload.cart_total.amount),
3350          currencyCode: String(payload.cart_total.currency_code || settings.currency || "JPY")
3351        } : null;
3352        var currentLines = (Array.isArray(payload.cart_lines) ? payload.cart_lines : []).map(function (line) {
3353          var amount = Number(line && line.price && line.price.amount);
3354          if (!line || !/^gid:\/\/shopify\/ProductVariant\/[1-9][0-9]*$/.test(String(line.variant_id || "")) || !Number.isFinite(amount)) {
3355            return null;
3356          }
3357          return {
3358            variantId: String(line.variant_id),
3359            sellingPlanId: String(line.selling_plan_id || ""),
3360            quantity: Math.max(1, Number.parseInt(line.quantity, 10) || 1),
3361            price: { amount: amount, currencyCode: String(line.price.currency_code || settings.currency || "JPY") }
3362          };
3363        }).filter(Boolean);
3364
3365        preparedCheckout = {
3366          fingerprint: fingerprint,
3367          checkoutUrl: String(payload.checkout_url),
3368          cartId: String(payload.cart_id || ""),
3369          cartToken: String(payload.cart_token || ""),
3370          discountCode: discountCode,
3371          items: items.slice(),
3372          totalPrice: totalPrice,
3373          currentLines: currentLines
3374        };
3375        prepareController = null;
3376        busy = false;
3377        render();
3378        return preparedCheckout;
3379      }).catch(function (error) {
3380        if (requestId !== prepareSequence) {
3381          return null;
3382        }
3383        prepareController = null;
3384        if (error && error.name === "AbortError") {
3385          return null;
3386        }
3387        preparedCheckout = null;
3388        busy = false;
3389        render();
3390        setText(errorBox, error && error.message ? error.message : (labels.checkoutError || "購入画面を準備できませんでした。"));
3391        setHidden(errorBox, false);
3392        return null;
3393      });
3394    }
3395
3396    list.addEventListener("click", function (event) {
3397      var button = event.target.closest("[data-cart-action]");
3398      var row = button && button.closest("[data-cart-key]");
3399      var key = row && row.getAttribute("data-cart-key");
3400      var items = readCart();
3401      var itemIndex = key && items.findIndex(function (candidate) { return candidate.key === key; });
3402      var item = Number.isFinite(itemIndex) && itemIndex >= 0 ? items[itemIndex] : null;
3403      var action;
3404      var nextQuantity;
3405      var delta;
3406
3407      if (!button || !item) {
3408        return;
3409      }
3410      action = button.getAttribute("data-cart-action");
3411      pendingCartFocus = {
3412        entryId: item.entryId || "",
3413        index: itemIndex,
3414        action: action === "remove" ? "decrease" : action,
3415        message: action === "remove" ? item.title + (labels.cartRemoved || "をカートから削除しました。") : item.title + (labels.cartQuantityChanged || "の数量を変更しました。")
3416      };
3417      if (action === "increase") {
3418        nextQuantity = Math.min(cartItemMaximum(item), item.quantity + 1);
3419        delta = nextQuantity - item.quantity;
3420        if (delta <= 0) {
3421          return;
3422        }
3423        updateCartItem(key, nextQuantity);
3424        pushCartQuantityEvent("add_to_cart", item, delta, itemIndex);
3425      } else if (action === "decrease") {
3426        nextQuantity = Math.max(0, item.quantity - 1);
3427        delta = item.quantity - nextQuantity;
3428        updateCartItem(key, nextQuantity);
3429        pushCartQuantityEvent("remove_from_cart", item, delta, itemIndex);
3430      } else {
3431        updateCartItem(key, 0);
3432        pushCartQuantityEvent("remove_from_cart", item, item.quantity, itemIndex);
3433      }
3434    });
3435
3436    checkout.addEventListener("click", function (event) {
3437      var items = readCart();
3438      var fingerprint = cartFingerprint(items);
3439      var handoff = preparedCheckout;
3440      var analyticsItems;
3441      var value;
3442      var currency;
3443
3444      event.preventDefault();
3445
3446      if (busy || !items.length) {
3447        return;
3448      }
3449
3450      if (!handoff || handoff.fingerprint !== fingerprint || !checkout.href) {
3451        prepareCheckout(items);
3452        return;
3453      }
3454
3455      // Do not stop propagation and do not replace this href. Google's linker
3456      // listens to the trusted click at document level and decorates the final
3457      // Shopify Checkout URL before the deferred navigation reads it.
3458      busy = true;
3459      setHidden(errorBox, true);
3460      checkout.setAttribute("aria-disabled", "true");
3461      checkout.setAttribute("aria-busy", "true");
3462      analyticsItems = items.map(function (item, index) {
3463        return analyticsItemWithCurrentPrice(item, index);
3464      });
3465      value = handoff.totalPrice ? handoff.totalPrice.amount : items.reduce(function (total, item) {
3466        return total + (item.price ? item.price.amount * item.quantity : 0);
3467      }, 0);
3468      currency = handoff.totalPrice ? handoff.totalPrice.currencyCode : ((items[0].price && items[0].price.currencyCode) || "JPY");
3469
3470      if (handoff.cartId && handoff.cartToken) {
3471        rememberPendingCheckout(handoff.cartId, handoff.cartToken, handoff.items, handoff.discountCode);
3472      }
3473      // Shopify Pixel owns the standard begin_checkout event when the hosted
3474      // Checkout actually starts. Keep this pre-navigation intent distinct so
3475      // one checkout is not counted once on WordPress and again on Shopify.
3476      sendAnalyticsEvent("checkout_click", {
3477        currency: currency,
3478        value: Number(value.toFixed(2)),
3479        items: analyticsItems
3480      }, function () {
3481        navigateToPreparedCheckout(checkout, handoff.checkoutUrl);
3482      });
3483    });
3484
3485    checkout.addEventListener("keydown", function (event) {
3486      if (
3487        (event.key === "Enter" || event.key === " ") &&
3488        checkout.getAttribute("aria-disabled") === "true" &&
3489        !busy &&
3490        readCart().length
3491      ) {
3492        event.preventDefault();
3493        prepareCheckout(readCart());
3494      }
3495    });
3496
3497    document.addEventListener("clc:cart-changed", function () {
3498      invalidatePreparedCheckout();
3499      render();
3500      if (!cart.hidden) {
3501        prepareCheckout(readCart());
3502      }
3503    });
3504    document.addEventListener("clc:panel-opened", function (event) {
3505      var detail = event.detail || {};
3506      if (detail.panelId === cart.id) {
3507        pushViewCartOnce(readCart());
3508        // Reset any prior `_gl` decoration to the validated raw Checkout URL.
3509        // The next trusted click then receives a fresh linker value.
3510        prepareCheckout(readCart());
3511      }
3512    });
3513    window.addEventListener("pageshow", function (event) {
3514      if (!event.persisted) {
3515        return;
3516      }
3517      invalidatePreparedCheckout();
3518      render();
3519      if (!cart.hidden) {
3520        prepareCheckout(readCart());
3521      }
3522    });
3523    render();
3524  }
3525
3526  function initQuiz(quiz) {
3527    var form = quiz.querySelector(".clc-quiz-form");
3528    var steps = toArray(quiz.querySelectorAll("[data-clc-quiz-step]"));
3529    var progress = quiz.querySelector("[data-clc-quiz-progress]");
3530    var progressTrack = quiz.querySelector("[data-clc-quiz-progress-track]");
3531    var progressBar = quiz.querySelector("[data-clc-quiz-progress-bar]");
3532    var back = quiz.querySelector("[data-clc-quiz-back]");
3533    var status = quiz.querySelector("[data-clc-quiz-status]");
3534    var result = quiz.querySelector("[data-clc-quiz-result]");
3535    var resultHeading = quiz.querySelector("[data-clc-quiz-result-heading]");
3536    var resultProfile = quiz.querySelector("[data-clc-quiz-result-profile]");
3537    var profileData = quiz.querySelector("[data-clc-quiz-profiles]");
3538    var productCards = toArray(quiz.querySelectorAll("[data-clc-quiz-product]"));
3539    var reset = quiz.querySelector("[data-clc-quiz-reset]");
3540    var current = 0;
3541    var advanceTimer = 0;
3542    var profiles = {};
3543
3544    if (!form || !steps.length || !result || quiz.getAttribute("data-clc-quiz-ready") === "true") {
3545      return;
3546    }
3547    quiz.setAttribute("data-clc-quiz-ready", "true");
3548
3549    try {
3550      profiles = JSON.parse((profileData && profileData.textContent) || "{}");
3551    } catch (error) {
3552      profiles = {};
3553    }
3554
3555    function progressPercent(index) {
3556      return Math.round(((index + 1) / steps.length) * 100);
3557    }
3558
3559    function showStep(index, focusQuestion) {
3560      var percent;
3561
3562      window.clearTimeout(advanceTimer);
3563      current = Math.max(0, Math.min(steps.length - 1, index));
3564      steps.forEach(function (step, stepIndex) { setHidden(step, stepIndex !== current); });
3565      setText(progress, String(current + 1));
3566      percent = progressPercent(current);
3567      if (progressTrack) {
3568        progressTrack.setAttribute("aria-valuenow", String(percent));
3569      }
3570      if (progressBar) {
3571        progressBar.style.width = String(percent) + "%";
3572      }
3573      if (back) {
3574        setHidden(back, current === 0);
3575      }
3576      setHidden(form, false);
3577      setHidden(result, true);
3578      steps[current].classList.remove("is-entering");
3579      window.requestAnimationFrame(function () {
3580        steps[current].classList.add("is-entering");
3581      });
3582      if (focusQuestion) {
3583        window.setTimeout(function () {
3584          var legend = steps[current] && steps[current].querySelector("legend");
3585          if (legend) {
3586            legend.focus({ preventScroll: true });
3587          }
3588        }, 0);
3589      }
3590    }
3591
3592    function selectedInputs() {
3593      return toArray(form.querySelectorAll("input:checked[data-quiz-scores]"));
3594    }
3595
3596    function calculateRanking() {
3597      var scores = Object.create(null);
3598      selectedInputs().forEach(function (input) {
3599        String(input.getAttribute("data-quiz-scores") || "").split(",").forEach(function (pair) {
3600          var parts = pair.split(":");
3601          var handle = parts[0];
3602          var score = Number(parts[1]) || 0;
3603          scores[handle] = (scores[handle] || 0) + score;
3604        });
3605      });
3606
3607      return productCards.map(function (card, index) {
3608        var handle = card.getAttribute("data-clc-quiz-product") || "";
3609        return { card: card, handle: handle, score: scores[handle] || 0, index: index };
3610      }).sort(function (a, b) {
3611        return b.score - a.score || a.index - b.index;
3612      });
3613    }
3614
3615    function showResults() {
3616      var ranked = calculateRanking().slice(0, 3);
3617      var taste = form.querySelector('[data-quiz-question="taste"]:checked');
3618      var roast = form.querySelector('[data-quiz-question="roast"]:checked');
3619      var profileKey = String((taste && taste.value) || "") + "_" + String((roast && roast.value) || "");
3620      var resultItems = [];
3621
3622      productCards.forEach(function (card) {
3623        setHidden(card, true);
3624        card.classList.remove("is-recommended");
3625        card.style.removeProperty("order");
3626      });
3627      ranked.forEach(function (item, index) {
3628        var image = item.card.querySelector("img");
3629        setHidden(item.card, false);
3630        item.card.classList.add("is-recommended");
3631        item.card.style.order = String(index + 1);
3632        if (image) {
3633          image.loading = "eager";
3634          image.fetchPriority = index === 0 ? "high" : "auto";
3635        }
3636        resultItems.push({
3637          item_id: item.card.getAttribute("data-product-id") || item.handle,
3638          item_name: item.card.getAttribute("data-product-title") || item.handle,
3639          price: Number(item.card.getAttribute("data-price-amount")) || 0,
3640          index: index + 1
3641        });
3642      });
3643      setText(resultProfile, profiles[profileKey] || "あなたにぴったりのコーヒーを見つけました。");
3644      setHidden(form, true);
3645      setHidden(result, false);
3646      sendAnalyticsEvent("view_item_list", {
3647        item_list_id: "coffee_finder_results",
3648        item_list_name: "コーヒー診断結果",
3649        items: resultItems
3650      });
3651      sendAnalyticsEvent("select_content", {
3652        content_type: "coffee_finder",
3653        item_id: ranked.length ? ranked[0].handle : ""
3654      });
3655      if (resultHeading) {
3656        resultHeading.focus({ preventScroll: true });
3657      }
3658    }
3659
3660    function handleAnswer(input) {
3661      var questionId = input.getAttribute("data-quiz-question") || "";
3662      var selectedLabel = input.closest("label");
3663
3664      window.clearTimeout(advanceTimer);
3665      if (selectedLabel) {
3666        selectedLabel.classList.add("is-selected");
3667      }
3668      setText(status, current < steps.length - 1 ? "回答を選択しました。次の質問へ進みます。" : "回答を選択しました。診断結果を表示します。");
3669      sendAnalyticsEvent("select_content", {
3670        content_type: "coffee_finder_answer",
3671        item_id: questionId + ":" + String(input.value || ""),
3672        index: current + 1
3673      });
3674      advanceTimer = window.setTimeout(function () {
3675        if (current < steps.length - 1) {
3676          showStep(current + 1, true);
3677        } else {
3678          showResults();
3679        }
3680      }, 320);
3681    }
3682
3683    form.addEventListener("change", function (event) {
3684      var input = event.target.closest('input[type="radio"][data-quiz-scores]');
3685      if (input && !input.disabled) {
3686        handleAnswer(input);
3687      }
3688    });
3689    form.addEventListener("submit", function (event) {
3690      event.preventDefault();
3691    });
3692    if (back) {
3693      back.addEventListener("click", function () {
3694        var target = Math.max(0, current - 1);
3695        steps.slice(target).forEach(function (step) {
3696          toArray(step.querySelectorAll('input[type="radio"]')).forEach(function (input) {
3697            input.checked = false;
3698          });
3699          toArray(step.querySelectorAll("label.is-selected")).forEach(function (label) {
3700            label.classList.remove("is-selected");
3701          });
3702        });
3703        showStep(target, true);
3704      });
3705    }
3706    result.addEventListener("click", function (event) {
3707      var addButton = event.target.closest("[data-clc-quiz-add]");
3708      var productLink = event.target.closest("[data-clc-quiz-product-link]");
3709      var card = event.target.closest("[data-clc-quiz-product]");
3710
3711      if (addButton && card) {
3712        var added = addCartItem({
3713          productHandle: card.getAttribute("data-product-handle"),
3714          productId: card.getAttribute("data-product-id"),
3715          title: card.getAttribute("data-product-title"),
3716          variantId: card.getAttribute("data-variant-id"),
3717          variantTitle: "",
3718          sellingPlanId: "",
3719          sellingPlanName: "",
3720          quantity: 1,
3721          price: {
3722            amount: Number(card.getAttribute("data-price-amount")) || 0,
3723            currencyCode: card.getAttribute("data-price-currency") || "JPY"
3724          },
3725          imageUrl: card.getAttribute("data-image-url"),
3726          imageAlt: card.getAttribute("data-product-title")
3727        });
3728
3729        if (added) {
3730          setText(addButton, labels.addedToCart || "カートに追加しました");
3731          sendAnalyticsEvent("add_to_cart", {
3732            currency: card.getAttribute("data-price-currency") || "JPY",
3733            value: (Number(card.getAttribute("data-price-amount")) || 0) * added,
3734            items: [{
3735              item_id: card.getAttribute("data-product-id") || card.getAttribute("data-product-handle"),
3736              item_name: card.getAttribute("data-product-title"),
3737              price: Number(card.getAttribute("data-price-amount")) || 0,
3738              quantity: added
3739            }]
3740          });
3741          openUtilityPanel("clc-cart-panel", addButton);
3742          window.setTimeout(function () {
3743            setText(addButton, labels.addToCart || "カートに追加");
3744          }, 1800);
3745        }
3746      } else if (productLink && card) {
3747        sendAnalyticsEvent("select_item", {
3748          item_list_id: "coffee_finder_results",
3749          item_list_name: "コーヒー診断結果",
3750          items: [{
3751            item_id: card.getAttribute("data-product-id") || card.getAttribute("data-product-handle"),
3752            item_name: card.getAttribute("data-product-title"),
3753            price: Number(card.getAttribute("data-price-amount")) || 0,
3754            quantity: 1
3755          }]
3756        });
3757      }
3758    });
3759    if (reset) {
3760      reset.addEventListener("click", function () {
3761        window.clearTimeout(advanceTimer);
3762        form.reset();
3763        toArray(form.querySelectorAll("label.is-selected")).forEach(function (label) {
3764          label.classList.remove("is-selected");
3765        });
3766        productCards.forEach(function (card) { setHidden(card, true); });
3767        setHidden(result, true);
3768        setHidden(form, false);
3769        showStep(0, true);
3770      });
3771    }
3772    showStep(0, false);
3773  }
3774
3775  function initNavigation(storefront, index) {
3776    var toggle = storefront.querySelector(".clc-nav-toggle");
3777    var panel = storefront.querySelector(".clc-nav-panel");
3778    var disclosures;
3779
3780    if (!toggle || !panel || toggle.getAttribute("data-clc-initialized") === "true") {
3781      return;
3782    }
3783
3784    toggle.setAttribute("data-clc-initialized", "true");
3785
3786    if (!panel.id) {
3787      panel.id = "clc-nav-panel-" + String(index + 1);
3788    }
3789
3790    toggle.setAttribute("aria-controls", panel.id);
3791
3792    if (!toggle.hasAttribute("aria-expanded")) {
3793      toggle.setAttribute("aria-expanded", "false");
3794    }
3795
3796    disclosures = toArray(panel.querySelectorAll(".clc-nav-disclosure"));
3797
3798    var media = window.matchMedia ? window.matchMedia("(max-width: 1439px)") : null;
3799
3800    function isMobile() {
3801      return media ? media.matches : window.innerWidth <= 1439;
3802    }
3803
3804    function closeDisclosures(except) {
3805      disclosures.forEach(function (disclosure) {
3806        if (disclosure !== except && disclosure.open) {
3807          disclosure.open = false;
3808        }
3809      });
3810    }
3811
3812    function setOpen(open) {
3813      var expanded = Boolean(open) && isMobile();
3814      toggle.setAttribute("aria-expanded", expanded ? "true" : "false");
3815      toggle.setAttribute("aria-label", expanded ?
3816        (labels.closeMenu || "メニューを閉じる") :
3817        (labels.openMenu || "メニューを開く"));
3818      panel.classList.toggle("is-open", expanded);
3819      setHidden(panel, isMobile() && !expanded);
3820      document.body.classList.toggle("clc-navigation-open", expanded);
3821      if (!expanded) {
3822        closeDisclosures(null);
3823      }
3824    }
3825
3826    function synchronizeViewport() {
3827      if (isMobile()) {
3828        setOpen(toggle.getAttribute("aria-expanded") === "true");
3829      } else {
3830        toggle.setAttribute("aria-expanded", "false");
3831        panel.classList.remove("is-open");
3832        panel.hidden = false;
3833        document.body.classList.remove("clc-navigation-open");
3834        closeDisclosures(null);
3835      }
3836    }
3837
3838    disclosures.forEach(function (disclosure) {
3839      var summary = disclosure.querySelector(":scope > summary");
3840
3841      if (!summary) {
3842        return;
3843      }
3844
3845      disclosure.addEventListener("toggle", function () {
3846        if (disclosure.open) {
3847          closeDisclosures(disclosure);
3848        }
3849      });
3850    });
3851
3852    toggle.addEventListener("click", function () {
3853      setOpen(toggle.getAttribute("aria-expanded") !== "true");
3854    });
3855
3856    panel.addEventListener("click", function (event) {
3857      if (event.target.closest("a, [data-clc-panel-open]")) {
3858        closeDisclosures(null);
3859        if (isMobile()) {
3860          setOpen(false);
3861        }
3862      }
3863    });
3864
3865    storefront.addEventListener("keydown", function (event) {
3866      var openDisclosure;
3867
3868      if (event.key !== "Escape") {
3869        return;
3870      }
3871
3872      openDisclosure = panel.querySelector(".clc-nav-disclosure[open]");
3873      if (openDisclosure) {
3874        event.preventDefault();
3875        openDisclosure.open = false;
3876        if (openDisclosure.querySelector(":scope > summary")) {
3877          openDisclosure.querySelector(":scope > summary").focus();
3878        }
3879        return;
3880      }
3881
3882      if (toggle.getAttribute("aria-expanded") === "true") {
3883        event.preventDefault();
3884        setOpen(false);
3885        toggle.focus();
3886      }
3887    });
3888
3889    storefront.addEventListener("focusin", function (event) {
3890      if (
3891        isMobile() &&
3892        toggle.getAttribute("aria-expanded") === "true" &&
3893        event.target !== toggle &&
3894        !panel.contains(event.target)
3895      ) {
3896        setOpen(false);
3897      }
3898    });
3899
3900    document.addEventListener("pointerdown", function (event) {
3901      if (panel.contains(event.target) || toggle.contains(event.target)) {
3902        return;
3903      }
3904
3905      closeDisclosures(null);
3906      if (isMobile() && toggle.getAttribute("aria-expanded") === "true") {
3907        setOpen(false);
3908      }
3909    });
3910
3911    if (media && typeof media.addEventListener === "function") {
3912      media.addEventListener("change", synchronizeViewport);
3913    } else if (media && typeof media.addListener === "function") {
3914      media.addListener(synchronizeViewport);
3915    } else {
3916      window.addEventListener("resize", synchronizeViewport);
3917    }
3918
3919    synchronizeViewport();
3920  }
3921
3922  function initContactFormA11y(storefront) {
3923    toArray(storefront.querySelectorAll(".clc-contact-form .wpcf7-checkbox, .clc-contact-form .wpcf7-radio")).forEach(function (group, index) {
3924      var paragraph;
3925      var groupLabel;
3926      var labelId;
3927
3928      if (group.getAttribute("data-clc-group-ready") === "true") {
3929        return;
3930      }
3931      group.setAttribute("data-clc-group-ready", "true");
3932      paragraph = group.closest("p");
3933      groupLabel = paragraph ? paragraph.querySelector(":scope > label") : null;
3934      if (!groupLabel || !String(groupLabel.textContent || "").trim()) {
3935        return;
3936      }
3937
3938      labelId = groupLabel.id || "clc-contact-group-label-" + String(index + 1);
3939      groupLabel.id = labelId;
3940      group.setAttribute("role", "group");
3941      group.setAttribute("aria-labelledby", labelId);
3942      if (group.classList.contains("wpcf7-validates-as-required")) {
3943        group.setAttribute("aria-required", "true");
3944      }
3945    });
3946  }
3947
3948  function initialize() {
3949    var storefronts = toArray(document.querySelectorAll(".clc-storefront"));
3950
3951    storefronts.forEach(function (storefront, index) {
3952      allIncluding(storefront, ".clc-product").forEach(initProduct);
3953      allIncluding(storefront, ".clc-collection").forEach(initCollection);
3954      allIncluding(storefront, ".clc-rich-text").forEach(initRichText);
3955      allIncluding(storefront, "[data-clc-horizontal-scroll]").forEach(initHorizontalScroll);
3956      allIncluding(storefront, "[data-clc-home-hero]").forEach(initHomeHero);
3957      allIncluding(storefront, "[data-clc-home-testimonials]").forEach(initHomeTestimonials);
3958      allIncluding(storefront, "[data-clc-weekly-countdown]").forEach(initWeeklyCountdown);
3959      allIncluding(storefront, "[data-clc-home-product-list]").forEach(initHomeProductList);
3960      allIncluding(storefront, "[data-clc-cart]").forEach(initCart);
3961      allIncluding(storefront, "[data-clc-quiz]").forEach(initQuiz);
3962      initContactFormA11y(storefront);
3963      initUtilityPanels(storefront);
3964      allIncluding(storefront, "[data-clc-cart-reminder]").forEach(initCartReminder);
3965      initNavigation(storefront, index);
3966    });
3967    reconcilePendingCheckouts();
3968  }
3969
3970  window.addEventListener("pageshow", function (event) {
3971    if (event.persisted) {
3972      reconcilePendingCheckouts(true);
3973    }
3974  });
3975  window.addEventListener("storage", function (event) {
3976    if (event.storageArea !== window.localStorage) {
3977      return;
3978    }
3979    if (event.key === cartStorageKey) {
3980      cartStorageAvailable = true;
3981      document.dispatchEvent(new CustomEvent("clc:cart-changed", {
3982        detail: { items: readCart(), source: "storage" }
3983      }));
3984    }
3985    if (event.key === pendingCheckoutStorageKey) {
3986      pendingCheckoutStorageAvailable = true;
3987      reconcilePendingCheckouts(true);
3988    }
3989  });
3990  ready(initialize);
3991}());

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.