@gemboss/gem-framework 0.18.16 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +109 -1
  2. package/dist/SOURCE.json +3 -3
  3. package/dist/gem-framework.css +23 -1
  4. package/dist/gem-runtime.js +97 -54
  5. package/dist/index.js +2511 -44
  6. package/dist/motion/core.d.ts +208 -0
  7. package/dist/motion/core.js +1 -0
  8. package/dist/motion/orbit.d.ts +4 -0
  9. package/dist/motion/orbit.js +1 -0
  10. package/dist/motion/quick-view.d.ts +4 -0
  11. package/dist/motion/quick-view.js +1 -0
  12. package/dist/motion/rail.d.ts +5 -0
  13. package/dist/motion/rail.js +1 -0
  14. package/dist/orbit-base.css +39 -0
  15. package/dist/plugins/before_after.js +1 -1
  16. package/dist/plugins/bundle.js +1 -1
  17. package/dist/plugins/carousel.js +1 -1
  18. package/dist/plugins/cart-drawer.js +1 -1
  19. package/dist/plugins/context-adapter.js +1 -1
  20. package/dist/plugins/countdown.js +1 -1
  21. package/dist/plugins/delivery-estimate.d.ts +9 -0
  22. package/dist/plugins/delivery-estimate.js +1 -0
  23. package/dist/plugins/disclosure.js +1 -1
  24. package/dist/plugins/hero-slideshow.js +1 -1
  25. package/dist/plugins/img-in.js +1 -1
  26. package/dist/plugins/localization.js +1 -1
  27. package/dist/plugins/media-gallery.js +1 -1
  28. package/dist/plugins/mobile-menu.js +1 -1
  29. package/dist/plugins/offer-engine.js +1 -1
  30. package/dist/plugins/orbit.d.ts +9 -0
  31. package/dist/plugins/orbit.js +1 -0
  32. package/dist/plugins/personalize.d.ts +9 -0
  33. package/dist/plugins/personalize.js +1 -0
  34. package/dist/plugins/predictive-search.js +1 -1
  35. package/dist/plugins/print-preview.d.ts +9 -0
  36. package/dist/plugins/print-preview.js +1 -0
  37. package/dist/plugins/promo-popup.js +1 -1
  38. package/dist/plugins/quantity-input.js +1 -1
  39. package/dist/plugins/quick-view.js +1 -69
  40. package/dist/plugins/rail.d.ts +9 -0
  41. package/dist/plugins/rail.js +1 -0
  42. package/dist/plugins/recently-viewed.js +1 -1
  43. package/dist/plugins/reveal-on-scroll.js +1 -1
  44. package/dist/plugins/reviews.js +1 -1
  45. package/dist/plugins/runtime-slot-loader.js +1 -1
  46. package/dist/plugins/seasonal-effects.js +1 -1
  47. package/dist/plugins/size-guide.d.ts +9 -0
  48. package/dist/plugins/size-guide.js +38 -0
  49. package/dist/plugins/sticky-atc.js +1 -1
  50. package/dist/plugins/tabs.js +1 -1
  51. package/dist/plugins/variant-selects.js +1 -1
  52. package/dist/pod/core.d.ts +26 -0
  53. package/dist/pod/core.js +1 -0
  54. package/dist/pod/delivery-estimate.d.ts +28 -0
  55. package/dist/pod/delivery-estimate.js +1 -0
  56. package/dist/pod/personalize.d.ts +25 -0
  57. package/dist/pod/personalize.js +1 -0
  58. package/dist/pod/print-preview.d.ts +8 -0
  59. package/dist/pod/print-preview.js +1 -0
  60. package/dist/pod-buybox.css +55 -0
  61. package/dist/popup-shell.css +15 -0
  62. package/dist/primitives.css +4 -1
  63. package/dist/rail-base.css +48 -0
  64. package/dist/runtime-core.d.ts +2 -0
  65. package/dist/runtime-core.js +26 -15
  66. package/dist/section-gate.js +2522 -36
  67. package/dist/tokens.css +4 -0
  68. package/package.json +20 -1
@@ -186,6 +186,11 @@ function gemButtonGradientEndCss(accent = "var(--gem-color-accent)", accentText
186
186
  var GEM_FOCUS_RING_CSS_VAR = "--gem-focus-ring: 2px solid var(--gem-color-accent);";
187
187
  var GEM_TAP_MIN_CSS_VAR = "--gem-tap-min: 44px;";
188
188
  var GEM_FOCUS_OFFSET_CSS_VAR = "--gem-focus-offset: 3px;";
189
+ var GEM_STROKE_CSS_VARS = [
190
+ "--gem-stroke: calc(1px * var(--gem-style-border, 1));",
191
+ "--gem-stroke-thick: calc(2px * var(--gem-style-border, 1));"
192
+ ];
193
+ var GEM_UNDERLINE_THICKNESS_CSS_VAR = "--gem-underline-thickness: 2px;";
189
194
  var GEM_DERIVED_SPACE_STEPS = { "2xs": 0.125, ms: 0.75, "4xl": 7 };
190
195
  var GEM_DERIVED_SPACE_CSS_VARIABLES = Object.keys(GEM_DERIVED_SPACE_STEPS).map((key) => `--gem-space-${slugify(key)}`);
191
196
  function gemDerivedSpaceCssVars(spacing) {
@@ -330,15 +335,15 @@ function gemBandScheme(c, band) {
330
335
  const away = mixHex(g, hexRelativeLuminance(i) > hexRelativeLuminance(g) ? "#000000" : "#ffffff", 0.08);
331
336
  const surface = hexContrastRatio(i, toward) >= MIN_TEXT_CONTRAST ? toward : away;
332
337
  const grounds = [g, surface];
333
- const text = aaTextColor(i, grounds);
338
+ const text2 = aaTextColor(i, grounds);
334
339
  const accent = aaTextColor(c.accent, grounds);
335
340
  return {
336
341
  background: g,
337
342
  surface,
338
- text,
343
+ text: text2,
339
344
  muted: aaTextColor(mixHex(i, g, 0.3), grounds),
340
345
  accent,
341
- accentText: aaLabelColor(c.accentText, accent, [g, text]),
346
+ accentText: aaLabelColor(c.accentText, accent, [g, text2]),
342
347
  border: mixHex(g, i, 0.22),
343
348
  success: aaTextColor(c.success, grounds),
344
349
  warning: aaTextColor(c.warning, grounds),
@@ -563,7 +568,7 @@ function createGemTokenCss(tokens, selector = ":root", options = {}) {
563
568
  ...phone.layout.section ? [`--gem-layout-section-mobile: ${phone.layout.section};`] : [],
564
569
  ...toCssVars("component", tokens.component),
565
570
  ...toCssVars("morphology", tokens.morphology),
566
- // THE GOVERNED TYPE RAMP — --gem-h1..h6 / --gem-body / --gem-text-sm.
571
+ // THE GOVERNED TYPE RAMP — --gem-h1..h6 / --gem-body / --gem-text-sm / --gem-text-xs (26/10/2026).
567
572
  // gemcapture maps every measured font-size onto these names via `font_token`
568
573
  // (clone_kit._type_token), and the gem-framework contract documents them, but nothing in the
569
574
  // live framework defined them: a rebuild that followed the contract styled its headings with
@@ -578,7 +583,9 @@ function createGemTokenCss(tokens, selector = ":root", options = {}) {
578
583
  // type ramp and registered in validation.ts. Follows the accent, so presets retint it for free.
579
584
  GEM_FOCUS_RING_CSS_VAR,
580
585
  GEM_TAP_MIN_CSS_VAR,
581
- GEM_FOCUS_OFFSET_CSS_VAR
586
+ GEM_FOCUS_OFFSET_CSS_VAR,
587
+ ...GEM_STROKE_CSS_VARS,
588
+ GEM_UNDERLINE_THICKNESS_CSS_VAR
582
589
  ];
583
590
  const formBlock = gemFormCssBlock(tokens.form);
584
591
  const buttonCase = gemButtonCaseCss(tokens, selector);
@@ -626,6 +633,13 @@ function gemTypeRampCssVars(typography) {
626
633
  return [
627
634
  `--gem-type-base: ${typeRampValue(typography, "base") ?? "1rem"};`,
628
635
  `--gem-type-scale: ${typeRampValue(typography, "scale") ?? "1.22"};`,
636
+ // Chris 26/10/2026: P3 of the GemPages v7 scale (H1–H6, P1–P3 + Custom), one step under text-sm. P1 is body, P2 is
637
+ // text-sm; P3 had no name, so a caption or a fine-print line could only be a px count that no style pack retunes.
638
+ // Same pattern as text-sm, off the base rather than off text-sm: base ÷ scale², 10.75px at 16px and 1.22. Declared
639
+ // FIRST because this list is the ramp's order, small to large, and the seam's `steps` must equal it in order
640
+ // (check-token-steps-match-framework.ts). At the defaults it sits above gem_font_size's 10px floor; that floor bounds
641
+ // the px number only, so a plan with a steeper ratio (1.333: 9px) can take this step lower, as it can text-sm.
642
+ "--gem-text-xs: calc(var(--gem-type-base) / (var(--gem-type-scale) * var(--gem-type-scale)));",
629
643
  "--gem-text-sm: calc(var(--gem-type-base) / var(--gem-type-scale));",
630
644
  "--gem-body: var(--gem-type-base);",
631
645
  "--gem-h6: calc(var(--gem-type-base) * var(--gem-type-scale));",
@@ -735,6 +749,1468 @@ function gemFrameworkEditionCss() {
735
749
  return GEM_FRAMEWORK_EDITIONS.flatMap((e) => e.css.map((rule) => rule.replaceAll("&", gemFrameworkEditionScope(e.edition)))).join("\n");
736
750
  }
737
751
 
752
+ // ../../packages/gem-framework/src/motion/core.js
753
+ var clamp = (v, lo, hi) => v < lo ? lo : v > hi ? hi : v;
754
+ var lerp = (a, b, t) => a + (b - a) * t;
755
+ var mod = (v, n) => (v % n + n) % n;
756
+ function smoothstep(edge0, edge1, v) {
757
+ const t = clamp((v - edge0) / (edge1 - edge0), 0, 1);
758
+ return t * t * (3 - 2 * t);
759
+ }
760
+ var follow = (dt, tau) => tau <= 0 ? 1 : 1 - Math.exp(-dt / tau);
761
+ function spring(s, target, dt, stiffness, damping) {
762
+ const f = dt / 16.67;
763
+ s.v += (-(s.x - target) * stiffness - s.v * damping) * f;
764
+ s.x += s.v * f;
765
+ return s;
766
+ }
767
+ function ringDelta(a, b, n) {
768
+ let d = mod(b - a, n);
769
+ if (d > n / 2) d -= n;
770
+ return d;
771
+ }
772
+ function ringPose(i, n, pos, o) {
773
+ const theta = ringDelta(pos, i, n) * o.spread;
774
+ const hidden = Math.abs(theta) > Math.PI;
775
+ const depth = (1 - Math.cos(theta)) / 2;
776
+ return {
777
+ x: o.rx * Math.sin(theta),
778
+ y: -o.ry * depth,
779
+ depth,
780
+ scale: lerp(1, o.back, depth),
781
+ opacity: hidden ? 0 : lerp(1, o.fade, depth),
782
+ blur: o.blur * depth,
783
+ z: Math.round((1 - depth) * 100),
784
+ lean: Math.sin(theta) * o.lean,
785
+ hidden
786
+ };
787
+ }
788
+ var throwTarget = (pos, velocity, carry, maxSlots) => Math.round(pos + clamp(velocity * carry, -maxSlots, maxSlots));
789
+ var written = /* @__PURE__ */ new WeakMap();
790
+ function setStyle(el, prop, value) {
791
+ let rec = written.get(el);
792
+ if (!rec) written.set(el, rec = {});
793
+ if (rec[prop] === value) return;
794
+ rec[prop] = value;
795
+ if (value === "") el.style.removeProperty(prop);
796
+ else el.style.setProperty(prop, value);
797
+ }
798
+ var effects = /* @__PURE__ */ new Map();
799
+ function defineEffect(spec) {
800
+ const effect = { hook: `[data-gem-${spec.key}]`, load: "viewport", renderer: "dom", tier: "static", fallback: "markup", settings: {}, ...spec, declared: spec };
801
+ effects.set(effect.key, effect);
802
+ return effect;
803
+ }
804
+ function frameTask(ctx, el, step, options) {
805
+ const task = ctx.scheduler.add(step, options);
806
+ ctx.cleanup(() => task.remove());
807
+ observeVisibility(ctx, el, (shown) => task.visible(shown));
808
+ return task;
809
+ }
810
+ function observeVisibility(ctx, el, cb, margin) {
811
+ const IO = ctx.window.IntersectionObserver;
812
+ if (typeof IO !== "function") return;
813
+ new IO((entries) => cb(entries.some((e) => e.isIntersecting)), { rootMargin: margin || "120px" }).observe(el);
814
+ }
815
+ function observeSize(ctx, el, cb) {
816
+ const RO = ctx.window.ResizeObserver;
817
+ if (typeof RO === "function") new RO(() => cb()).observe(el);
818
+ else ctx.on(ctx.window, "resize", () => cb());
819
+ }
820
+ function horizontalDrag(ctx, el, h) {
821
+ let id = null;
822
+ let x0 = 0;
823
+ let y0 = 0;
824
+ let decided = false;
825
+ let horizontal = false;
826
+ let moved = 0;
827
+ let samples = [];
828
+ const release = (e, fire) => {
829
+ if (e.pointerId !== id) return;
830
+ id = null;
831
+ const first = samples[0];
832
+ const last = samples[samples.length - 1];
833
+ const span = last.t - first.t;
834
+ const vx = span > 0 && e.timeStamp - last.t < 80 ? (last.x - first.x) / span : 0;
835
+ if (fire && horizontal) h.end(vx, moved, e);
836
+ else h.end(0, 0, e);
837
+ };
838
+ ctx.on(el, "pointerdown", (e) => {
839
+ if (e.button !== 0 || id !== null) return;
840
+ id = e.pointerId;
841
+ x0 = e.clientX;
842
+ y0 = e.clientY;
843
+ decided = horizontal = false;
844
+ moved = 0;
845
+ samples = [{ x: e.clientX, t: e.timeStamp }];
846
+ h.start(e);
847
+ });
848
+ ctx.on(el, "pointermove", (e) => {
849
+ if (e.pointerId !== id) return;
850
+ const dx = e.clientX - x0;
851
+ if (!decided && Math.hypot(dx, e.clientY - y0) > 6) {
852
+ decided = true;
853
+ horizontal = Math.abs(dx) > Math.abs(e.clientY - y0);
854
+ if (!horizontal) return release(e, false);
855
+ try {
856
+ el.setPointerCapture(e.pointerId);
857
+ } catch {
858
+ }
859
+ }
860
+ if (!horizontal) return;
861
+ moved = Math.max(moved, Math.abs(dx));
862
+ samples.push({ x: e.clientX, t: e.timeStamp });
863
+ while (samples.length > 2 && e.timeStamp - samples[0].t > 90) samples.shift();
864
+ h.move(dx, e);
865
+ });
866
+ ctx.on(el, "pointerup", (e) => release(e, true));
867
+ ctx.on(el, "pointercancel", (e) => release(e, true));
868
+ }
869
+ function wheelSteps(ctx, el, onStep) {
870
+ let acc = 0;
871
+ let lock = 0;
872
+ ctx.on(
873
+ el,
874
+ "wheel",
875
+ (e) => {
876
+ if (Math.abs(e.deltaX) <= Math.abs(e.deltaY) || e.target.closest && e.target.closest("dialog")) return;
877
+ e.preventDefault();
878
+ acc += e.deltaX;
879
+ if (e.timeStamp < lock || Math.abs(acc) < 24) return;
880
+ onStep(Math.sign(acc));
881
+ acc = 0;
882
+ lock = e.timeStamp + 280;
883
+ },
884
+ { passive: false }
885
+ );
886
+ }
887
+ function arrowKeys(ctx, el, { step, activate, accept }) {
888
+ ctx.on(el, "keydown", (e) => {
889
+ if (accept && !accept(e)) return;
890
+ if (e.key === "ArrowRight" || e.key === "ArrowLeft") step(e.key === "ArrowRight" ? 1 : -1);
891
+ else if (!activate || e.key !== "Enter" && e.key !== " " || activate(e) === false) return;
892
+ e.preventDefault();
893
+ });
894
+ }
895
+ function flip(el, from, options) {
896
+ if (!from || !from.width || typeof el.animate !== "function") return null;
897
+ const box = el.getBoundingClientRect();
898
+ if (!box.height) return null;
899
+ const away = { transform: `translate(${from.left + from.width / 2 - box.left - box.width / 2}px, ${from.top + from.height / 2 - box.top - box.height / 2}px) scale(${from.height / box.height})` };
900
+ const home = { transform: "none" };
901
+ return el.animate(options.back ? [home, away] : [away, home], { duration: options.duration, easing: options.easing, fill: "both" });
902
+ }
903
+ function loadImage(doc, image) {
904
+ const img = doc.createElement("img");
905
+ if (image.srcset) img.srcset = image.srcset;
906
+ img.src = image.src;
907
+ return typeof img.decode === "function" ? img.decode().then(() => img, () => img) : Promise.resolve(img);
908
+ }
909
+ function putImage(img, image) {
910
+ if (!img || !image || !image.src || img.getAttribute("src") === image.src) return;
911
+ if (image.srcset) img.setAttribute("srcset", image.srcset);
912
+ else img.removeAttribute("srcset");
913
+ img.setAttribute("src", image.src);
914
+ }
915
+ function swapText(el, text2, still) {
916
+ if (!el || el.textContent === text2) return;
917
+ if (still || typeof el.animate !== "function") {
918
+ el.textContent = text2;
919
+ return;
920
+ }
921
+ const out = el.animate([{ opacity: 1 }, { opacity: 0, transform: "translateY(-6px)" }], { duration: 140, fill: "forwards" });
922
+ out.onfinish = () => {
923
+ el.textContent = text2;
924
+ out.cancel();
925
+ el.animate([{ opacity: 0, transform: "translateY(8px)" }, { opacity: 1, transform: "none" }], { duration: 220, easing: "cubic-bezier(.2,.7,.2,1)" });
926
+ };
927
+ }
928
+ function readProduct(el) {
929
+ try {
930
+ const p = JSON.parse(el.getAttribute("data-gem-product") || "null");
931
+ return p && p.image && p.image.src ? p : null;
932
+ } catch {
933
+ return null;
934
+ }
935
+ }
936
+ function formatMoney(cents, format) {
937
+ const value = (Number(cents) || 0) / 100;
938
+ const group = (str, sep) => str.replace(/\B(?=(\d{3})+(?!\d))/g, sep);
939
+ const [int, dec] = value.toFixed(2).split(".");
940
+ return (format || "${{amount}}").replace(/\{\{\s*(\w+)\s*\}\}/, (_, key) => {
941
+ if (key === "amount_no_decimals") return group(String(Math.round(value)), ",");
942
+ if (key === "amount_with_comma_separator") return `${group(int, ".")},${dec}`;
943
+ if (key === "amount_no_decimals_with_comma_separator") return group(String(Math.round(value)), ".");
944
+ return `${group(int, ",")}.${dec}`;
945
+ });
946
+ }
947
+ function priceText(p, el, which) {
948
+ const compare = which === "compare";
949
+ const words = compare ? p.compare_at_text : p.price_text;
950
+ if (words != null) return String(words);
951
+ const cents = compare ? p.compare_at : p.price;
952
+ if (cents == null || cents === "") return "";
953
+ if (typeof cents !== "number") return String(cents);
954
+ const holder = el && el.closest ? el.closest("[data-gem-money-format]") : null;
955
+ return formatMoney(cents, holder ? holder.getAttribute("data-gem-money-format") : void 0);
956
+ }
957
+
958
+ // ../../packages/gem-framework/src/motion/orbit.js
959
+ var pad = (v) => String(v).padStart(2, "0");
960
+ function mountOrbit(root, ctx) {
961
+ const items = [];
962
+ const products = [];
963
+ const hits = [];
964
+ const imgs = [];
965
+ root.querySelectorAll("[data-gem-orbit-item]").forEach((item) => {
966
+ const product = readProduct(item);
967
+ if (!product) return;
968
+ items.push(item);
969
+ products.push(product);
970
+ hits.push(item.querySelector(".gem-orbit__hit") || item);
971
+ imgs.push(item.querySelector("img"));
972
+ });
973
+ const n = items.length;
974
+ if (!n) return false;
975
+ const part = (name) => root.querySelector(".gem-orbit__" + name);
976
+ const stage = part("stage") || root;
977
+ const track = part("track");
978
+ const style = ctx.renderer.style;
979
+ let s = ctx.settings;
980
+ const start = Math.min(n - 1, s.start);
981
+ let pos = start;
982
+ let target = start;
983
+ let dragFrom = 0;
984
+ let dragging = false;
985
+ let rendered = false;
986
+ let front = -1;
987
+ let ring = null;
988
+ let slotPx = 60;
989
+ const full = () => ctx.tier() === "full";
990
+ const layout = () => {
991
+ s = ctx.reread();
992
+ const w = stage.clientWidth;
993
+ ring = {
994
+ rx: w * s.rx,
995
+ ry: stage.clientHeight * s.tilt,
996
+ back: s.back,
997
+ fade: s.fade,
998
+ blur: full() ? s.blur : 0,
999
+ lean: s.lean,
1000
+ spread: (s.spread || 360 / n) * Math.PI / 180
1001
+ };
1002
+ slotPx = Math.max(60, ring.rx * ring.spread / 1.2);
1003
+ };
1004
+ const setTint = (tint) => {
1005
+ if (!s.tint) return;
1006
+ if (tint) root.style.setProperty("--gem-motion-tint", tint);
1007
+ else root.style.removeProperty("--gem-motion-tint");
1008
+ };
1009
+ const frontIndex = () => mod(Math.round(pos), n);
1010
+ const indexOf = (el) => items.findIndex((item) => item === el || item.contains(el));
1011
+ const updateFront = (initial) => {
1012
+ const i = frontIndex();
1013
+ if (i === front) return;
1014
+ front = i;
1015
+ const p = products[i];
1016
+ items.forEach((el, k) => {
1017
+ if (k === i) el.setAttribute("aria-current", "true");
1018
+ else el.removeAttribute("aria-current");
1019
+ hits[k].tabIndex = k === i ? 0 : -1;
1020
+ });
1021
+ if (!initial) {
1022
+ const still = ctx.tier() === "static";
1023
+ const price = priceText(p, root);
1024
+ swapText(part("name"), p.title || "", still);
1025
+ swapText(part("price"), price, still);
1026
+ const count = part("count");
1027
+ if (count) count.textContent = pad(i + 1) + " / " + pad(n);
1028
+ const bar = part("bar");
1029
+ if (bar) bar.style.transform = "scaleX(" + (i + 1) / n + ")";
1030
+ const live = part("live");
1031
+ const words = { index: i + 1, count: n, title: p.title || "", price };
1032
+ if (live) live.textContent = s.live.replace(/\{(\w+)\}/g, (m, k) => Object.hasOwn(words, k) ? String(words[k]) : m);
1033
+ setTint(p.tint);
1034
+ }
1035
+ ctx.emit("gem:motion:front", { index: i, product: p });
1036
+ };
1037
+ const frame = (dt, time) => {
1038
+ let moving = dragging;
1039
+ if (!dragging) {
1040
+ const d = target - pos;
1041
+ if (ctx.tier() === "static" || Math.abs(d) < 8e-4) pos = target;
1042
+ else {
1043
+ pos += d * follow(dt, s.ease);
1044
+ moving = true;
1045
+ }
1046
+ }
1047
+ const drift = s.drift && full();
1048
+ for (let i = 0; i < n; i++) {
1049
+ const p = ringPose(i, n, pos, ring);
1050
+ const bob = drift ? Math.sin(time / 1100 + i * 1.9) * 5 * (1 - p.depth) : 0;
1051
+ const el = items[i];
1052
+ style(el, "transform", `translate3d(${p.x.toFixed(1)}px, ${(p.y + bob).toFixed(1)}px, 0) scale(${p.scale.toFixed(3)}) rotate(${p.lean.toFixed(2)}deg)`);
1053
+ style(el, "opacity", p.opacity.toFixed(3));
1054
+ style(el, "z-index", String(p.z));
1055
+ const blur = Math.round(p.blur * 2) / 2;
1056
+ style(el, "filter", blur > 0 ? `blur(${blur}px)` : "none");
1057
+ style(el, "visibility", p.hidden ? "hidden" : "visible");
1058
+ }
1059
+ rendered = true;
1060
+ updateFront(false);
1061
+ return moving || drift;
1062
+ };
1063
+ const refresh = () => {
1064
+ layout();
1065
+ if (rendered) task.kick();
1066
+ };
1067
+ const task = frameTask(ctx, root, frame, { onDegrade: refresh });
1068
+ const goTo = (index) => {
1069
+ const from = Math.round(target);
1070
+ target = from + ringDelta(from, mod(index, n), n);
1071
+ task.kick();
1072
+ };
1073
+ hits.forEach((hit, i) => {
1074
+ ctx.on(hit, "click", (event) => {
1075
+ if (i === frontIndex()) return;
1076
+ event.preventDefault();
1077
+ goTo(i);
1078
+ });
1079
+ });
1080
+ root.querySelectorAll("[data-gem-orbit-step]").forEach((button) => {
1081
+ ctx.on(button, "click", () => goTo(target + (Number(button.getAttribute("data-gem-orbit-step")) || 0)));
1082
+ });
1083
+ if (track) {
1084
+ ctx.on(track, "click", (event) => {
1085
+ const r = track.getBoundingClientRect();
1086
+ if (r.width) goTo(Math.round((event.clientX - r.left) / r.width * (n - 1)));
1087
+ });
1088
+ }
1089
+ horizontalDrag(ctx, stage, {
1090
+ start: () => {
1091
+ dragFrom = pos;
1092
+ },
1093
+ move: (dx) => {
1094
+ dragging = true;
1095
+ root.setAttribute("data-gem-state", "dragging");
1096
+ pos = dragFrom - dx / slotPx;
1097
+ task.kick();
1098
+ },
1099
+ end: (vx, moved) => {
1100
+ if (!dragging) return;
1101
+ dragging = false;
1102
+ root.setAttribute("data-gem-state", "ready");
1103
+ target = moved < 4 ? Math.round(pos) : throwTarget(pos, -vx / slotPx, s.carry, Math.min(3, Math.ceil(n / 2)));
1104
+ task.kick();
1105
+ }
1106
+ });
1107
+ wheelSteps(ctx, root, (d) => goTo(target + d));
1108
+ arrowKeys(ctx, root, {
1109
+ accept: (e) => e.target === root || hits.includes(e.target),
1110
+ step: (d) => goTo(target + d),
1111
+ activate: (e) => e.target === root ? hits[frontIndex()].click() : false
1112
+ });
1113
+ layout();
1114
+ observeSize(ctx, root, refresh);
1115
+ root.gemQuickViewHost = {
1116
+ key: "orbit",
1117
+ products,
1118
+ indexOf,
1119
+ opens: (el) => indexOf(el) === frontIndex(),
1120
+ imageFor: (i) => imgs[i] || null,
1121
+ triggerFor: (i) => hits[i] || null,
1122
+ goTo,
1123
+ setOpen: (open) => root.setAttribute("data-gem-state", open ? "open" : "ready"),
1124
+ setImage: (i, image) => putImage(imgs[i], image),
1125
+ setTint
1126
+ };
1127
+ root.setAttribute("data-gem-quick-view-host", "orbit");
1128
+ root.setAttribute("data-gem-state", "ready");
1129
+ updateFront(true);
1130
+ if (s.drift && full()) task.kick();
1131
+ return () => {
1132
+ delete root.gemQuickViewHost;
1133
+ root.removeAttribute("data-gem-quick-view-host");
1134
+ root.removeAttribute("data-gem-state");
1135
+ };
1136
+ }
1137
+ var orbitEffect = defineEffect({
1138
+ key: "orbit",
1139
+ hook: "[data-gem-orbit]",
1140
+ load: "viewport",
1141
+ renderer: "dom",
1142
+ tier: "static",
1143
+ fallback: "markup",
1144
+ budget: { gzip: 6144 },
1145
+ settings: {
1146
+ rx: { type: "number", min: 0.1, max: 0.5, step: 0.01, default: 0.36, css: true, label: "Ring width (share of the stage width)" },
1147
+ tilt: { type: "number", min: 0, max: 0.5, step: 0.01, default: 0.3, css: true, label: "Tilt (rise of the back, share of the stage height)" },
1148
+ size: { type: "number", min: 0.2, max: 0.9, step: 0.01, default: 0.62, css: true, label: "Piece size (share of the stage height)" },
1149
+ back: { type: "number", min: 0.1, max: 1, step: 0.01, default: 0.42, css: true, label: "Scale at the back" },
1150
+ fade: { type: "number", min: 0, max: 1, step: 0.01, default: 0.35, css: true, label: "Opacity at the back" },
1151
+ blur: { type: "number", min: 0, max: 8, step: 0.5, default: 2.5, css: true, label: "Blur at the back (px)" },
1152
+ lean: { type: "number", min: 0, max: 20, step: 1, default: 5, css: true, label: "Lean at the sides (degrees)" },
1153
+ spread: { type: "number", min: 0, max: 180, step: 1, default: 0, css: true, label: "Degrees between pieces (0: round the full ring)" },
1154
+ start: { type: "integer", min: 0, default: 0, css: true, label: "Piece in front at first" },
1155
+ ease: { type: "number", min: 40, max: 600, step: 10, default: 140, label: "Settle time (ms)" },
1156
+ carry: { type: "number", min: 0, max: 800, step: 10, default: 240, label: "How far a throw carries (ms)" },
1157
+ tint: { type: "boolean", default: true, label: "Tint the band with the front piece's colour" },
1158
+ drift: { type: "boolean", default: false, label: "Idle float (full tier only)" },
1159
+ live: { type: "text", default: "{index} / {count}: {title}, {price}", label: "What a screen reader hears when the front piece changes" }
1160
+ },
1161
+ mount: mountOrbit
1162
+ });
1163
+
1164
+ // ../../packages/gem-framework/src/motion/rail.js
1165
+ var COMPACT_BELOW = 700;
1166
+ var ROD_TOP = 14;
1167
+ function createTurn(el) {
1168
+ if (!el) return null;
1169
+ const side = el.querySelector(".gem-turn__side");
1170
+ const front = el.querySelector(".gem-turn__front");
1171
+ const sheets = el.querySelectorAll(".gem-turn__sheet");
1172
+ const frames = Math.floor(Number(el.getAttribute("data-gem-turn-frames"))) || 0;
1173
+ const cols = Math.floor(Number(el.getAttribute("data-gem-turn-cols"))) || 0;
1174
+ const mode = sheets.length > 1 && frames > 1 && cols > 0 ? "sprite" : side && front ? "pair" : front ? "flip" : null;
1175
+ if (!mode) return null;
1176
+ const rows = Math.ceil(frames / Math.max(1, cols)) || 1;
1177
+ const place = (sheet, f) => setStyle(sheet, "transform", `translate(${(-(f % cols) * 100 / cols).toFixed(3)}%, ${(-Math.floor(f / cols) * 100 / rows).toFixed(3)}%)`);
1178
+ let last = -1;
1179
+ return {
1180
+ mode,
1181
+ /** what the quick view flies out of */
1182
+ image: mode === "sprite" ? el : front,
1183
+ set(t) {
1184
+ const q = Math.round(t * 1e3) / 1e3;
1185
+ if (q === last) return;
1186
+ last = q;
1187
+ if (mode === "pair") {
1188
+ setStyle(side, "transform", `perspective(900px) rotateY(${(-82 * q).toFixed(1)}deg)`);
1189
+ setStyle(side, "opacity", (1 - smoothstep(0.42, 0.78, q)).toFixed(3));
1190
+ setStyle(front, "opacity", smoothstep(0.45, 0.82, q).toFixed(3));
1191
+ setStyle(front, "transform", `scale(${lerp(0.9, 1, q).toFixed(3)})`);
1192
+ } else if (mode === "sprite") {
1193
+ const f = q * (frames - 1);
1194
+ const a = Math.floor(f);
1195
+ place(sheets[0], a);
1196
+ place(sheets[1], Math.min(frames - 1, a + 1));
1197
+ setStyle(sheets[1], "opacity", (f - a).toFixed(3));
1198
+ } else {
1199
+ setStyle(front, "transform", `perspective(900px) rotateY(${lerp(-74, 0, q).toFixed(1)}deg) scale(${lerp(0.86, 1, q).toFixed(3)})`);
1200
+ }
1201
+ },
1202
+ /** Another colour of the same piece (picked in the quick view). A sprite keeps its sheet. */
1203
+ setImages(image, sideImage) {
1204
+ if (mode === "sprite") return;
1205
+ putImage(front, image);
1206
+ putImage(side, sideImage);
1207
+ }
1208
+ };
1209
+ }
1210
+ function mountRail(root, ctx) {
1211
+ const items = [];
1212
+ const products = [];
1213
+ const hits = [];
1214
+ const bodies = [];
1215
+ const turns = [];
1216
+ root.querySelectorAll("[data-gem-rail-item]").forEach((item) => {
1217
+ const product = readProduct(item);
1218
+ if (!product) return;
1219
+ const hit = item.querySelector(".gem-rail__hit") || item;
1220
+ items.push(item);
1221
+ products.push(product);
1222
+ hits.push(hit);
1223
+ bodies.push(item.querySelector(".gem-rail__body") || hit);
1224
+ turns.push(createTurn(item.querySelector(".gem-turn")));
1225
+ });
1226
+ const n = items.length;
1227
+ if (!n) return false;
1228
+ const stage = root.querySelector(".gem-rail__stage") || root;
1229
+ const track = root.querySelector(".gem-rail__track") || stage;
1230
+ const caption = root.querySelector(".gem-rail__caption");
1231
+ const rod = root.querySelector(".gem-rail__rod path");
1232
+ const style = ctx.renderer.style;
1233
+ const turn = items.map(() => 0);
1234
+ const hangers = items.map(() => ({ x: 0, v: 0 }));
1235
+ let s = ctx.settings;
1236
+ let goal = items.map(() => 0);
1237
+ let focused = -1;
1238
+ let compact = false;
1239
+ let scrolled = false;
1240
+ let rendered = false;
1241
+ let w = 0;
1242
+ let h = 0;
1243
+ let lastX = 0;
1244
+ let lastT = 0;
1245
+ let dpx = 0;
1246
+ const still = () => ctx.tier() === "static";
1247
+ const rodPath = () => `M0 ${ROD_TOP} Q50 ${Math.round(ROD_TOP * 10 + s.sag * 2e3) / 10} 100 ${ROD_TOP}`;
1248
+ if (rod && rod.getAttribute("d") !== rodPath()) rod.setAttribute("d", rodPath());
1249
+ const layout = () => {
1250
+ s = ctx.reread();
1251
+ const was = compact;
1252
+ compact = root.clientWidth < COMPACT_BELOW;
1253
+ w = stage.clientWidth;
1254
+ h = stage.clientHeight;
1255
+ if (compact && !was) items.forEach((el) => style(el, "transform", ""));
1256
+ };
1257
+ const rodY = (x) => {
1258
+ const t = clamp(x / Math.max(1, w), 0, 1);
1259
+ return h * ROD_TOP / 100 + 4 * s.sag * h * t * (1 - t);
1260
+ };
1261
+ const slots = () => {
1262
+ const weights = turn.map((t) => 1 + s.grow * t * t);
1263
+ const unit = w * 0.92 / weights.reduce((a, b) => a + b, 0);
1264
+ let acc = w * 0.04;
1265
+ return weights.map((x) => {
1266
+ const c = acc + x * unit / 2;
1267
+ acc += x * unit;
1268
+ return { c, width: x * unit };
1269
+ });
1270
+ };
1271
+ const nearest = (x) => {
1272
+ let best = -1;
1273
+ let bestD = Infinity;
1274
+ slots().forEach(({ c, width }, i) => {
1275
+ const d = Math.abs(c - x);
1276
+ if (d < bestD && d < width * 1.2) {
1277
+ best = i;
1278
+ bestD = d;
1279
+ }
1280
+ });
1281
+ return best;
1282
+ };
1283
+ const setTint = (tint) => {
1284
+ if (!s.tint) return;
1285
+ if (tint) root.style.setProperty("--gem-motion-tint", tint);
1286
+ else root.style.removeProperty("--gem-motion-tint");
1287
+ };
1288
+ const setFocus = (i, initial) => {
1289
+ if (i === focused) return;
1290
+ focused = i;
1291
+ goal = goal.map((_, k) => k === i ? 1 : 0);
1292
+ const p = products[i];
1293
+ if (initial) {
1294
+ turn[i] = 1;
1295
+ if (turns[i]) turns[i].set(1);
1296
+ return;
1297
+ }
1298
+ if (caption) caption.textContent = p ? [p.title, priceText(p, root)].filter(Boolean).join(" \xB7 ") : "";
1299
+ if (p) {
1300
+ setTint(p.tint);
1301
+ ctx.emit("gem:motion:front", { index: i, product: p });
1302
+ }
1303
+ task.kick();
1304
+ };
1305
+ const frame = (dt) => {
1306
+ const k = follow(dt, s.ease);
1307
+ let moving = false;
1308
+ if (compact && scrolled) {
1309
+ const mid = track.scrollLeft + track.clientWidth / 2;
1310
+ let best = 0;
1311
+ items.forEach((el, i) => {
1312
+ if (Math.abs(el.offsetLeft + el.offsetWidth / 2 - mid) < Math.abs(items[best].offsetLeft + items[best].offsetWidth / 2 - mid)) best = i;
1313
+ });
1314
+ if (best !== focused) setFocus(best);
1315
+ }
1316
+ for (let i = 0; i < n; i++) {
1317
+ const d = goal[i] - turn[i];
1318
+ turn[i] = still() || Math.abs(d) < 2e-3 ? goal[i] : turn[i] + d * k;
1319
+ if (turn[i] !== goal[i]) moving = true;
1320
+ const hanger = hangers[i];
1321
+ if (still()) hanger.x = hanger.v = 0;
1322
+ else {
1323
+ spring(hanger, 0, dt, 0.02, 0.09);
1324
+ hanger.x = clamp(hanger.x, -9, 9);
1325
+ if (Math.abs(hanger.x) > 0.02 || Math.abs(hanger.v) > 0.02) moving = true;
1326
+ else hanger.x = hanger.v = 0;
1327
+ }
1328
+ if (turns[i]) turns[i].set(turn[i]);
1329
+ style(bodies[i], "transform", `rotate(${hanger.x.toFixed(2)}deg)`);
1330
+ }
1331
+ if (!compact) {
1332
+ slots().forEach(({ c }, i) => {
1333
+ style(items[i], "transform", `translate3d(${c.toFixed(1)}px, ${rodY(c).toFixed(1)}px, 0)`);
1334
+ style(items[i], "z-index", String(Math.round(10 + turn[i] * 10)));
1335
+ });
1336
+ }
1337
+ rendered = true;
1338
+ return moving;
1339
+ };
1340
+ const refresh = () => {
1341
+ layout();
1342
+ if (rendered) task.kick();
1343
+ };
1344
+ const task = frameTask(ctx, root, frame, { onDegrade: refresh });
1345
+ hits.forEach((hit, i) => ctx.on(hit, "focus", () => setFocus(i)));
1346
+ ctx.on(stage, "click", (event) => {
1347
+ if (compact || hits.some((hit) => hit.contains(event.target))) return;
1348
+ const i = nearest(event.clientX - stage.getBoundingClientRect().left);
1349
+ if (i >= 0) hits[i].click();
1350
+ });
1351
+ ctx.on(stage, "pointermove", (event) => {
1352
+ if (compact || event.pointerType !== "mouse") return;
1353
+ const x = event.clientX - stage.getBoundingClientRect().left;
1354
+ dpx = clamp((x - lastX) / Math.max(1, event.timeStamp - lastT), -3, 3);
1355
+ lastX = x;
1356
+ lastT = event.timeStamp;
1357
+ if (!still() && s.swing) {
1358
+ slots().forEach(({ c, width }, i) => {
1359
+ hangers[i].v += dpx * 0.35 * s.swing * clamp(1 - Math.abs(c - x) / (width * 2.5), 0, 1);
1360
+ });
1361
+ }
1362
+ setFocus(nearest(x));
1363
+ task.kick();
1364
+ });
1365
+ ctx.on(stage, "pointerleave", () => {
1366
+ if (!compact) setFocus(-1);
1367
+ });
1368
+ ctx.on(track, "keydown", (event) => {
1369
+ if (event.key !== "ArrowRight" && event.key !== "ArrowLeft") return;
1370
+ event.preventDefault();
1371
+ hits[clamp(Math.max(0, focused) + (event.key === "ArrowRight" ? 1 : -1), 0, n - 1)].focus();
1372
+ });
1373
+ ctx.on(
1374
+ track,
1375
+ "scroll",
1376
+ () => {
1377
+ if (!compact) return;
1378
+ scrolled = true;
1379
+ task.kick();
1380
+ },
1381
+ { passive: true }
1382
+ );
1383
+ layout();
1384
+ observeSize(ctx, root, refresh);
1385
+ if (compact) setFocus(0, true);
1386
+ const goTo = (index) => {
1387
+ const i = (index % n + n) % n;
1388
+ if (compact && typeof track.scrollTo === "function") {
1389
+ track.scrollTo({ left: items[i].offsetLeft - (track.clientWidth - items[i].offsetWidth) / 2, behavior: still() ? "auto" : "smooth" });
1390
+ }
1391
+ setFocus(i);
1392
+ };
1393
+ root.gemQuickViewHost = {
1394
+ key: "rail",
1395
+ products,
1396
+ indexOf: (el) => items.findIndex((item) => item === el || item.contains(el)),
1397
+ opens: () => true,
1398
+ imageFor: (i) => (turns[i] ? turns[i].image : items[i] && items[i].querySelector("img")) || null,
1399
+ triggerFor: (i) => hits[i] || null,
1400
+ goTo,
1401
+ setOpen: (open) => root.setAttribute("data-gem-state", open ? "open" : "ready"),
1402
+ setImage: (i, image, side) => turns[i] && turns[i].setImages(image, side),
1403
+ setTint
1404
+ };
1405
+ root.setAttribute("data-gem-quick-view-host", "rail");
1406
+ root.setAttribute("data-gem-state", "ready");
1407
+ return () => {
1408
+ delete root.gemQuickViewHost;
1409
+ root.removeAttribute("data-gem-quick-view-host");
1410
+ root.removeAttribute("data-gem-state");
1411
+ };
1412
+ }
1413
+ var railEffect = defineEffect({
1414
+ key: "rail",
1415
+ hook: "[data-gem-rail]",
1416
+ load: "viewport",
1417
+ renderer: "dom",
1418
+ tier: "static",
1419
+ fallback: "markup",
1420
+ budget: { gzip: 6144 },
1421
+ settings: {
1422
+ sag: { type: "number", min: 0, max: 0.2, step: 0.01, default: 0.07, css: true, label: "How far the rod dips (share of the stage height)" },
1423
+ height: { type: "number", min: 0.3, max: 0.85, step: 0.01, default: 0.7, css: true, label: "Garment height (share of the stage height)" },
1424
+ grow: { type: "number", min: 0, max: 3, step: 0.1, default: 1.5, label: "How much wider a turned piece gets" },
1425
+ swing: { type: "number", min: 0, max: 2, step: 0.1, default: 1, label: "Hanger swing (0: off)" },
1426
+ ease: { type: "number", min: 40, max: 600, step: 10, default: 120, label: "Turn time (ms)" },
1427
+ tint: { type: "boolean", default: true, label: "Tint the band with the turned piece's colour" }
1428
+ },
1429
+ mount: mountRail
1430
+ });
1431
+
1432
+ // ../../packages/gem-framework/src/motion/quick-view.js
1433
+ var EASE_OUT = "cubic-bezier(.2,.75,.15,1)";
1434
+ var LABELS = {
1435
+ add: "Add to bag",
1436
+ adding: "Adding\u2026",
1437
+ added: "Added",
1438
+ added_status: "added to bag",
1439
+ failed: "Could not add to bag",
1440
+ choose: "Choose an option",
1441
+ sold_out: "Sold out",
1442
+ details: "Full details",
1443
+ previous: "Previous product",
1444
+ next: "Next product",
1445
+ close: "Close",
1446
+ colour: "Colour",
1447
+ size: "Size"
1448
+ };
1449
+ var panelCount = 0;
1450
+ var pad2 = (v) => String(v).padStart(2, "0");
1451
+ var text = (v) => v == null ? "" : String(v);
1452
+ function cardHost(trigger) {
1453
+ const own = trigger.closest("[data-gem-product]");
1454
+ const group = trigger.closest("[data-gem-quick-view-group]");
1455
+ const products = [];
1456
+ const sources = [];
1457
+ let index = -1;
1458
+ for (const el of group ? group.querySelectorAll("[data-gem-product]") : own ? [own] : []) {
1459
+ const product = readProduct(el);
1460
+ if (!product) continue;
1461
+ if (el === own) index = products.length;
1462
+ products.push(product);
1463
+ sources.push(el);
1464
+ }
1465
+ return {
1466
+ index,
1467
+ host: {
1468
+ key: "card",
1469
+ products,
1470
+ imageFor: (i) => sources[i] ? sources[i].querySelector("img") : null,
1471
+ triggerFor: (i) => sources[i] ? sources[i].matches("[data-gem-quick-view-trigger]") ? sources[i] : sources[i].querySelector("[data-gem-quick-view-trigger]") : null,
1472
+ goTo: () => {
1473
+ },
1474
+ setOpen: () => {
1475
+ }
1476
+ }
1477
+ };
1478
+ }
1479
+ function sessionOf(ctx) {
1480
+ if (ctx.session.quickView) return ctx.session.quickView;
1481
+ const shared = { panels: /* @__PURE__ */ new Map(), pending: null };
1482
+ ctx.session.quickView = shared;
1483
+ ctx.on(ctx.window, "gem:cart:added", () => shared.pending && shared.pending(true));
1484
+ ctx.on(ctx.window, "gem:cart:add-failed", () => shared.pending && shared.pending(false));
1485
+ ctx.cleanup(() => {
1486
+ shared.panels.forEach((panel) => panel.destroy());
1487
+ shared.panels.clear();
1488
+ });
1489
+ return shared;
1490
+ }
1491
+ function createPanel(parent, ctx, shared) {
1492
+ const doc = parent.ownerDocument;
1493
+ const win = ctx.window;
1494
+ const still = () => ctx.tier() === "static";
1495
+ const make = (tag, className, attrs) => {
1496
+ const node = doc.createElement(tag);
1497
+ node.className = className;
1498
+ for (const [k, v] of Object.entries(attrs || {})) node.setAttribute(k, v);
1499
+ return node;
1500
+ };
1501
+ panelCount += 1;
1502
+ const titleId = "gem-quick-view-title-" + panelCount;
1503
+ const shell = make("div", "gem-popup-shell gem-quick-view__shell", {
1504
+ "data-gem-quick-view-panel": "",
1505
+ "data-gem-popup-desktop": "split-left",
1506
+ "data-gem-popup-mobile": "sheet",
1507
+ "data-gem-popup-media": "top",
1508
+ "data-gem-popup-align": "start"
1509
+ });
1510
+ const dialog = make("dialog", "gem-popup-shell__dialog gem-quick-view__dialog", { "aria-labelledby": titleId });
1511
+ const frame = make("div", "gem-popup-shell__frame gem-quick-view__frame");
1512
+ const surface = make("div", "gem-quick-view__surface", { "aria-hidden": "true" });
1513
+ const close = make("button", "gem-popup-shell__close gem-quick-view__close", { type: "button" });
1514
+ const media = make("div", "gem-popup-shell__media gem-quick-view__media");
1515
+ const img = make("img", "gem-quick-view__image", { alt: "" });
1516
+ const imgNext = make("img", "gem-quick-view__image gem-quick-view__image--next", { alt: "", "aria-hidden": "true" });
1517
+ const body = make("div", "gem-popup-shell__panel gem-quick-view__panel");
1518
+ const bar = make("div", "gem-quick-view__bar");
1519
+ const prev = make("button", "gem-quick-view__icon", { type: "button" });
1520
+ const next = make("button", "gem-quick-view__icon", { type: "button" });
1521
+ const counter = make("span", "gem-quick-view__count");
1522
+ const eyebrow = make("p", "gem-quick-view__eyebrow");
1523
+ const heading = make("h2", "gem-quick-view__title", { id: titleId });
1524
+ const price = make("p", "gem-quick-view__price");
1525
+ const compare = make("s", "gem-quick-view__compare");
1526
+ const swatches = make("div", "gem-quick-view__swatches", { role: "radiogroup" });
1527
+ const sizes = make("div", "gem-quick-view__sizes", { role: "radiogroup" });
1528
+ const add = make("button", "gem-button gem-button--primary gem-quick-view__add", { type: "button" });
1529
+ const status = make("p", "gem-quick-view__status", { role: "status" });
1530
+ const desc = make("p", "gem-quick-view__description");
1531
+ const details = make("a", "gem-quick-view__details");
1532
+ close.textContent = "\xD7";
1533
+ prev.textContent = "\u2039";
1534
+ next.textContent = "\u203A";
1535
+ media.append(img, imgNext);
1536
+ bar.append(prev, counter, next);
1537
+ body.append(bar, eyebrow, heading, price, compare, swatches, sizes, add, status, desc, details);
1538
+ frame.append(close, media, body);
1539
+ dialog.append(surface, frame);
1540
+ shell.append(dialog);
1541
+ parent.append(shell);
1542
+ const chosen = /* @__PURE__ */ new Map();
1543
+ let host = null;
1544
+ let list = [];
1545
+ let index = 0;
1546
+ let color = 0;
1547
+ let size = -1;
1548
+ let source = null;
1549
+ let returnTo = null;
1550
+ let priceAt = null;
1551
+ let labels = LABELS;
1552
+ let closing = false;
1553
+ let resetTimer = 0;
1554
+ const product = () => list[index];
1555
+ const animate = (el, frames, options) => typeof el.animate === "function" ? el.animate(frames, options) : null;
1556
+ const cancelAll = (el) => {
1557
+ if (el.getAnimations) el.getAnimations().forEach((a) => a.cancel());
1558
+ };
1559
+ const hideSource = (hide) => {
1560
+ if (!source) return;
1561
+ if (hide) source.setAttribute("data-gem-quick-view-source", "");
1562
+ else source.removeAttribute("data-gem-quick-view-source");
1563
+ };
1564
+ const moveSource = () => {
1565
+ const target = host.imageFor(index);
1566
+ if (target === source) return;
1567
+ hideSource(false);
1568
+ source = target;
1569
+ if (dialog.open) hideSource(true);
1570
+ };
1571
+ const setTint = (tint) => {
1572
+ if (host.setTint) host.setTint(tint);
1573
+ else if (tint) shell.style.setProperty("--gem-motion-tint", tint);
1574
+ else shell.style.removeProperty("--gem-motion-tint");
1575
+ };
1576
+ const showImage = (image, fade) => {
1577
+ img.alt = image.alt || text(product().title);
1578
+ if (!fade || still() || typeof imgNext.animate !== "function") return putImage(img, image);
1579
+ putImage(imgNext, image);
1580
+ const swap = () => {
1581
+ const settle = () => {
1582
+ putImage(img, image);
1583
+ imgNext.removeAttribute("src");
1584
+ imgNext.removeAttribute("srcset");
1585
+ };
1586
+ const run = imgNext.animate([{ opacity: 0, transform: "scale(.97)" }, { opacity: 1, transform: "none" }], { duration: 280, easing: EASE_OUT });
1587
+ if (run && run.finished) run.finished.then(settle, settle);
1588
+ else settle();
1589
+ };
1590
+ if (typeof imgNext.decode === "function") imgNext.decode().then(swap, swap);
1591
+ else swap();
1592
+ };
1593
+ const variant = () => {
1594
+ const p = product();
1595
+ const c = p.colors && p.colors[color];
1596
+ const z = p.sizes && p.sizes[size];
1597
+ if (!Array.isArray(p.variants)) return null;
1598
+ return p.variants.find((v) => (!c || (v.options || []).includes(c.name)) && (!z || (v.options || []).includes(z.name))) || null;
1599
+ };
1600
+ const variantId = () => {
1601
+ const p = product();
1602
+ const match = variant();
1603
+ if (match) return match.available === false ? null : match.id;
1604
+ const c = p.colors && p.colors[color];
1605
+ const z = p.sizes && p.sizes[size];
1606
+ if (z && z.variant_id != null) return z.variant_id;
1607
+ if (c && c.variant_ids && c.variant_ids.length) return c.variant_ids[0];
1608
+ return p.variant_id != null ? p.variant_id : null;
1609
+ };
1610
+ const priced = () => {
1611
+ const v = variant();
1612
+ return v && (v.price_text != null || v.price != null) ? v : product();
1613
+ };
1614
+ const showPrice = (fade) => {
1615
+ const words = priceText(priced(), priceAt);
1616
+ if (fade) swapText(price, words, still());
1617
+ else price.textContent = words;
1618
+ compare.textContent = priceText(priced(), priceAt, "compare");
1619
+ compare.hidden = !compare.textContent;
1620
+ };
1621
+ const updateAdd = () => {
1622
+ const p = product();
1623
+ const z = p.sizes && p.sizes[size];
1624
+ const soldOut = p.sold_out === true || Boolean(p.sizes && p.sizes.length) && size < 0;
1625
+ add.disabled = soldOut;
1626
+ add.textContent = soldOut ? labels.sold_out : [labels.add, z ? z.name : "", priceText(priced(), priceAt)].filter(Boolean).join(" \xB7 ");
1627
+ };
1628
+ const render = (fade) => {
1629
+ const p = product();
1630
+ const colors = Array.isArray(p.colors) ? p.colors : [];
1631
+ color = Math.min(chosen.get(p.id) || 0, Math.max(0, colors.length - 1));
1632
+ size = (p.sizes || []).findIndex((z) => z.available !== false);
1633
+ const c = colors[color];
1634
+ counter.textContent = pad2(index + 1) + " / " + pad2(list.length);
1635
+ bar.hidden = list.length < 2;
1636
+ const put = (el, value) => {
1637
+ if (fade) swapText(el, value, still());
1638
+ else el.textContent = value;
1639
+ };
1640
+ put(eyebrow, text(p.eyebrow));
1641
+ put(heading, text(p.title));
1642
+ showPrice(fade);
1643
+ eyebrow.hidden = !p.eyebrow;
1644
+ desc.textContent = text(p.description);
1645
+ desc.hidden = !p.description;
1646
+ details.textContent = labels.details;
1647
+ if (p.url) details.setAttribute("href", p.url);
1648
+ details.hidden = !p.url;
1649
+ showImage(c && c.image || p.image, fade);
1650
+ setTint(c && c.tint || p.tint);
1651
+ swatches.replaceChildren(
1652
+ ...colors.map((col, i) => {
1653
+ const b = make("button", "gem-quick-view__swatch", { type: "button", role: "radio", "aria-checked": String(i === color), "aria-label": col.name, title: col.name, "data-gem-quick-view-color": String(i) });
1654
+ if (col.hex) b.style.setProperty("--gem-quick-view-swatch", col.hex);
1655
+ return b;
1656
+ })
1657
+ );
1658
+ swatches.hidden = colors.length < 2;
1659
+ sizes.replaceChildren(
1660
+ ...(p.sizes || []).map((z, i) => {
1661
+ const b = make("button", "gem-quick-view__size", { type: "button", role: "radio", "aria-checked": String(i === size), "data-gem-quick-view-size": String(i) });
1662
+ b.textContent = z.name;
1663
+ if (z.available === false) {
1664
+ b.disabled = true;
1665
+ b.setAttribute("aria-label", z.name + ", " + labels.sold_out);
1666
+ }
1667
+ return b;
1668
+ })
1669
+ );
1670
+ sizes.hidden = !(p.sizes && p.sizes.length);
1671
+ status.textContent = "";
1672
+ updateAdd();
1673
+ moveSource();
1674
+ for (const k of [index - 1, index + 1]) if (list.length > 1) loadImage(doc, list[(k + list.length) % list.length].image);
1675
+ };
1676
+ const step = (delta) => {
1677
+ if (list.length < 2) return;
1678
+ index = (index + delta + list.length) % list.length;
1679
+ host.goTo(index);
1680
+ render(true);
1681
+ };
1682
+ const finish = (ok, before) => {
1683
+ shared.pending = null;
1684
+ add.disabled = false;
1685
+ add.textContent = ok ? labels.added : before;
1686
+ status.textContent = ok ? text(product().title) + ": " + labels.added_status : labels.failed;
1687
+ if (resetTimer) win.clearTimeout(resetTimer);
1688
+ resetTimer = ok ? win.setTimeout(updateAdd, 1600) : 0;
1689
+ };
1690
+ const addToCart = () => {
1691
+ const id = variantId();
1692
+ if (id == null || id === "") {
1693
+ status.textContent = labels.choose;
1694
+ return;
1695
+ }
1696
+ const before = add.textContent;
1697
+ add.disabled = true;
1698
+ add.textContent = labels.adding;
1699
+ add.dispatchEvent(new win.CustomEvent("gem:cart:add", { bubbles: true, composed: true, detail: { variantId: String(id), quantity: 1 } }));
1700
+ if (win.__gemCartAddBridge) shared.pending = (ok) => finish(ok, before);
1701
+ else finish(true, before);
1702
+ };
1703
+ const pick = (group, i) => {
1704
+ Array.from(group.children).forEach((el, k) => el.setAttribute("aria-checked", String(k === i)));
1705
+ if (group === sizes) size = i;
1706
+ else {
1707
+ const p = product();
1708
+ const c = p.colors && p.colors[i];
1709
+ if (!c) return;
1710
+ color = i;
1711
+ chosen.set(p.id, i);
1712
+ if (c.image) {
1713
+ showImage(c.image, true);
1714
+ if (host.setImage) host.setImage(index, c.image, c.side);
1715
+ }
1716
+ setTint(c.tint || p.tint);
1717
+ }
1718
+ showPrice(false);
1719
+ updateAdd();
1720
+ };
1721
+ const done = (reason) => {
1722
+ dialog.removeAttribute("data-gem-state");
1723
+ cancelAll(img);
1724
+ if (dialog.open) dialog.close();
1725
+ hideSource(false);
1726
+ host.setOpen(false);
1727
+ closing = false;
1728
+ const back = host.triggerFor(index) || returnTo;
1729
+ if (back && typeof back.focus === "function") back.focus();
1730
+ ctx.api.emit("gem:quick-view:close", { source: "gem-runtime", reason, productId: product().id });
1731
+ };
1732
+ const shut = (reason) => {
1733
+ if (!dialog.open || closing) return;
1734
+ closing = true;
1735
+ moveSource();
1736
+ const to = source && source.getBoundingClientRect ? source.getBoundingClientRect() : null;
1737
+ const flight = still() || !to ? null : flip(img, to, { back: true, duration: 440, easing: "cubic-bezier(.4,0,.2,1)" });
1738
+ if (!flight || !flight.finished) return done(reason);
1739
+ dialog.setAttribute("data-gem-state", "closing");
1740
+ for (const el of [body, surface, close]) animate(el, [{ opacity: 1 }, { opacity: 0 }], { duration: 220, fill: "forwards" });
1741
+ host.setOpen(false);
1742
+ const end = () => {
1743
+ [body, surface, close].forEach(cancelAll);
1744
+ done(reason);
1745
+ };
1746
+ flight.finished.then(end, end);
1747
+ };
1748
+ ctx.on(close, "click", () => shut("close-control"));
1749
+ ctx.on(prev, "click", () => step(-1));
1750
+ ctx.on(next, "click", () => step(1));
1751
+ ctx.on(add, "click", addToCart);
1752
+ for (const [group, attr] of [[swatches, "data-gem-quick-view-color"], [sizes, "data-gem-quick-view-size"]]) {
1753
+ ctx.on(group, "click", (event) => {
1754
+ const b = event.target.closest && event.target.closest(`[${attr}]`);
1755
+ if (b && !b.disabled) pick(group, Number(b.getAttribute(attr)));
1756
+ });
1757
+ }
1758
+ ctx.on(dialog, "cancel", (event) => {
1759
+ event.preventDefault();
1760
+ shut("escape");
1761
+ });
1762
+ ctx.on(dialog, "click", (event) => {
1763
+ if (event.target === dialog) shut("backdrop");
1764
+ });
1765
+ ctx.on(dialog, "keydown", (event) => {
1766
+ if (event.target.closest && event.target.closest(".gem-quick-view__swatches, .gem-quick-view__sizes")) return;
1767
+ if (event.key === "ArrowRight" || event.key === "ArrowLeft") step(event.key === "ArrowRight" ? 1 : -1);
1768
+ });
1769
+ return {
1770
+ shell,
1771
+ open(nextHost, at, trigger, nextLabels) {
1772
+ if (dialog.open) return;
1773
+ cancelAll(img);
1774
+ host = nextHost;
1775
+ list = host.products;
1776
+ index = at;
1777
+ labels = nextLabels;
1778
+ returnTo = trigger;
1779
+ priceAt = trigger;
1780
+ source = null;
1781
+ close.setAttribute("aria-label", labels.close);
1782
+ prev.setAttribute("aria-label", labels.previous);
1783
+ next.setAttribute("aria-label", labels.next);
1784
+ swatches.setAttribute("aria-label", labels.colour);
1785
+ sizes.setAttribute("aria-label", labels.size);
1786
+ render(false);
1787
+ source = host.imageFor(index);
1788
+ const from = source && source.getBoundingClientRect ? source.getBoundingClientRect() : null;
1789
+ dialog.showModal();
1790
+ dialog.setAttribute("data-gem-state", "open");
1791
+ host.setOpen(true);
1792
+ hideSource(true);
1793
+ ctx.api.emit("gem:quick-view:open", { source: "gem-runtime", productId: product().id, productUrl: product().url || null, index, count: list.length, host: host.key });
1794
+ if (still()) return;
1795
+ flip(img, from, { duration: 560, easing: EASE_OUT });
1796
+ animate(surface, [{ opacity: 0, transform: "scale(.97)" }, { opacity: 1, transform: "none" }], { duration: 360, easing: EASE_OUT });
1797
+ Array.from(body.children).forEach(
1798
+ (el, i) => animate(el, [{ opacity: 0, transform: "translateY(12px)" }, { opacity: 1, transform: "none" }], { duration: 380, delay: 120 + i * 40, easing: EASE_OUT, fill: "backwards" })
1799
+ );
1800
+ },
1801
+ /** Unmount: close at once (no flight) and leave the root as the server drew it. */
1802
+ destroy() {
1803
+ if (dialog.open) done("unmount");
1804
+ shell.remove();
1805
+ }
1806
+ };
1807
+ }
1808
+ function mountQuickView(trigger, ctx) {
1809
+ const shared = sessionOf(ctx);
1810
+ ctx.on(trigger, "click", (event) => {
1811
+ if (event.defaultPrevented) return;
1812
+ const hostEl = trigger.closest("[data-gem-quick-view-host]");
1813
+ const host = hostEl && hostEl.gemQuickViewHost;
1814
+ if (host && !host.opens(trigger)) return;
1815
+ const card = host ? null : cardHost(trigger);
1816
+ const index = host ? host.indexOf(trigger) : card.index;
1817
+ if (index < 0 || !(host || card.host).products[index]) return;
1818
+ event.preventDefault();
1819
+ const parent = host && hostEl || trigger.closest("[data-gem-quick-view-group]") || trigger.closest(".gem-section, [data-gem-section-id]") || trigger.parentElement;
1820
+ let panel = shared.panels.get(parent);
1821
+ if (!panel || !panel.shell.isConnected) shared.panels.set(parent, panel = createPanel(parent, ctx, shared));
1822
+ const own = ctx.reread().labels;
1823
+ panel.open(host || card.host, index, trigger, { ...LABELS, ...own && typeof own === "object" ? own : {} });
1824
+ });
1825
+ }
1826
+ var quickViewEffect = defineEffect({
1827
+ key: "quick-view",
1828
+ hook: "[data-gem-quick-view-trigger]",
1829
+ load: "interaction",
1830
+ renderer: "dom",
1831
+ tier: "static",
1832
+ fallback: "markup",
1833
+ budget: { gzip: 6144 },
1834
+ settings: {
1835
+ labels: {
1836
+ type: "json",
1837
+ default: {},
1838
+ inherit: true,
1839
+ label: "The panel's words, JSON on the trigger or an ancestor: add, adding, added, added_status, failed, choose, sold_out, details, previous, next, close, colour, size"
1840
+ }
1841
+ },
1842
+ mount: mountQuickView
1843
+ });
1844
+
1845
+ // ../../packages/gem-framework/src/motion-engine.ts
1846
+ var GEM_ORBIT_BASE_CSS_LINES = [
1847
+ ".gem-orbit { display: block; position: relative; background-color: var(--gem-motion-tint, transparent); }",
1848
+ ".gem-orbit:focus-visible { outline: var(--gem-focus-ring); outline-offset: calc(-1 * var(--gem-focus-offset)); }",
1849
+ ".gem-orbit__stage { position: relative; height: var(--gem-orbit-stage-height, clamp(340px, 58vh, 620px)); overflow: hidden; container-type: size; }",
1850
+ ".gem-orbit__ring { list-style: none; margin: 0; padding: 0; position: absolute; left: 50%; top: 66%; width: 0; height: 0; }",
1851
+ ".gem-orbit__item {",
1852
+ " --gem-orbit-k: calc(mod(var(--gem-orbit-i, 0) - var(--gem-orbit-start, 0) + var(--gem-orbit-n, 1) / 2, var(--gem-orbit-n, 1)) - var(--gem-orbit-n, 1) / 2);",
1853
+ " --gem-orbit-a: calc(var(--gem-orbit-k) * var(--gem-orbit-spread, calc(360 / var(--gem-orbit-n, 1))) * 1deg);",
1854
+ " --gem-orbit-d: calc((1 - cos(var(--gem-orbit-a))) / 2);",
1855
+ " position: absolute; left: calc(var(--gem-orbit-size, 0.62) * -50cqh); top: calc(var(--gem-orbit-size, 0.62) * -50cqh);",
1856
+ " width: calc(var(--gem-orbit-size, 0.62) * 100cqh); height: calc(var(--gem-orbit-size, 0.62) * 100cqh);",
1857
+ " transform: translate3d(calc(var(--gem-orbit-rx, 0.36) * 100cqw * sin(var(--gem-orbit-a))), calc(var(--gem-orbit-tilt, 0.3) * -100cqh * var(--gem-orbit-d)), 0)",
1858
+ " scale(calc(1 + (var(--gem-orbit-back, 0.42) - 1) * var(--gem-orbit-d))) rotate(calc(sin(var(--gem-orbit-a)) * var(--gem-orbit-lean, 5) * 1deg));",
1859
+ " opacity: calc(1 + (var(--gem-orbit-fade, 0.35) - 1) * var(--gem-orbit-d));",
1860
+ " z-index: calc((1 - var(--gem-orbit-d)) * 100);",
1861
+ " filter: blur(calc(var(--gem-orbit-blur, 2.5) * var(--gem-orbit-d) * 1px));",
1862
+ "}",
1863
+ ".gem-orbit__item::after {",
1864
+ " content: ''; position: absolute; left: 18%; right: 18%; bottom: -2%; height: 7%; z-index: -1; border-radius: var(--gem-radius-pill);",
1865
+ " background: radial-gradient(closest-side, color-mix(in srgb, var(--gem-color-scrim, #000) 18%, transparent), transparent);",
1866
+ "}",
1867
+ // A hit is a <button> or an <a href> to the product (still a link on a page without the engine).
1868
+ ".gem-orbit__hit { display: block; width: 100%; height: 100%; padding: 0; border: 0; color: inherit; background: transparent; text-decoration: none; cursor: pointer; }",
1869
+ ".gem-orbit__hit:focus-visible { outline: var(--gem-focus-ring); outline-offset: var(--gem-focus-offset); border-radius: var(--gem-radius-md); }",
1870
+ ".gem-orbit__img { display: block; width: 100%; height: 100%; object-fit: contain; pointer-events: none; }",
1871
+ ".gem-orbit__caption { display: grid; justify-items: center; gap: var(--gem-space-2xs); margin: calc(-1 * var(--gem-space-xs)) 0 0; text-align: center; }",
1872
+ ".gem-orbit__name { margin: 0; font-family: var(--gem-font-heading); font-size: var(--gem-h4); font-weight: 600; line-height: 1.2; }",
1873
+ ".gem-orbit__price { margin: 0; color: var(--gem-color-muted); font-variant-numeric: tabular-nums; }",
1874
+ ".gem-orbit__dock { display: flex; align-items: center; justify-content: center; gap: var(--gem-space-sm); padding: var(--gem-space-md) 0 var(--gem-space-xs); font-size: var(--gem-text-sm); font-variant-numeric: tabular-nums; }",
1875
+ ".gem-orbit__arrow {",
1876
+ " display: inline-grid; place-items: center; width: var(--gem-tap-min); height: var(--gem-tap-min); padding: 0; cursor: pointer;",
1877
+ " border: var(--gem-stroke, 1px) solid currentColor; border-radius: var(--gem-radius-pill); color: inherit; background: transparent; font: inherit; line-height: 1;",
1878
+ "}",
1879
+ ".gem-orbit__arrow:hover { color: var(--gem-color-background); background: var(--gem-color-text); border-color: var(--gem-color-text); }",
1880
+ ".gem-orbit__arrow:focus-visible { outline: var(--gem-focus-ring); outline-offset: var(--gem-focus-offset); }",
1881
+ ".gem-orbit__count { min-width: 7ch; text-align: center; font-variant-numeric: tabular-nums; }",
1882
+ ".gem-orbit__live { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }",
1883
+ ".gem-orbit__track { position: relative; width: min(180px, 30vw); height: var(--gem-stroke-thick, 2px); background: color-mix(in srgb, currentColor 18%, transparent); }",
1884
+ ".gem-orbit__track::before { content: ''; position: absolute; inset: calc(-1 * var(--gem-space-sm)) 0; }",
1885
+ ".gem-orbit__bar { position: absolute; inset: 0; background: currentColor; transform-origin: left; transform: scaleX(calc((var(--gem-orbit-start, 0) + 1) / var(--gem-orbit-n, 1))); }",
1886
+ // The light and static tiers draw no blur (a phone, reduced motion): the first paint already matches what they draw.
1887
+ "@media (pointer: coarse), (prefers-reduced-motion: reduce) { .gem-orbit__item { filter: none; } }"
1888
+ ];
1889
+ var GEM_RAIL_BASE_CSS_LINES = [
1890
+ ".gem-rail { display: block; position: relative; container-type: inline-size; background-color: var(--gem-motion-tint, transparent); }",
1891
+ ".gem-rail__stage { position: relative; height: var(--gem-rail-stage-height, clamp(380px, 62vh, 640px)); overflow: hidden; container-type: size; }",
1892
+ // The rod is drawn in a 0..100 viewBox with preserveAspectRatio="none": it fits any stage without being redrawn.
1893
+ ".gem-rail__rod { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; pointer-events: none; }",
1894
+ ".gem-rail__rod path { fill: none; stroke: var(--gem-rail-rod, currentColor); stroke-width: 5; stroke-linecap: round; vector-effect: non-scaling-stroke; opacity: 0.85; }",
1895
+ ".gem-rail__track { list-style: none; margin: 0; padding: 0; position: absolute; inset: 0; }",
1896
+ // At rest slot i of n is centred at 4% + 92% · (i + ½) / n of the width and hangs from the rod's curve there.
1897
+ ".gem-rail__item {",
1898
+ " --gem-rail-t: calc(0.04 + 0.92 * (var(--gem-rail-i, 0) + 0.5) / var(--gem-rail-n, 1));",
1899
+ " position: absolute; left: 0; top: 0; width: 0; height: 0; z-index: 10;",
1900
+ " transform: translate3d(calc(var(--gem-rail-t) * 100cqw), calc((0.14 + 4 * var(--gem-rail-sag, 0.07) * var(--gem-rail-t) * (1 - var(--gem-rail-t))) * 100cqh), 0);",
1901
+ "}",
1902
+ ".gem-rail__hit {",
1903
+ " position: absolute; left: 0; top: -6px; width: calc(var(--gem-rail-height, 0.7) * 78cqh); height: calc(var(--gem-rail-height, 0.7) * 100cqh + 34px);",
1904
+ " padding: 0; border: 0; color: inherit; background: transparent; text-decoration: none; cursor: pointer; transform: translateX(-50%);",
1905
+ "}",
1906
+ ".gem-rail__hit:focus-visible { outline: var(--gem-focus-ring); outline-offset: var(--gem-focus-offset); border-radius: var(--gem-radius-md); }",
1907
+ ".gem-rail__body { position: absolute; inset: 0; display: block; transform-origin: 50% 0; }",
1908
+ ".gem-rail__hook {",
1909
+ " position: absolute; left: calc(50% - 8px); top: 0; width: 16px; height: 22px; opacity: 0.8;",
1910
+ " border: var(--gem-stroke-thick, 2px) solid var(--gem-rail-rod, currentColor); border-bottom: 0; border-radius: var(--gem-radius-md) var(--gem-radius-md) 0 0;",
1911
+ "}",
1912
+ ".gem-rail__hook::after { content: ''; position: absolute; left: calc(50% - 23px); top: 18px; width: 46px; height: var(--gem-stroke-thick, 2px); background: var(--gem-rail-rod, currentColor); border-radius: var(--gem-radius-sm); }",
1913
+ ".gem-rail__tag {",
1914
+ " position: absolute; left: calc(50% + 14px); top: 18px; z-index: 2; padding: var(--gem-space-2xs) var(--gem-space-xs); transform: rotate(4deg);",
1915
+ " font-family: var(--gem-font-mono); font-size: var(--gem-text-xs); font-weight: 600; line-height: 1; font-variant-numeric: tabular-nums;",
1916
+ " color: var(--gem-color-text); background: var(--gem-color-background); border-radius: var(--gem-radius-sm); box-shadow: var(--gem-shadow-sm);",
1917
+ "}",
1918
+ ".gem-rail .gem-turn { position: absolute; inset: 26px 0 0; width: auto; height: auto; }",
1919
+ ".gem-rail__caption { margin: var(--gem-space-xs) 0 0; min-height: 1.5em; text-align: center; font-variant-numeric: tabular-nums; }",
1920
+ // Under 700 px (the effect's breakpoint, on the same width): a swipe row with snap points; the centred piece turns.
1921
+ "@container (max-width: 699px) {",
1922
+ " .gem-rail__stage { height: auto; overflow: visible; container-type: normal; }",
1923
+ " .gem-rail__rod { display: none; }",
1924
+ " .gem-rail__track { position: static; display: flex; gap: var(--gem-space-2xs); overflow-x: auto; scroll-snap-type: x mandatory; padding: 0 20cqw var(--gem-space-xs); scrollbar-width: none; }",
1925
+ " .gem-rail__track::-webkit-scrollbar { display: none; }",
1926
+ " .gem-rail__item { position: relative; flex: 0 0 60cqw; width: auto; height: calc(72cqw + 34px); transform: none; scroll-snap-align: center; border-top: var(--gem-stroke-thick, 4px) solid var(--gem-rail-rod, currentColor); }",
1927
+ " .gem-rail__hit { left: 50%; width: 100%; height: 100%; }",
1928
+ // The row rests with its first piece centred (the track's padding) and facing the shopper; the plugin settles there.
1929
+ " .gem-rail__item:first-child .gem-turn__side { opacity: 0; transform: perspective(900px) rotateY(-82deg); }",
1930
+ " .gem-rail__item:first-child .gem-turn:has(> .gem-turn__side) > .gem-turn__front { opacity: 1; transform: scale(1); }",
1931
+ " .gem-rail__item:first-child .gem-turn:not(:has(> .gem-turn__side)) > .gem-turn__front { transform: perspective(900px) rotateY(0deg) scale(1); }",
1932
+ " .gem-rail__item:first-child .gem-turn__sheet { transform: translate(calc(-100% / var(--gem-turn-cols, 1) * mod(var(--gem-turn-frames, 1) - 1, var(--gem-turn-cols, 1))), calc(-100% / var(--gem-turn-rows, 1) * round(down, (var(--gem-turn-frames, 1) - 1) / var(--gem-turn-cols, 1)))); }",
1933
+ "}",
1934
+ // The turn rests on its side view: pair (side + front photo), flip (front only), sprite (two copies of a frame sheet).
1935
+ ".gem-turn { position: relative; display: block; width: 100%; height: 100%; }",
1936
+ ".gem-turn[data-gem-turn-frames] { overflow: hidden; }",
1937
+ ".gem-turn__side, .gem-turn__front { position: absolute; inset: 0; display: block; width: 100%; height: 100%; object-fit: contain; object-position: 50% 0; pointer-events: none; }",
1938
+ ".gem-turn__side { transform: perspective(900px) rotateY(0deg); backface-visibility: hidden; }",
1939
+ ".gem-turn:has(> .gem-turn__side) > .gem-turn__front { opacity: 0; transform: scale(0.9); }",
1940
+ ".gem-turn:not(:has(> .gem-turn__side)) > .gem-turn__front { transform: perspective(900px) rotateY(-74deg) scale(0.86); }",
1941
+ ".gem-turn__sheet { position: absolute; left: 0; top: 0; display: block; max-width: none; pointer-events: none; width: calc(var(--gem-turn-cols, 1) * 100%); height: calc(var(--gem-turn-rows, 1) * 100%); }",
1942
+ ".gem-turn__sheet + .gem-turn__sheet { opacity: 0; }"
1943
+ ];
1944
+ var GEM_ORBIT_BASE_CSS = GEM_ORBIT_BASE_CSS_LINES.join("\n") + "\n";
1945
+ var GEM_RAIL_BASE_CSS = GEM_RAIL_BASE_CSS_LINES.join("\n") + "\n";
1946
+ var GEM_ORBIT_RUNTIME_CSS = [
1947
+ ".gem-orbit { --gem-orbit-css: 1; outline: none; user-select: none; -webkit-user-select: none; transition: background-color var(--gem-motion-slow, 0.6s) var(--gem-ease-soft, ease); }",
1948
+ ".gem-orbit[data-gem-state] .gem-orbit__stage { touch-action: pan-y; cursor: grab; }",
1949
+ ".gem-orbit[data-gem-state='dragging'] .gem-orbit__stage { cursor: grabbing; }",
1950
+ ".gem-orbit[data-gem-state] .gem-orbit__item { will-change: transform; }",
1951
+ ".gem-orbit[data-gem-state] .gem-orbit__hit { transition: opacity var(--gem-motion-base, 0.35s) var(--gem-ease-soft, ease), transform var(--gem-motion-base, 0.35s) var(--gem-ease-soft, ease); }",
1952
+ ".gem-orbit[data-gem-state] .gem-orbit__bar { transition: transform var(--gem-motion-slow, 0.4s) var(--gem-ease-soft, ease); }",
1953
+ ".gem-orbit[data-gem-state] .gem-orbit__arrow { transition: color var(--gem-motion-fast, 0.2s) ease, background-color var(--gem-motion-fast, 0.2s) ease; }",
1954
+ ".gem-orbit[data-gem-state='open'] .gem-orbit__item:not([aria-current='true']) .gem-orbit__hit { opacity: 0; transform: scale(0.92); }",
1955
+ "@media (prefers-reduced-motion: reduce) { .gem-orbit, .gem-orbit[data-gem-state] .gem-orbit__hit, .gem-orbit[data-gem-state] .gem-orbit__bar, .gem-orbit[data-gem-state] .gem-orbit__arrow { transition: none; } }"
1956
+ ].join("\n") + "\n";
1957
+ var GEM_RAIL_RUNTIME_CSS = [
1958
+ ".gem-rail { --gem-rail-css: 1; user-select: none; -webkit-user-select: none; transition: background-color var(--gem-motion-slow, 0.6s) var(--gem-ease-soft, ease); }",
1959
+ ".gem-rail[data-gem-state] .gem-rail__item { will-change: transform; }",
1960
+ // Unturned neighbours' hit boxes overlap on the rail: once bound, a click on the stage goes to the piece whose slot it is in
1961
+ // (motion/rail.js), so the hits take no pointer events; keyboard focus and Enter stay on them. The phone row keeps its own.
1962
+ ".gem-rail[data-gem-state] .gem-rail__stage { cursor: pointer; }",
1963
+ ".gem-rail[data-gem-state] .gem-rail__hit { pointer-events: none; transition: opacity var(--gem-motion-base, 0.35s) var(--gem-ease-soft, ease); }",
1964
+ "@container (max-width: 699px) { .gem-rail[data-gem-state] .gem-rail__hit { pointer-events: auto; } }",
1965
+ ".gem-rail[data-gem-state='open'] .gem-rail__hit { opacity: 0.15; }",
1966
+ "@media (prefers-reduced-motion: reduce) { .gem-rail, .gem-rail[data-gem-state] .gem-rail__hit { transition: none; } }"
1967
+ ].join("\n") + "\n";
1968
+ var GEM_QUICK_VIEW_CSS = [
1969
+ "[data-gem-quick-view-trigger] { --gem-quick-view-css: 1; }",
1970
+ ".gem-quick-view__shell { display: contents; }",
1971
+ ".gem-quick-view__shell .gem-quick-view__dialog { overflow: visible; isolation: isolate; color: var(--gem-color-text); background: transparent; box-shadow: none; user-select: text; -webkit-user-select: text; }",
1972
+ ".gem-quick-view__surface {",
1973
+ " position: absolute; inset: 0; z-index: -1; border-radius: inherit; box-shadow: var(--gem-shadow-md);",
1974
+ " background: color-mix(in srgb, var(--gem-motion-tint, var(--gem-color-background)) 45%, var(--gem-color-background));",
1975
+ " transition: background-color var(--gem-motion-slow, 0.5s) var(--gem-ease-soft, ease);",
1976
+ "}",
1977
+ ".gem-quick-view__shell .gem-quick-view__media { overflow: visible; background: transparent; min-height: 0; }",
1978
+ ".gem-quick-view__image { position: absolute; inset: 6% 4%; width: 92%; height: 88%; object-fit: contain; transform-origin: 50% 50%; will-change: transform; }",
1979
+ ".gem-quick-view__image--next { opacity: 0; pointer-events: none; }",
1980
+ ".gem-quick-view__image--next:not([src]) { display: none; }",
1981
+ ".gem-quick-view__shell .gem-quick-view__panel { justify-content: flex-start; gap: var(--gem-space-sm); }",
1982
+ ".gem-quick-view__shell [hidden] { display: none; }",
1983
+ ".gem-quick-view__bar { display: flex; align-items: center; gap: var(--gem-space-xs); font-family: var(--gem-font-mono); font-size: var(--gem-text-xs); font-variant-numeric: tabular-nums; }",
1984
+ ".gem-quick-view__icon {",
1985
+ " display: inline-grid; place-items: center; width: var(--gem-tap-min); height: var(--gem-tap-min); padding: 0; cursor: pointer;",
1986
+ " border: var(--gem-stroke, 1px) solid color-mix(in srgb, currentColor 25%, transparent); border-radius: var(--gem-radius-pill);",
1987
+ " color: inherit; background: transparent; font: inherit; font-size: var(--gem-h5); line-height: 1;",
1988
+ "}",
1989
+ ".gem-quick-view__icon:hover { border-color: currentColor; }",
1990
+ ".gem-quick-view__icon:focus-visible, .gem-quick-view__swatch:focus-visible, .gem-quick-view__size:focus-visible, .gem-quick-view__details:focus-visible { outline: var(--gem-focus-ring); outline-offset: var(--gem-focus-offset); }",
1991
+ ".gem-quick-view__eyebrow { margin: var(--gem-space-md) 0 0; color: var(--gem-color-muted); font-family: var(--gem-font-mono); font-size: var(--gem-text-xs); letter-spacing: 0.12em; line-height: 1.3; text-transform: uppercase; }",
1992
+ ".gem-quick-view__title { margin: 0; font-family: var(--gem-font-heading); font-size: var(--gem-h2); line-height: 1.05; }",
1993
+ ".gem-quick-view__price { margin: 0; font-size: var(--gem-h5); font-variant-numeric: tabular-nums; }",
1994
+ ".gem-quick-view__compare { margin: calc(-1 * var(--gem-space-xs)) 0 0; color: var(--gem-color-muted); font-size: var(--gem-text-sm); }",
1995
+ ".gem-quick-view__swatches, .gem-quick-view__sizes { display: flex; flex-wrap: wrap; gap: var(--gem-space-xs); }",
1996
+ ".gem-quick-view__swatch { position: relative; width: var(--gem-tap-min); height: var(--gem-tap-min); padding: 0; border: 0; color: inherit; background: transparent; cursor: pointer; }",
1997
+ ".gem-quick-view__swatch::before {",
1998
+ " content: ''; position: absolute; inset: 8px; border-radius: var(--gem-radius-pill); background: var(--gem-quick-view-swatch, var(--gem-color-surface));",
1999
+ " box-shadow: inset 0 0 0 var(--gem-stroke, 1px) color-mix(in srgb, var(--gem-color-text) 15%, transparent);",
2000
+ "}",
2001
+ ".gem-quick-view__swatch[aria-checked='true']::after { content: ''; position: absolute; inset: 3px; border: var(--gem-stroke-thick, 2px) solid currentColor; border-radius: var(--gem-radius-pill); }",
2002
+ ".gem-quick-view__size {",
2003
+ " display: inline-grid; place-items: center; min-width: var(--gem-tap-min); height: var(--gem-tap-min); padding: 0 var(--gem-space-sm); cursor: pointer;",
2004
+ " border: var(--gem-stroke, 1px) solid color-mix(in srgb, currentColor 22%, transparent); border-radius: var(--gem-radius-pill);",
2005
+ " color: inherit; background: transparent; font: inherit; font-size: var(--gem-text-sm);",
2006
+ "}",
2007
+ ".gem-quick-view__size[aria-checked='true'] { color: var(--gem-color-background); background: var(--gem-color-text); border-color: var(--gem-color-text); }",
2008
+ ".gem-quick-view__size:disabled { opacity: 0.4; text-decoration: line-through; cursor: not-allowed; }",
2009
+ ".gem-quick-view__add { width: 100%; margin: var(--gem-space-xs) 0 0; font-variant-numeric: tabular-nums; }",
2010
+ ".gem-quick-view__status { margin: 0; min-height: 1.2em; color: var(--gem-color-muted); font-size: var(--gem-text-sm); }",
2011
+ ".gem-quick-view__description { margin: 0; max-width: 46ch; font-size: var(--gem-text-sm); line-height: 1.55; }",
2012
+ ".gem-quick-view__details { color: inherit; font-size: var(--gem-text-sm); text-underline-offset: 3px; }",
2013
+ // The photo the panel flew out of stays hidden in the page while the panel shows it.
2014
+ "[data-gem-quick-view-source] { visibility: hidden; }",
2015
+ "@keyframes gem-quick-view-fade { from { opacity: 0; } to { opacity: 1; } }",
2016
+ ".gem-quick-view__dialog[data-gem-state='open']::backdrop { animation: gem-quick-view-fade var(--gem-motion-base, 0.35s) ease both; }",
2017
+ ".gem-quick-view__dialog[data-gem-state='closing']::backdrop { animation: gem-quick-view-fade var(--gem-motion-base, 0.35s) ease reverse both; }",
2018
+ "@media (max-width: 749px) { .gem-quick-view__shell .gem-quick-view__media { height: 34dvh; } }",
2019
+ "@media (prefers-reduced-motion: reduce) { .gem-quick-view__dialog::backdrop { animation: none; } .gem-quick-view__surface { transition: none; } }"
2020
+ ].join("\n") + "\n";
2021
+ var GEM_MOTION_CLASSES = [
2022
+ ...["", "__stage", "__ring", "__item", "__hit", "__img", "__caption", "__name", "__price", "__dock", "__arrow", "__count", "__track", "__bar", "__live"].map((part) => `gem-orbit${part}`),
2023
+ ...["", "__stage", "__rod", "__track", "__item", "__hit", "__body", "__hook", "__tag", "__caption"].map((part) => `gem-rail${part}`),
2024
+ ...["", "__side", "__front", "__sheet"].map((part) => `gem-turn${part}`),
2025
+ ...[
2026
+ "shell",
2027
+ "dialog",
2028
+ "frame",
2029
+ "surface",
2030
+ "close",
2031
+ "media",
2032
+ "image",
2033
+ "image--next",
2034
+ "panel",
2035
+ "bar",
2036
+ "icon",
2037
+ "count",
2038
+ "eyebrow",
2039
+ "title",
2040
+ "price",
2041
+ "compare",
2042
+ "swatches",
2043
+ "swatch",
2044
+ "sizes",
2045
+ "size",
2046
+ "add",
2047
+ "status",
2048
+ "description",
2049
+ "details"
2050
+ ].map((part) => `gem-quick-view__${part}`)
2051
+ ];
2052
+ var gemMotionEffectList = [orbitEffect, railEffect, quickViewEffect];
2053
+ var compactCss = (css) => css.replace(/\s+/g, " ").replace(/\s*([{};])\s*/g, "$1").replace(/:\s+/g, ":").replace(/,\s+/g, ",").replace(/;}/g, "}").trim();
2054
+ var effectPluginLines = (effect, name, css, before = [], detail = "count") => [
2055
+ ` registerPlugin('${effect.key}', ({ api }) => {`,
2056
+ ...before,
2057
+ " const on = (el, type, fn, opt) => { if (canListen(el)) el.addEventListener(type, fn, opt); };",
2058
+ ` const count = mountEffects(${name}, { window, document, on, mark, api, css: ${JSON.stringify(compactCss(css))} }, onUnmount);`,
2059
+ ` api.emit('gem:${effect.key}:ready', { source: 'gem-runtime', ${detail} });`,
2060
+ " });"
2061
+ ];
2062
+ var effectImports = (file, name) => ['import { mountEffects } from "../motion/core.js";', `import { ${name} } from "../motion/${file}.js";`];
2063
+ function gemMotionChunks(legacyQuickView) {
2064
+ return {
2065
+ orbit: { hook: orbitEffect.hook, when: orbitEffect.load, imports: effectImports("orbit", "orbitEffect"), body: effectPluginLines(orbitEffect, "orbitEffect", GEM_ORBIT_RUNTIME_CSS) },
2066
+ rail: { hook: railEffect.hook, when: railEffect.load, imports: effectImports("rail", "railEffect"), body: effectPluginLines(railEffect, "railEffect", GEM_RAIL_RUNTIME_CSS) },
2067
+ "quick-view": {
2068
+ hook: `${quickViewEffect.hook}, ${legacyQuickView.hook}`,
2069
+ when: quickViewEffect.load,
2070
+ imports: effectImports("quick-view", "quickViewEffect"),
2071
+ body: effectPluginLines(quickViewEffect, "quickViewEffect", GEM_QUICK_VIEW_CSS, legacyQuickView.lines, legacyQuickView.readyDetail)
2072
+ }
2073
+ };
2074
+ }
2075
+
2076
+ // ../../packages/gem-framework/src/pod-engine.ts
2077
+ var P = ".gem-el-product-personalization";
2078
+ var V = ".gem-el-product-print-preview";
2079
+ var D = ".gem-el-product-delivery-estimate";
2080
+ var S = ".gem-el-product-design-switcher";
2081
+ var GEM_POD_BUYBOX_CSS_LINES = [
2082
+ // product-personalization: one field per element. The wrapper is the hook; a field whose condition fails is [hidden].
2083
+ `${P} { display: grid; gap: var(--gem-space-2xs); min-width: 0; }`,
2084
+ `${P}[hidden] { display: none; }`,
2085
+ `${P}__label { display: grid; gap: var(--gem-space-2xs); }`,
2086
+ `${P}__title { font-size: var(--gem-text-sm); font-weight: 600; }`,
2087
+ `${P}__required { color: var(--gem-color-danger); }`,
2088
+ `${P}__input { box-sizing: border-box; width: 100%; min-height: var(--gem-tap-min); padding: var(--gem-space-xs) var(--gem-space-sm); font: inherit; color: var(--gem-color-text); background: var(--gem-color-background); border: var(--gem-stroke, 1px) solid var(--gem-color-border); border-radius: var(--gem-radius-md); }`,
2089
+ `textarea${P}__input { min-height: calc(var(--gem-tap-min) * 2); resize: vertical; }`,
2090
+ `${P}__meta { display: flex; gap: var(--gem-space-xs); align-items: baseline; }`,
2091
+ `${P}__help { margin: 0; color: var(--gem-color-muted); font-size: var(--gem-text-xs); }`,
2092
+ `${P}__count { margin-left: auto; color: var(--gem-color-muted); font-size: var(--gem-text-xs); font-variant-numeric: tabular-nums; }`,
2093
+ // swatch and image-choice: radios inside labels; the radio stays focusable, its picture shows the state.
2094
+ `${P}__options { display: flex; flex-wrap: wrap; gap: var(--gem-space-xs); margin: 0; padding: 0; border: 0; min-width: 0; }`,
2095
+ `${P}__option { position: relative; display: inline-grid; justify-items: center; gap: var(--gem-space-2xs); font-size: var(--gem-text-xs); cursor: pointer; }`,
2096
+ `${P}__option input { position: absolute; inset: 0; margin: 0; opacity: 0; cursor: pointer; }`,
2097
+ `${P}__swatch { display: block; width: var(--gem-tap-min); height: var(--gem-tap-min); border-radius: var(--gem-radius-pill); background: var(--gem-personalize-swatch, var(--gem-color-surface)); border: var(--gem-stroke, 1px) solid var(--gem-color-border); }`,
2098
+ `${P}__choice-image { display: block; width: calc(var(--gem-tap-min) * 1.75); aspect-ratio: 1; object-fit: cover; border-radius: var(--gem-radius-md); border: var(--gem-stroke, 1px) solid var(--gem-color-border); }`,
2099
+ `${P}__option input:checked ~ ${P}__swatch, ${P}__option input:checked ~ ${P}__choice-image { box-shadow: 0 0 0 var(--gem-stroke-thick, 2px) var(--gem-color-background), 0 0 0 calc(var(--gem-stroke-thick, 2px) * 2) var(--gem-color-text); }`,
2100
+ `${P}__option input:focus-visible ~ ${P}__swatch, ${P}__option input:focus-visible ~ ${P}__choice-image { outline: var(--gem-focus-ring); outline-offset: calc(var(--gem-focus-offset) + var(--gem-stroke-thick, 2px) * 2); }`,
2101
+ `${P}__checkbox { display: inline-flex; align-items: center; gap: var(--gem-space-xs); min-height: var(--gem-tap-min); cursor: pointer; }`,
2102
+ // upload: a dashed drop box around a visually hidden (still focusable) file input, a thumbnail once a photo is picked.
2103
+ `${P}__upload { position: relative; display: grid; justify-items: center; gap: var(--gem-space-2xs); padding: var(--gem-space-md); text-align: center; background: var(--gem-color-surface); border: var(--gem-stroke, 1px) dashed var(--gem-color-border); border-radius: var(--gem-radius-md); cursor: pointer; }`,
2104
+ `${P}__upload:focus-within { outline: var(--gem-focus-ring); outline-offset: var(--gem-focus-offset); }`,
2105
+ `${P}__file { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: pointer; }`,
2106
+ `${P}__thumb { display: block; width: calc(var(--gem-tap-min) * 2); aspect-ratio: 1; object-fit: cover; border-radius: var(--gem-radius-sm); }`,
2107
+ `${P}__thumb[hidden], ${P}__thumb:not([src]) { display: none; }`,
2108
+ `${P}__privacy { margin: 0; color: var(--gem-color-muted); font-size: var(--gem-text-xs); }`,
2109
+ `${P}__error { margin: 0; color: var(--gem-color-danger); font-size: var(--gem-text-xs); }`,
2110
+ `${P}__error:empty, ${P}__error[hidden] { display: none; }`,
2111
+ // product-print-preview: the product's current image, and the print area on it (percent of the image, from the element's
2112
+ // area_* props or the gemboss.print_area metafield). Empty until the shopper types or uploads.
2113
+ `${V} { position: relative; display: block; overflow: hidden; border-radius: var(--gem-media-radius, var(--gem-radius-md)); }`,
2114
+ `${V}__media { display: block; width: 100%; height: auto; }`,
2115
+ `${V}__area { position: absolute; left: calc(var(--gem-print-preview-x, 20) * 1%); top: calc(var(--gem-print-preview-y, 35) * 1%); width: calc(var(--gem-print-preview-w, 60) * 1%); height: calc(var(--gem-print-preview-h, 20) * 1%); display: grid; place-items: center; overflow: hidden; pointer-events: none; container-type: size; }`,
2116
+ `${V}[data-gem-print-preview-shape="circle"] ${V}__area { border-radius: 50%; }`,
2117
+ `${V}[data-gem-print-preview-font="body"] { --gem-print-preview-font: var(--gem-font-body); }`,
2118
+ `${V}[data-gem-print-preview-font="accent"] { --gem-print-preview-font: var(--gem-font-accent, var(--gem-font-heading)); }`,
2119
+ // product-delivery-estimate: three steps on a line (timeline), or one sentence (line). Dates are the server's for its today
2120
+ // until the plugin rewrites them in the shopper's.
2121
+ `${D} { display: grid; gap: var(--gem-space-xs); font-size: var(--gem-text-sm); }`,
2122
+ `${D}__steps { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--gem-space-sm); margin: 0; padding: 0; list-style: none; }`,
2123
+ `${D}__step { position: relative; display: grid; justify-items: center; gap: var(--gem-space-2xs); text-align: center; }`,
2124
+ `${D}__step::before { content: ""; width: 0.75em; height: 0.75em; border-radius: var(--gem-radius-pill); background: var(--gem-color-accent); }`,
2125
+ `${D}__step + ${D}__step::after { content: ""; position: absolute; top: 0.375em; right: calc(50% + 0.75em); width: calc(100% - 1.5em + var(--gem-space-sm)); height: var(--gem-stroke, 1px); background: var(--gem-color-border); }`,
2126
+ `${D}__label { color: var(--gem-color-muted); font-size: var(--gem-text-xs); }`,
2127
+ `${D}__date { font-weight: 600; font-variant-numeric: tabular-nums; }`,
2128
+ `${D}__note { margin: 0; color: var(--gem-color-muted); font-size: var(--gem-text-xs); }`,
2129
+ `${D}[data-gem-delivery-style="line"] ${D}__steps { display: flex; flex-wrap: wrap; gap: var(--gem-space-2xs) var(--gem-space-sm); }`,
2130
+ `${D}[data-gem-delivery-style="line"] ${D}__step { display: inline-flex; gap: var(--gem-space-2xs); text-align: start; }`,
2131
+ `${D}[data-gem-delivery-style="line"] ${D}__step::before, ${D}[data-gem-delivery-style="line"] ${D}__step::after { content: none; }`,
2132
+ // product-design-switcher: one link per product that shares the design (pills, or cards with the sibling's photo).
2133
+ `${S} { display: grid; gap: var(--gem-space-xs); }`,
2134
+ `${S}__list { display: flex; flex-wrap: wrap; gap: var(--gem-space-xs); margin: 0; padding: 0; list-style: none; }`,
2135
+ `${S}__item { display: inline-flex; align-items: center; gap: var(--gem-space-2xs); min-height: var(--gem-tap-min); padding: var(--gem-space-2xs) var(--gem-space-sm); font-size: var(--gem-text-sm); color: var(--gem-color-text); text-decoration: none; background: var(--gem-color-background); border: var(--gem-stroke, 1px) solid var(--gem-color-border); border-radius: var(--gem-radius-pill); }`,
2136
+ `${S}__item:hover { border-color: var(--gem-color-text); }`,
2137
+ `${S}__item:focus-visible { outline: var(--gem-focus-ring); outline-offset: var(--gem-focus-offset); }`,
2138
+ `${S}__item[aria-current="page"] { border-color: var(--gem-color-text); box-shadow: inset 0 0 0 var(--gem-stroke, 1px) var(--gem-color-text); }`,
2139
+ `${S}__price { color: var(--gem-color-muted); font-variant-numeric: tabular-nums; }`,
2140
+ `${S}__image { display: none; }`,
2141
+ `${S}[data-gem-design-switcher-style="cards"] ${S}__list { display: grid; grid-template-columns: repeat(auto-fill, minmax(7rem, 1fr)); }`,
2142
+ `${S}[data-gem-design-switcher-style="cards"] ${S}__item { flex-direction: column; align-items: stretch; padding: var(--gem-space-xs); border-radius: var(--gem-radius-md); }`,
2143
+ `${S}[data-gem-design-switcher-style="cards"] ${S}__image { display: block; width: 100%; aspect-ratio: 1; object-fit: cover; border-radius: var(--gem-radius-sm); }`,
2144
+ `${S}[data-gem-design-switcher-style="cards"] ${S}__label { font-weight: 600; }`
2145
+ ];
2146
+ var GEM_PERSONALIZE_RUNTIME_CSS_LINES = [
2147
+ `${P}[data-gem-personalize-invalid] ${P}__input, ${P}[data-gem-personalize-invalid] ${P}__upload, ${P}[data-gem-personalize-invalid] ${P}__swatch, ${P}[data-gem-personalize-invalid] ${P}__choice-image { border-color: var(--gem-color-danger); }`,
2148
+ `${P}[data-gem-personalize-state="uploading"] ${P}__upload { cursor: progress; opacity: 0.7; }`,
2149
+ `${P}__count[data-gem-personalize-full] { color: var(--gem-color-text); font-weight: 600; }`
2150
+ ];
2151
+ var GEM_PRINT_PREVIEW_RUNTIME_CSS_LINES = [
2152
+ // The words: the theme font the element names, the colour it follows, as many lines as it allows, sized by the area's
2153
+ // height (container units); a word wider than the area is scaled down by the plugin with a transform, never re-laid out.
2154
+ `${V}__text { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: var(--gem-print-preview-lines, 2); max-width: 100%; margin: 0; overflow: hidden; overflow-wrap: anywhere; font-family: var(--gem-print-preview-font, var(--gem-font-heading)); font-size: calc(82cqh / var(--gem-print-preview-lines, 2)); line-height: 1.1; color: var(--gem-print-preview-color, var(--gem-color-text)); text-align: center; transform-origin: center; }`,
2155
+ `${V}[data-gem-print-preview-align="left"] ${V}__area { place-items: center start; }`,
2156
+ `${V}[data-gem-print-preview-align="left"] ${V}__text { text-align: start; transform-origin: left center; }`,
2157
+ `${V}[data-gem-print-preview-align="right"] ${V}__area { place-items: center end; }`,
2158
+ `${V}[data-gem-print-preview-align="right"] ${V}__text { text-align: end; transform-origin: right center; }`,
2159
+ `${V}__photo { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }`
2160
+ ];
2161
+ var GEM_POD_BUYBOX_CSS = GEM_POD_BUYBOX_CSS_LINES.join("\n") + "\n";
2162
+ var GEM_PERSONALIZE_RUNTIME_CSS = GEM_PERSONALIZE_RUNTIME_CSS_LINES.join("\n") + "\n";
2163
+ var GEM_PRINT_PREVIEW_RUNTIME_CSS = GEM_PRINT_PREVIEW_RUNTIME_CSS_LINES.join("\n") + "\n";
2164
+ var GEM_POD_CLASSES = [
2165
+ ...["", "__label", "__title", "__required", "__input", "__meta", "__help", "__count", "__options", "__option", "__swatch", "__choice-image", "__checkbox", "__upload", "__file", "__thumb", "__privacy", "__error"].map((part) => `gem-el-product-personalization${part}`),
2166
+ ...["", "__media", "__area", "__text", "__photo"].map((part) => `gem-el-product-print-preview${part}`),
2167
+ ...["", "__steps", "__step", "__label", "__date", "__note"].map((part) => `gem-el-product-delivery-estimate${part}`),
2168
+ ...["", "__list", "__item", "__label", "__price", "__image"].map((part) => `gem-el-product-design-switcher${part}`)
2169
+ ];
2170
+ var GEM_POD_CSS_VARIABLES = [
2171
+ "--gem-personalize-css",
2172
+ "--gem-personalize-swatch",
2173
+ "--gem-print-preview-css",
2174
+ "--gem-print-preview-x",
2175
+ "--gem-print-preview-y",
2176
+ "--gem-print-preview-w",
2177
+ "--gem-print-preview-h",
2178
+ "--gem-print-preview-lines",
2179
+ "--gem-print-preview-color",
2180
+ "--gem-print-preview-font",
2181
+ // No accent font token in gem-framework yet: a skin may set it, the preview falls back to the heading font.
2182
+ "--gem-font-accent"
2183
+ ];
2184
+ var GEM_POD_PLUGINS = [
2185
+ { key: "personalize", hook: "[data-gem-personalize]", when: "viewport", budget: { gzip: 6144 } },
2186
+ { key: "print-preview", hook: "[data-gem-print-preview]", when: "interaction", budget: { gzip: 6144 } },
2187
+ { key: "delivery-estimate", hook: "[data-gem-delivery-estimate]", when: "viewport", budget: { gzip: 6144 } }
2188
+ ];
2189
+ var MOUNTS = {
2190
+ personalize: { name: "mountPersonalize", file: "personalize", css: GEM_PERSONALIZE_RUNTIME_CSS },
2191
+ "print-preview": { name: "mountPrintPreview", file: "print-preview", css: GEM_PRINT_PREVIEW_RUNTIME_CSS },
2192
+ "delivery-estimate": { name: "mountDeliveryEstimate", file: "delivery-estimate", css: "" }
2193
+ };
2194
+ function gemPodChunks() {
2195
+ const chunks = {};
2196
+ for (const { key, hook, when } of GEM_POD_PLUGINS) {
2197
+ const { name, file, css } = MOUNTS[key];
2198
+ chunks[key] = {
2199
+ hook,
2200
+ when,
2201
+ imports: [`import { ${name} } from "../pod/${file}.js";`],
2202
+ body: [
2203
+ ` registerPlugin('${key}', ({ api }) => {`,
2204
+ " const on = (el, type, fn, opt) => { if (canListen(el)) el.addEventListener(type, fn, opt); };",
2205
+ ` const count = ${name}({ window, document, on, mark, api${css ? `, css: ${JSON.stringify(compactCss(css))}` : ""} }, onUnmount);`,
2206
+ ` api.emit('gem:${key}:ready', { source: 'gem-runtime', count });`,
2207
+ " });"
2208
+ ]
2209
+ };
2210
+ }
2211
+ return chunks;
2212
+ }
2213
+
738
2214
  // ../../packages/gem-framework/src/manifest.ts
739
2215
  var gemFrameworkPrimitiveGroups = [
740
2216
  {
@@ -930,6 +2406,19 @@ var gemFrameworkPrimitiveGroups = [
930
2406
  "gem-popup-shell__teaser"
931
2407
  ]
932
2408
  },
2409
+ {
2410
+ // The motion engine (motion-engine.ts, 0.19.0): the orbit, rail and turn markup a section draws, and the quick view panel
2411
+ // the quick-view plugin builds. Styled by their own base / runtime stylesheets, never by gem-framework.css.
2412
+ group: "motion",
2413
+ classes: [...GEM_MOTION_CLASSES]
2414
+ },
2415
+ {
2416
+ // The Boss Theme POD buy box (pod-engine.ts, 0.20.0): the four elements gemsite renders (personalization v2, print
2417
+ // preview, delivery estimate, design switcher) and what their plugins draw. Styled by dist/pod-buybox.css and the
2418
+ // personalize / print-preview chunks' own sheets, never by gem-framework.css.
2419
+ group: "pod",
2420
+ classes: [...GEM_POD_CLASSES]
2421
+ },
933
2422
  {
934
2423
  group: "sections",
935
2424
  classes: [
@@ -1103,6 +2592,24 @@ var GEM_POPUP_SHELL_CSS_LINES = [
1103
2592
  " .gem-popup-shell[data-gem-popup-media='background']:is([data-gem-popup-desktop='split-left'], [data-gem-popup-desktop='split-right'], [data-gem-popup-desktop='card']) .gem-popup-shell__close { color: var(--gem-color-text); }",
1104
2593
  " .gem-popup-shell[data-gem-popup-media='background']:is([data-gem-popup-desktop='split-left'], [data-gem-popup-desktop='split-right'], [data-gem-popup-desktop='card']) .gem-popup-shell__media::after { content: none; }",
1105
2594
  "}",
2595
+ // Ngăn bám mép (drawer-end, GemFactory gap #49, 21/10/2026): bảng size mở từ nút cạnh ô chọn size là ngăn cao hết màn ở mép
2596
+ // phải trên desktop (American Tall 640px, Alo 745px), không phải hộp giữa màn. Rộng theo --gem-popup-drawer-width (mặc định
2597
+ // 640px, không quá màn), bám mép CUỐI dòng (margin-inline: auto 0, nên dưới RTL là mép trái) và trượt vào từ đó
2598
+ // (--gem-motion-base / --gem-ease-enter, như ngăn giỏ của framework). Chỉ là bố cục desktop: mobile vẫn takeover / sheet.
2599
+ "@keyframes gem-popup-drawer-in { from { translate: calc(100% * var(--gem-popup-drawer-dir, 1)) 0; } }",
2600
+ ".gem-popup-shell__dialog:dir(rtl) { --gem-popup-drawer-dir: -1; }",
2601
+ "@media (min-width: 750px) {",
2602
+ " .gem-popup-shell[data-gem-popup-desktop='drawer-end'] .gem-popup-shell__dialog {",
2603
+ " width: min(var(--gem-popup-drawer-width, 640px), 100vw); max-width: 100vw; height: 100dvh; max-height: 100dvh;",
2604
+ " margin-block: 0; margin-inline: auto 0; border-radius: 0;",
2605
+ " animation: gem-popup-drawer-in var(--gem-motion-base, 180ms) var(--gem-ease-enter, ease-out);",
2606
+ " }",
2607
+ " .gem-popup-shell[data-gem-popup-desktop='drawer-end'] .gem-popup-shell__frame { grid-template-columns: 1fr; align-content: start; min-height: 100%; }",
2608
+ " .gem-popup-shell[data-gem-popup-desktop='drawer-end'] .gem-popup-shell__media { max-height: 240px; }",
2609
+ " .gem-popup-shell[data-gem-popup-desktop='drawer-end'] .gem-popup-shell__panel { justify-content: flex-start; align-items: stretch; text-align: start; }",
2610
+ "}",
2611
+ // SAU rule animation và cùng độ ưu tiên (bài học ngăn giỏ 02/10/2026: đặt trước hay yếu hơn thì ngăn vẫn trượt).
2612
+ "@media (prefers-reduced-motion: reduce) { .gem-popup-shell[data-gem-popup-desktop='drawer-end'] .gem-popup-shell__dialog { animation: none; } }",
1106
2613
  "@media (max-width: 749px) {",
1107
2614
  // Mobile: takeover is the whole screen; a sheet rises from the bottom to at most 88% of it. The image
1108
2615
  // sits under the text (top when media=top), capped at a third of the screen; with no image or the image
@@ -1138,6 +2645,9 @@ var GEM_POPUP_SHELL_CSS_LINES = [
1138
2645
  "@media (min-width: 750px) {",
1139
2646
  " :root:has([data-gem-canvas]) .gem-popup-shell[data-gem-popup-preview='true'][data-gem-popup-desktop='fullscreen'] { padding: 0; }",
1140
2647
  " :root:has([data-gem-canvas]) .gem-popup-shell[data-gem-popup-preview='true'][data-gem-popup-desktop='fullscreen'] .gem-popup-shell__dialog { height: 720px; max-height: none; }",
2648
+ // Ngăn trong canvas: cao như khung xem trước của fullscreen, đứng yên (không trượt mỗi lần canvas vẽ lại).
2649
+ " :root:has([data-gem-canvas]) .gem-popup-shell[data-gem-popup-preview='true'][data-gem-popup-desktop='drawer-end'] { padding-inline: 0; }",
2650
+ " :root:has([data-gem-canvas]) .gem-popup-shell[data-gem-popup-preview='true'][data-gem-popup-desktop='drawer-end'] .gem-popup-shell__dialog { height: 720px; max-height: none; animation: none; }",
1141
2651
  "}",
1142
2652
  "@media (max-width: 749px) {",
1143
2653
  " :root:has([data-gem-canvas]) .gem-popup-shell[data-gem-popup-preview='true'] { padding: 0; }",
@@ -1333,7 +2843,7 @@ var GEM_INLINE_MARK_CSS_LINES = [
1333
2843
  ":where(.gem-mark--alert) { color: var(--gem-color-danger, #b42318); }",
1334
2844
  ":where(.gem-mark--highlight.gem-mark--accent), :where(.gem-mark--highlight.gem-mark--alert) { color: var(--gem-mark-highlight-ink, #1f1d18); }",
1335
2845
  ".gem-mark-link { color: var(--gem-color-accent); text-decoration-line: underline; text-decoration-thickness: from-font; text-underline-offset: 0.18em; }",
1336
- ".gem-mark-link:hover { text-decoration-thickness: 2px; }",
2846
+ ".gem-mark-link:hover { text-decoration-thickness: var(--gem-underline-thickness, 2px); }",
1337
2847
  // Bút dạ thắng màu link: chữ trên nền vàng giữ mực đậm, dù span nằm trong link hay link nằm trong span (review #740 đo màu nhấn
1338
2848
  // trên vàng 4,32:1 ở nền trắng, 2,47:1 trên băng tối, 1,05:1 trên băng màu nhấn).
1339
2849
  ".gem-mark-link.gem-mark--highlight, .gem-mark--highlight .gem-mark-link { color: var(--gem-mark-highlight-ink, #1f1d18); }"
@@ -1552,6 +3062,14 @@ function createPrimitiveCss() {
1552
3062
  " cursor: not-allowed;",
1553
3063
  " opacity: 0.55;",
1554
3064
  "}",
3065
+ // Button shape of ONE section (GemFactory gap #69, 22/10/2026): matching a source, a section needs square, softly rounded
3066
+ // or pill buttons whatever the preset's button radius is, and writing border-radius on .gem-button takes the button
3067
+ // away from the preset for good (GemFactory's button_token_shadowed). `data-gem-button-shape` on the section root (or on
3068
+ // one button) sets the radius the button already reads; with no attribute the preset decides, as before. Square and
3069
+ // pill are shapes, so they do not scale; soft is a size, so it follows the preset's radius scale (--gem-style-radius).
3070
+ "[data-gem-button-shape='square'] { --gem-radius-button: 0px; }",
3071
+ "[data-gem-button-shape='soft'] { --gem-radius-button: var(--gem-radius-sm); }",
3072
+ "[data-gem-button-shape='pill'] { --gem-radius-button: 999px; }",
1555
3073
  ".gem-input:disabled, .gem-select:disabled, .gem-textarea:disabled, .gem-quantity-input__button:disabled {",
1556
3074
  " cursor: not-allowed;",
1557
3075
  " opacity: 0.6;",
@@ -2929,8 +4447,8 @@ function createPrimitiveCss() {
2929
4447
  function createGemSectionRuntimeContractCss(selector = "[data-gem-section-library-id]", options = {}) {
2930
4448
  const background = options.backgroundVar ?? "var(--gem-color-background)";
2931
4449
  const surface = options.surfaceVar ?? "var(--gem-color-surface)";
2932
- const text = options.textVar ?? "var(--gem-color-text)";
2933
- const mutedText = options.mutedTextVar ?? `color-mix(in srgb, ${text} 72%, transparent)`;
4450
+ const text2 = options.textVar ?? "var(--gem-color-text)";
4451
+ const mutedText = options.mutedTextVar ?? `color-mix(in srgb, ${text2} 72%, transparent)`;
2934
4452
  const accent = options.accentVar ?? "var(--gem-color-accent)";
2935
4453
  const accentText = options.accentTextVar ?? "var(--gem-color-accent-text, #ffffff)";
2936
4454
  const border = options.borderVar ?? "var(--gem-color-border)";
@@ -2940,7 +4458,7 @@ function createGemSectionRuntimeContractCss(selector = "[data-gem-section-librar
2940
4458
  `${selector} {`,
2941
4459
  " box-sizing: border-box;",
2942
4460
  ` background: ${background};`,
2943
- ` color: ${text};`,
4461
+ ` color: ${text2};`,
2944
4462
  "}",
2945
4463
  `${selector} *,`,
2946
4464
  `${selector} *::before,`,
@@ -2999,7 +4517,7 @@ function createGemSectionRuntimeContractCss(selector = "[data-gem-section-librar
2999
4517
  " border-radius: var(--gem-radius-pill, 999px);",
3000
4518
  " padding: 6px 10px;",
3001
4519
  ` background: ${surface};`,
3002
- ` color: ${text};`,
4520
+ ` color: ${text2};`,
3003
4521
  " font-size: 0.8125rem;",
3004
4522
  " font-weight: 700;",
3005
4523
  "}",
@@ -5427,6 +6945,763 @@ function bundleEngineRuntimeScript() {
5427
6945
  }
5428
6946
 
5429
6947
  // ../../packages/gem-framework/src/runtime.ts
6948
+ var cartDrawerPluginRegistrationLines = [
6949
+ " registerPlugin('cart-drawer', ({ api }) => {",
6950
+ " const drawers = document.querySelectorAll('[data-gem-cart-drawer], [data-gem-cart-canvas], .gem-cart-drawer');",
6951
+ ` const cartLinks = document.querySelectorAll('[data-gem-cart-drawer-trigger], [data-gem-cart-toggle], a[href*="/cart"]');`,
6952
+ ` const cartForms = document.querySelectorAll('form[action*="/cart/add"], form[action*="/cart"]');`,
6953
+ " const drawer = drawers[0];",
6954
+ " const setDrawerA11y = (isOpen) => {",
6955
+ " if (!drawer) return;",
6956
+ " drawer.setAttribute && drawer.setAttribute('aria-expanded', isOpen ? 'true' : 'false');",
6957
+ " const panel = drawer.querySelector && drawer.querySelector('[data-gem-cart-canvas-panel], .gem-cart-drawer__panel');",
6958
+ " if (panel && panel.setAttribute) panel.setAttribute('aria-hidden', isOpen ? 'false' : 'true');",
6959
+ " };",
6960
+ " const openDrawer = (reason, href) => {",
6961
+ " if (drawer && drawer.tagName === 'DETAILS') drawer.open = true;",
6962
+ " if (drawer && drawer.dataset) drawer.dataset.gemCartDrawerState = 'open';",
6963
+ " setDrawerA11y(true);",
6964
+ " api.emit('gem:cart:open', { source: 'gem-runtime', reason, href });",
6965
+ " };",
6966
+ " const closeDrawer = (reason) => {",
6967
+ " if (drawer && drawer.tagName === 'DETAILS') drawer.open = false;",
6968
+ " if (drawer && drawer.dataset) drawer.dataset.gemCartDrawerState = 'closed';",
6969
+ " setDrawerA11y(false);",
6970
+ " api.emit('gem:cart:close', { source: 'gem-runtime', reason });",
6971
+ " };",
6972
+ " let bound = 0;",
6973
+ " drawers.forEach((drawer) => {",
6974
+ " if (!mark(drawer, 'cart-drawer')) return;",
6975
+ " drawer.dataset.gemCartDrawerState = drawer.hasAttribute('open') ? 'open' : 'ready';",
6976
+ " setDrawerA11y(drawer.hasAttribute('open'));",
6977
+ " drawer.querySelectorAll('[data-gem-cart-drawer-close], [data-gem-cart-canvas-close]').forEach((control) => { if (!canListen(control)) return; control.addEventListener('click', (event) => {",
6978
+ " event.preventDefault && event.preventDefault();",
6979
+ " closeDrawer('close-control');",
6980
+ " }); });",
6981
+ " if (canListen(drawer)) drawer.addEventListener('toggle', () => { drawer.dataset.gemCartDrawerState = drawer.hasAttribute('open') ? 'open' : 'closed'; });",
6982
+ " bound += 1;",
6983
+ " });",
6984
+ " cartLinks.forEach((link) => {",
6985
+ " if (!mark(link, 'cart-link')) return;",
6986
+ " if (!canListen(link)) return;",
6987
+ " link.addEventListener('click', (event) => {",
6988
+ " if (drawer && (link.hasAttribute('data-gem-cart-drawer-trigger') || link.hasAttribute('data-gem-cart-toggle'))) event.preventDefault && event.preventDefault();",
6989
+ " openDrawer('cart-link', link.href || undefined);",
6990
+ " });",
6991
+ " bound += 1;",
6992
+ " });",
6993
+ " if (canListen(document)) document.addEventListener('keydown', (event) => { if (event.key === 'Escape' && drawer && drawer.hasAttribute('open')) closeDrawer('escape'); });",
6994
+ " cartForms.forEach((form) => {",
6995
+ " if (!mark(form, 'cart-form')) return;",
6996
+ " if (!canListen(form)) return;",
6997
+ " form.addEventListener('submit', () => api.emit('gem:cart:submit', { source: 'gem-runtime', action: form.getAttribute('action') || undefined }));",
6998
+ " bound += 1;",
6999
+ " });",
7000
+ " api.emit('gem:cart:ready', { source: 'gem-runtime', count: bound });",
7001
+ " });"
7002
+ ];
7003
+ var mobileMenuPluginRegistrationLines = [
7004
+ " registerPlugin('mobile-menu', ({ api }) => {",
7005
+ " const menus = document.querySelectorAll('[data-gem-mobile-menu], details.gem-shell-header__drawer');",
7006
+ " let bound = 0;",
7007
+ " menus.forEach((menu) => {",
7008
+ " if (!mark(menu, 'mobile-menu')) return;",
7009
+ " const setMenuA11y = () => {",
7010
+ " menu.setAttribute && menu.setAttribute('aria-expanded', menu.open ? 'true' : 'false');",
7011
+ " const panel = menu.querySelector && menu.querySelector('[data-gem-mobile-menu-panel], .gem-shell-header__drawer-panel');",
7012
+ " if (panel && panel.setAttribute) panel.setAttribute('aria-hidden', menu.open ? 'false' : 'true');",
7013
+ " };",
7014
+ " menu.dataset.gemMenuState = menu.open ? 'open' : 'closed';",
7015
+ " setMenuA11y();",
7016
+ " if (!canListen(menu)) return;",
7017
+ " menu.addEventListener('toggle', () => {",
7018
+ " menu.dataset.gemMenuState = menu.open ? 'open' : 'closed';",
7019
+ " setMenuA11y();",
7020
+ " if (menu.open) {",
7021
+ " const panel = menu.querySelector && menu.querySelector('[data-gem-mobile-menu-panel], .gem-shell-header__drawer-panel');",
7022
+ ` const firstFocusable = panel && panel.querySelector && panel.querySelector('a[href], button, select, input, textarea, summary, [tabindex]:not([tabindex="-1"])');`,
7023
+ " if (firstFocusable && firstFocusable.focus) firstFocusable.focus();",
7024
+ " }",
7025
+ " api.emit(menu.open ? 'gem:menu:open' : 'gem:menu:close', { source: 'gem-runtime', id: menu.id || undefined });",
7026
+ " });",
7027
+ " menu.addEventListener('keydown', (event) => {",
7028
+ " if (event.key !== 'Escape' || !menu.open) return;",
7029
+ " menu.open = false;",
7030
+ " menu.dataset.gemMenuState = 'closed';",
7031
+ " setMenuA11y();",
7032
+ " const summary = menu.querySelector && menu.querySelector('summary');",
7033
+ " if (summary && summary.focus) summary.focus();",
7034
+ " });",
7035
+ " bound += 1;",
7036
+ " });",
7037
+ " api.emit('gem:menu:ready', { source: 'gem-runtime', count: bound });",
7038
+ " });"
7039
+ ];
7040
+ var quantityInputPluginRegistrationLines = [
7041
+ " registerPlugin('quantity-input', ({ api }) => {",
7042
+ " const controls = document.querySelectorAll('[data-gem-quantity-input], .gem-quantity-input');",
7043
+ " let bound = 0;",
7044
+ " const clamp = (value, min, max) => Math.max(min, Math.min(max, value));",
7045
+ " controls.forEach((control) => {",
7046
+ " if (!mark(control, 'quantity-input')) return;",
7047
+ ` const input = control.querySelector && control.querySelector('input[type="number"]');`,
7048
+ " if (!input) return;",
7049
+ " const setValue = (nextValue, reason) => {",
7050
+ " const min = Number.isFinite(Number(input.min)) ? Number(input.min) : 0;",
7051
+ " const max = Number.isFinite(Number(input.max)) && input.max !== '' ? Number(input.max) : Number.MAX_SAFE_INTEGER;",
7052
+ " const value = clamp(Number(nextValue) || min, min, max);",
7053
+ " input.value = String(value);",
7054
+ " api.emit('gem:quantity:change', { source: 'gem-runtime', value, reason, name: input.name || undefined });",
7055
+ " };",
7056
+ " if (!canListen(control) || !canListen(input)) return;",
7057
+ " control.addEventListener('click', (event) => {",
7058
+ " const button = event.target && event.target.closest ? event.target.closest('[data-gem-quantity-change]') : null;",
7059
+ " if (!button) return;",
7060
+ " event.preventDefault && event.preventDefault();",
7061
+ " const delta = Number(button.getAttribute('data-gem-quantity-change') || '0');",
7062
+ " setValue(Number(input.value || 0) + delta, delta > 0 ? 'increase' : 'decrease');",
7063
+ " });",
7064
+ " input.addEventListener('change', () => setValue(input.value, 'input'));",
7065
+ " bound += 1;",
7066
+ " });",
7067
+ " api.emit('gem:quantity:ready', { source: 'gem-runtime', count: bound });",
7068
+ " });"
7069
+ ];
7070
+ var variantSelectsPluginRegistrationLines = [
7071
+ " registerPlugin('variant-selects', ({ api }) => {",
7072
+ ` const selects = document.querySelectorAll('[data-gem-variant-select], select[name="id"]');`,
7073
+ " let bound = 0;",
7074
+ " selects.forEach((select) => {",
7075
+ " if (!mark(select, 'variant-selects')) return;",
7076
+ " let hasInitialized = false;",
7077
+ " const update = () => {",
7078
+ " const option = select.options && select.options[select.selectedIndex];",
7079
+ " const available = !option || option.getAttribute('data-gem-variant-available') !== 'false';",
7080
+ " const price = option && option.getAttribute('data-gem-variant-price');",
7081
+ " const mediaId = option && option.getAttribute('data-gem-variant-media-id');",
7082
+ " const form = select.closest && select.closest('form');",
7083
+ " const scope = (form && form.closest && form.closest('[data-gem-section-id], .gem-section, [data-qa-surface]')) || (select.closest && select.closest('[data-gem-section-id], .gem-section, [data-qa-surface]')) || document;",
7084
+ ` const submit = form && form.querySelector && form.querySelector('[type="submit"]');`,
7085
+ " const status = form && form.querySelector && form.querySelector('[data-gem-variant-status]');",
7086
+ " const priceOutput = scope && scope.querySelector && scope.querySelector('[data-gem-variant-price-output]');",
7087
+ " const availabilityOutput = scope && scope.querySelector && scope.querySelector('[data-gem-variant-availability]');",
7088
+ " if (form && form.dataset) form.dataset.gemSelectedVariantId = String(select.value || '');",
7089
+ " if (form && form.dataset && price) form.dataset.gemSelectedVariantPrice = price;",
7090
+ " if (form && form.dataset && mediaId) form.dataset.gemSelectedMediaId = mediaId;",
7091
+ " if (submit) submit.disabled = !available;",
7092
+ " if (submit) submit.setAttribute('aria-disabled', available ? 'false' : 'true');",
7093
+ " if (status) status.textContent = available ? 'Selected variant available' : 'Selected variant unavailable';",
7094
+ " if (hasInitialized && price && priceOutput) priceOutput.textContent = price;",
7095
+ " if (availabilityOutput) {",
7096
+ " availabilityOutput.textContent = available ? 'In stock' : 'Sold out';",
7097
+ " availabilityOutput.classList && availabilityOutput.classList.toggle('gem-product-main__availability--in-stock', available);",
7098
+ " availabilityOutput.classList && availabilityOutput.classList.toggle('gem-product-main__availability--sold-out', !available);",
7099
+ " }",
7100
+ " if (mediaId) {",
7101
+ ` const escapedMediaId = String(mediaId).replace(/["\\\\]/g, '\\\\$&');`,
7102
+ " const gallery = (scope && scope.querySelector && scope.querySelector('[data-gem-media-gallery], .gem-product-media-gallery')) || document.querySelector('[data-gem-media-gallery], .gem-product-media-gallery');",
7103
+ ` const thumb = gallery && gallery.querySelector && gallery.querySelector('[data-gem-media-id="' + escapedMediaId + '"]');`,
7104
+ " const mediaIndex = thumb && thumb.getAttribute && thumb.getAttribute('data-gem-media-index');",
7105
+ " if (thumb && typeof thumb.click === 'function') thumb.click();",
7106
+ " if (gallery && mediaIndex !== null && mediaIndex !== undefined) {",
7107
+ " gallery.dataset && (gallery.dataset.gemMediaActiveIndex = String(mediaIndex));",
7108
+ " gallery.querySelectorAll && gallery.querySelectorAll('[data-gem-media-thumb]').forEach((control) => {",
7109
+ " const isActive = control.getAttribute && control.getAttribute('data-gem-media-index') === String(mediaIndex);",
7110
+ " if (isActive) control.setAttribute('aria-current', 'true'); else control.removeAttribute('aria-current');",
7111
+ " if (control.dataset) control.dataset.gemMediaActive = isActive ? 'true' : 'false';",
7112
+ " });",
7113
+ " }",
7114
+ " }",
7115
+ " api.emit('gem:variant:change', { source: 'gem-runtime', variantId: select.value || undefined, available, price, mediaId });",
7116
+ " hasInitialized = true;",
7117
+ " };",
7118
+ " if (!canListen(select)) return;",
7119
+ " select.addEventListener('change', update);",
7120
+ " update();",
7121
+ " bound += 1;",
7122
+ " });",
7123
+ " api.emit('gem:variant:ready', { source: 'gem-runtime', count: bound });",
7124
+ " });"
7125
+ ];
7126
+ var predictiveSearchPluginRegistrationLines = [
7127
+ " registerPlugin('predictive-search', ({ api }) => {",
7128
+ ` const forms = document.querySelectorAll('[data-gem-predictive-search], [data-gem-search-canvas], form[action*="/search"], form[role="search"]');`,
7129
+ " const toggles = document.querySelectorAll('[data-gem-search-toggle]');",
7130
+ " const canvas = document.querySelector('[data-gem-search-canvas]');",
7131
+ " let bound = 0;",
7132
+ " const setCanvasState = (state, reason = 'api') => {",
7133
+ " if (!canvas || !canvas.dataset) return false;",
7134
+ " canvas.dataset.gemSearchCanvasState = state;",
7135
+ " canvas.setAttribute && canvas.setAttribute('aria-hidden', state === 'open' ? 'false' : 'true');",
7136
+ " if (state === 'open') {",
7137
+ ` const input = canvas.querySelector && canvas.querySelector('input[type="search"], input[name="q"]');`,
7138
+ " if (input && input.focus) input.focus();",
7139
+ " }",
7140
+ " api.emit(state === 'open' ? 'gem:search:open' : 'gem:search:close', { source: 'gem-runtime', reason });",
7141
+ " return true;",
7142
+ " };",
7143
+ " forms.forEach((form) => {",
7144
+ " if (!mark(form, 'predictive-search')) return;",
7145
+ ` const input = form.querySelector && form.querySelector('input[type="search"], input[name="q"]');`,
7146
+ " const results = form.querySelector && form.querySelector('[data-gem-predictive-search-results]');",
7147
+ " const setState = (state, value = '') => {",
7148
+ " if (form.dataset) form.dataset.gemPredictiveSearchState = state;",
7149
+ " if (results) {",
7150
+ " results.hidden = state === 'closed';",
7151
+ " if (results.dataset) results.dataset.gemPredictiveSearchState = state;",
7152
+ " results.setAttribute && results.setAttribute('aria-hidden', state === 'closed' ? 'true' : 'false');",
7153
+ " if (state === 'typing') results.textContent = value ? 'Search suggestions for ' + value : 'Start typing to search';",
7154
+ " if (state === 'empty') results.textContent = 'Start typing to search';",
7155
+ " }",
7156
+ " };",
7157
+ " if (results) {",
7158
+ " results.hidden = true;",
7159
+ " results.setAttribute && results.setAttribute('role', 'status');",
7160
+ " results.setAttribute && results.setAttribute('aria-live', 'polite');",
7161
+ " }",
7162
+ " setState('closed');",
7163
+ " if (canListen(input)) input.addEventListener('focus', () => { setState(input.value ? 'typing' : 'empty', input.value || ''); api.emit('gem:search:open', { source: 'gem-runtime', value: input.value || '' }); });",
7164
+ " const closeOnEscape = (event) => { if (event.key === 'Escape') { event.preventDefault && event.preventDefault(); event.stopPropagation && event.stopPropagation(); setState('closed'); api.emit('gem:search:close', { source: 'gem-runtime', reason: 'escape' }); } };",
7165
+ " if (canListen(input)) input.addEventListener('input', () => { setState(input.value ? 'typing' : 'empty', input.value || ''); api.emit('gem:search:input', { source: 'gem-runtime', value: input.value || '' }); });",
7166
+ " if (canListen(input)) input.addEventListener('keydown', closeOnEscape);",
7167
+ " if (canListen(form)) form.addEventListener('keydown', closeOnEscape);",
7168
+ " if (canListen(form)) form.addEventListener('submit', () => { setState('submit', input && input.value || ''); api.emit('gem:search:submit', { source: 'gem-runtime', value: input && input.value || '' }); });",
7169
+ " bound += 1;",
7170
+ " });",
7171
+ " if (canvas) {",
7172
+ " canvas.dataset && (canvas.dataset.gemSearchCanvasState = 'closed');",
7173
+ " canvas.setAttribute && canvas.setAttribute('aria-hidden', 'true');",
7174
+ " if (mark(canvas, 'search-canvas')) bound += 1;",
7175
+ " if (canListen(canvas)) canvas.addEventListener('keydown', (event) => { if (event.key === 'Escape') setCanvasState('closed', 'escape'); });",
7176
+ " }",
7177
+ " toggles.forEach((toggle) => {",
7178
+ " if (!mark(toggle, 'search-toggle')) return;",
7179
+ " if (!canListen(toggle)) return;",
7180
+ " toggle.addEventListener('click', (event) => {",
7181
+ " if (canvas) event.preventDefault && event.preventDefault();",
7182
+ " setCanvasState('open', 'toggle');",
7183
+ " });",
7184
+ " bound += 1;",
7185
+ " });",
7186
+ " api.emit('gem:search:ready', { source: 'gem-runtime', count: bound });",
7187
+ " });"
7188
+ ];
7189
+ var localizationPluginRegistrationLines = [
7190
+ " registerPlugin('localization', ({ api }) => {",
7191
+ ` const forms = document.querySelectorAll('form[action*="/localization"], form[action*="localization"], [data-gem-footer-localization], [data-gem-header-localization], .gem-navigation-shell__localization, .gem-shell-footer__localization, .gem-shell-header__localization-form');`,
7192
+ " let bound = 0;",
7193
+ " forms.forEach((form) => {",
7194
+ " if (!mark(form, 'localization')) return;",
7195
+ " form.dataset && (form.dataset.gemLocalizationState = 'ready');",
7196
+ ` form.querySelectorAll && form.querySelectorAll('select[name="country_code"], select[name="locale_code"]').forEach((select) => {`,
7197
+ " if (!canListen(select)) return;",
7198
+ " select.addEventListener('change', () => {",
7199
+ " form.dataset && (form.dataset.gemLocalizationState = 'changed');",
7200
+ " api.emit('gem:localization:change', { source: 'gem-runtime', name: select.name || undefined, value: select.value || undefined });",
7201
+ " });",
7202
+ " });",
7203
+ " canListen(form) && form.addEventListener('submit', () => {",
7204
+ " form.dataset && (form.dataset.gemLocalizationState = 'submit');",
7205
+ " api.emit('gem:localization:submit', { source: 'gem-runtime' });",
7206
+ " });",
7207
+ " bound += 1;",
7208
+ " });",
7209
+ " api.emit('gem:localization:ready', { source: 'gem-runtime', count: bound });",
7210
+ " });"
7211
+ ];
7212
+ var disclosurePluginRegistrationLines = [
7213
+ " registerPlugin('disclosure', ({ api }) => {",
7214
+ " const items = document.querySelectorAll('details[data-gem-disclosure], .gem-accordion details, .gem-tabs details, .gem-footer-shell details, [data-gem-footer-shell] details');",
7215
+ " let bound = 0;",
7216
+ " items.forEach((item) => {",
7217
+ " if (!mark(item, 'disclosure')) return;",
7218
+ " item.dataset.gemDisclosureState = item.open ? 'open' : 'closed';",
7219
+ " if (!canListen(item)) return;",
7220
+ " item.addEventListener('toggle', () => {",
7221
+ " item.dataset.gemDisclosureState = item.open ? 'open' : 'closed';",
7222
+ " api.emit(item.open ? 'gem:disclosure:open' : 'gem:disclosure:close', { source: 'gem-runtime', id: item.id || undefined });",
7223
+ " });",
7224
+ " item.addEventListener('keydown', (event) => {",
7225
+ " if (event.key !== 'Escape' || !item.open) return;",
7226
+ " event.preventDefault && event.preventDefault();",
7227
+ " item.open = false;",
7228
+ " item.dataset.gemDisclosureState = 'closed';",
7229
+ " const summary = item.querySelector && item.querySelector('summary');",
7230
+ " if (summary && summary.focus) summary.focus();",
7231
+ " api.emit('gem:disclosure:close', { source: 'gem-runtime', id: item.id || undefined, reason: 'escape' });",
7232
+ " });",
7233
+ " bound += 1;",
7234
+ " });",
7235
+ " api.emit('gem:disclosure:ready', { source: 'gem-runtime', count: bound });",
7236
+ " });"
7237
+ ];
7238
+ var tabsPluginRegistrationLines = [
7239
+ " registerPlugin('tabs', ({ api }) => {",
7240
+ // Ported from the Liquid theme's initTabs (gem-framework.js) so BOTH render paths honour the same
7241
+ // contract: [data-gem-tabs] root, [data-gem-tab=key] controls, [data-gem-panel=key] panels. The
7242
+ // headless runtime had no tabs plugin at all, while gemcapture's guidance told rebuilds to fake
7243
+ // tabs with <details data-gem-disclosure> — which is an accordion, not tabs, and binds nothing on
7244
+ // the Liquid side. Now the guidance names one contract and both runtimes implement it.
7245
+ " const roots = document.querySelectorAll('[data-gem-tabs]');",
7246
+ " let bound = 0;",
7247
+ " roots.forEach((root) => {",
7248
+ " if (!mark(root, 'tabs')) return;",
7249
+ " const tabs = Array.prototype.slice.call(root.querySelectorAll('[data-gem-tab]'));",
7250
+ " const panels = Array.prototype.slice.call(root.querySelectorAll('[data-gem-panel]'));",
7251
+ " if (!tabs.length || !panels.length) return;",
7252
+ " const select = (key, userInitiated) => {",
7253
+ " tabs.forEach((t) => t.setAttribute('aria-selected', String(t.getAttribute('data-gem-tab') === key)));",
7254
+ " let active = null;",
7255
+ " panels.forEach((p) => {",
7256
+ " if (p.getAttribute('data-gem-panel') === key) { p.removeAttribute('hidden'); active = p; }",
7257
+ " else p.setAttribute('hidden', '');",
7258
+ " });",
7259
+ // Same smart-scroll guard as the Liquid implementation: switching a tab must never jump the page,
7260
+ // and block:'nearest' is a no-op when the panel is already on screen.
7261
+ " if (userInitiated && active && active.getBoundingClientRect) {",
7262
+ " const r = active.getBoundingClientRect();",
7263
+ " const vh = window.innerHeight || document.documentElement.clientHeight;",
7264
+ " if (!(r.top < vh * 0.85 && r.bottom > vh * 0.15) && active.scrollIntoView) {",
7265
+ " active.scrollIntoView({ behavior: 'smooth', block: 'nearest' });",
7266
+ " }",
7267
+ " }",
7268
+ " api.emit('gem:tabs:change', { source: 'gem-runtime', key: key });",
7269
+ " };",
7270
+ " tabs.forEach((t) => {",
7271
+ " if (!canListen(t)) return;",
7272
+ " t.addEventListener('click', () => select(t.getAttribute('data-gem-tab'), true));",
7273
+ " });",
7274
+ // Seed from aria-selected so a captured `state: \"active\"` tab opens on the correct panel.
7275
+ " const initial = tabs.filter((t) => t.getAttribute('aria-selected') === 'true')[0] || tabs[0];",
7276
+ " select(initial.getAttribute('data-gem-tab'), false);",
7277
+ " bound += 1;",
7278
+ " });",
7279
+ " api.emit('gem:tabs:ready', { source: 'gem-runtime', count: bound });",
7280
+ " });"
7281
+ ];
7282
+ var mediaGalleryPluginRegistrationLines = [
7283
+ " registerPlugin('media-gallery', ({ api }) => {",
7284
+ " const galleries = document.querySelectorAll('[data-gem-media-gallery], .gem-media-gallery, .gem-product-media-gallery');",
7285
+ " let bound = 0;",
7286
+ " galleries.forEach((gallery) => {",
7287
+ // A wrapper matched only by its class (the thumbnail-stack <section class="gem-product-media-gallery">) around a
7288
+ // [data-gem-media-gallery] hook is not a second gallery: binding both ran every click twice and the outer one
7289
+ // renumbered the thumbnails' data-gem-media-index by counting every <img>, the main image included.
7290
+ " const hooked = !!(gallery.hasAttribute && gallery.hasAttribute('data-gem-media-gallery'));",
7291
+ " if (!hooked && gallery.querySelector && gallery.querySelector('[data-gem-media-gallery]')) return;",
7292
+ " if (!mark(gallery, 'media-gallery')) return;",
7293
+ // Main image + thumbnails (product-media-gallery.thumbnail-stack.v1): a thumbnail click puts its
7294
+ // data-gem-media-src / -alt on [data-gem-media-main], as the section's behavior.js did. Without this the plugin
7295
+ // only moved aria-current and the big image never changed. The markup's data-gem-media-index is kept.
7296
+ " const main = gallery.querySelector && gallery.querySelector('[data-gem-media-main]');",
7297
+ " const thumbs = main && gallery.querySelectorAll ? Array.prototype.slice.call(gallery.querySelectorAll('[data-gem-media-item]')) : [];",
7298
+ " if (main && thumbs.length) {",
7299
+ " gallery.dataset.gemMediaState = 'ready';",
7300
+ " const choose = (item, reason) => {",
7301
+ " const src = item.getAttribute('data-gem-media-src');",
7302
+ " const alt = item.getAttribute('data-gem-media-alt');",
7303
+ " if (src) { main.setAttribute('src', src); main.removeAttribute('srcset'); if (alt != null) main.setAttribute('alt', alt); }",
7304
+ " const index = Number(item.getAttribute('data-gem-media-index') || thumbs.indexOf(item));",
7305
+ " thumbs.forEach((thumb) => { const on = thumb === item; thumb.setAttribute('aria-current', String(on)); if (thumb.dataset) thumb.dataset.gemMediaActive = on ? 'true' : 'false'; });",
7306
+ " gallery.dataset.gemMediaActiveIndex = String(index);",
7307
+ " if (gallery.dispatchEvent && typeof window.CustomEvent === 'function') gallery.dispatchEvent(new window.CustomEvent('gem:media:select', { bubbles: true, detail: { index } }));",
7308
+ " api.emit('gem:media:change', { source: 'gem-runtime', index, galleryId: gallery.id || undefined, reason });",
7309
+ " };",
7310
+ " if (canListen(gallery)) gallery.addEventListener('click', (event) => {",
7311
+ " const item = event.target && event.target.closest ? event.target.closest('[data-gem-media-item]') : null;",
7312
+ " if (!item || !gallery.contains(item)) return;",
7313
+ " if (event.preventDefault) event.preventDefault();",
7314
+ " choose(item, 'click');",
7315
+ " });",
7316
+ " bound += 1;",
7317
+ " return;",
7318
+ " }",
7319
+ " const items = gallery.querySelectorAll('[data-gem-media-item], img, video, model-viewer');",
7320
+ " const thumbControls = gallery.querySelectorAll('[data-gem-media-thumb]');",
7321
+ " const controls = thumbControls.length ? thumbControls : items;",
7322
+ " gallery.dataset.gemMediaState = 'ready';",
7323
+ " items.forEach((item, index) => {",
7324
+ " if (!item.dataset) return;",
7325
+ " item.dataset.gemMediaIndex = String(index);",
7326
+ " });",
7327
+ " const activate = (target, reason = 'click') => {",
7328
+ " if (!target) return;",
7329
+ " const index = target.dataset && target.dataset.gemMediaIndex ? Number(target.dataset.gemMediaIndex) : undefined;",
7330
+ " if (!Number.isFinite(index)) return;",
7331
+ " controls.forEach((control) => {",
7332
+ " const isActive = control.dataset && Number(control.dataset.gemMediaIndex) === index;",
7333
+ " if (isActive) control.setAttribute('aria-current', 'true'); else control.removeAttribute('aria-current');",
7334
+ " if (control.dataset) control.dataset.gemMediaActive = isActive ? 'true' : 'false';",
7335
+ " });",
7336
+ " gallery.dataset.gemMediaActiveIndex = String(index);",
7337
+ " api.emit('gem:media:change', { source: 'gem-runtime', index, galleryId: gallery.id || undefined, reason });",
7338
+ " };",
7339
+ ` const firstActive = gallery.querySelector('[aria-current="true"], [data-gem-media-thumb]');`,
7340
+ " if (firstActive) activate(firstActive, 'init');",
7341
+ " if (canListen(gallery)) gallery.addEventListener('click', (event) => {",
7342
+ " const target = event.target && event.target.closest ? event.target.closest('[data-gem-media-item], img, video, model-viewer') : null;",
7343
+ " if (!target) return;",
7344
+ " activate(target, 'click');",
7345
+ " });",
7346
+ " controls.forEach((control) => { if (!canListen(control)) return; control.addEventListener('keydown', (event) => {",
7347
+ " if (!['Enter', ' '].includes(event.key)) return;",
7348
+ " event.preventDefault && event.preventDefault();",
7349
+ " activate(control, 'keyboard');",
7350
+ " }); });",
7351
+ " bound += 1;",
7352
+ " });",
7353
+ " api.emit('gem:media:ready', { source: 'gem-runtime', count: bound });",
7354
+ " });"
7355
+ ];
7356
+ var carouselPluginRegistrationLines = [
7357
+ " registerPlugin('carousel', ({ api }) => {",
7358
+ " const carousels = document.querySelectorAll('[data-gem-carousel], .gem-carousel');",
7359
+ " let bound = 0;",
7360
+ " carousels.forEach((carousel) => {",
7361
+ " if (!mark(carousel, 'carousel')) return;",
7362
+ " const viewport = carousel.querySelector && (carousel.querySelector('[data-gem-carousel-viewport]') || carousel.querySelector('.gem-carousel__viewport'));",
7363
+ " const slides = carousel.querySelectorAll('[data-gem-carousel-slide], .gem-carousel__slide');",
7364
+ " const previous = carousel.querySelector && carousel.querySelector('[data-gem-carousel-prev]');",
7365
+ " const next = carousel.querySelector && carousel.querySelector('[data-gem-carousel-next]');",
7366
+ " const slideCount = slides.length || 0;",
7367
+ " const currentIndex = () => {",
7368
+ " if (!viewport || !slideCount) return 0;",
7369
+ " const width = Number(viewport.clientWidth || viewport.offsetWidth || 1);",
7370
+ " return Math.max(0, Math.min(slideCount - 1, Math.round(Number(viewport.scrollLeft || 0) / width)));",
7371
+ " };",
7372
+ " const setIndex = (index, reason = 'api') => {",
7373
+ " const nextIndex = Math.max(0, Math.min(slideCount - 1, Number.isFinite(index) ? index : 0));",
7374
+ " carousel.dataset.gemCarouselIndex = String(nextIndex);",
7375
+ " carousel.dataset.gemCarouselCount = String(slideCount);",
7376
+ " if (viewport && viewport.scrollTo) viewport.scrollTo({ left: Number(viewport.clientWidth || viewport.offsetWidth || 0) * nextIndex, behavior: 'smooth' });",
7377
+ " api.emit('gem:carousel:change', { source: 'gem-runtime', id: carousel.id || undefined, index: nextIndex, count: slideCount, reason });",
7378
+ " };",
7379
+ " slides.forEach((slide, index) => { if (slide.dataset) slide.dataset.gemCarouselIndex = String(index); });",
7380
+ " carousel.dataset.gemCarouselState = slideCount > 1 ? 'ready' : 'single';",
7381
+ " carousel.dataset.gemCarouselCount = String(slideCount);",
7382
+ " carousel.dataset.gemCarouselIndex = String(currentIndex());",
7383
+ " if (canListen(previous)) previous.addEventListener('click', (event) => { event.preventDefault && event.preventDefault(); setIndex(currentIndex() - 1, 'previous'); });",
7384
+ " if (canListen(next)) next.addEventListener('click', (event) => { event.preventDefault && event.preventDefault(); setIndex(currentIndex() + 1, 'next'); });",
7385
+ " if (canListen(viewport)) viewport.addEventListener('scroll', () => { carousel.dataset.gemCarouselIndex = String(currentIndex()); }, { passive: true });",
7386
+ " bound += 1;",
7387
+ " });",
7388
+ " api.emit('gem:carousel:ready', { source: 'gem-runtime', count: bound });",
7389
+ " });"
7390
+ ];
7391
+ var heroSlideshowPluginRegistrationLines = [
7392
+ " registerPlugin('hero-slideshow', ({ api }) => {",
7393
+ " const roots = document.querySelectorAll('[data-gem-hero-slideshow]');",
7394
+ " let bound = 0;",
7395
+ " roots.forEach((root) => {",
7396
+ " if (!mark(root, 'hero-slideshow')) return;",
7397
+ " const slides = Array.prototype.slice.call(root.querySelectorAll('[data-gem-hero-slide]'));",
7398
+ " if (slides.length < 2) return;",
7399
+ " const dots = Array.prototype.slice.call(root.querySelectorAll('[data-gem-hero-dot]'));",
7400
+ " const previous = root.querySelector && root.querySelector('[data-gem-hero-prev]');",
7401
+ " const next = root.querySelector && root.querySelector('[data-gem-hero-next]');",
7402
+ " const pause = root.querySelector && root.querySelector('[data-gem-hero-pause]');",
7403
+ " const reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;",
7404
+ " const autoplay = !!(root.dataset && root.dataset.autoplay === 'true') && !reduce;",
7405
+ " const interval = Math.max(Number((root.dataset && root.dataset.autoplayInterval) || 6000), 4000);",
7406
+ " let active = 0;",
7407
+ " for (let i = 0; i < slides.length; i += 1) { if ((slides[i].className || '').indexOf('gem-hero-slideshow__slide--active') > -1) { active = i; break; } }",
7408
+ " let timer = null; let paused = false;",
7409
+ " const show = (index) => {",
7410
+ " active = (index + slides.length) % slides.length;",
7411
+ " slides.forEach((s, i) => { const on = i === active; if (s.classList) s.classList.toggle('gem-hero-slideshow__slide--active', on); s.hidden = !on; });",
7412
+ // `tabindex` đi cùng `aria-selected` vì các chấm này khai `role=\"tab\"`: một tablist chỉ được
7413
+ // chiếm MỘT điểm dừng Tab, và mũi tên mới là thứ chạy giữa các tab (xử lý ngay dưới).
7414
+ " dots.forEach((d, i) => { const on = i === active; if (d.classList) d.classList.toggle('gem-hero-slideshow__dot--active', on); if (d.setAttribute) { d.setAttribute('aria-selected', String(on)); d.setAttribute('tabindex', on ? '0' : '-1'); } });",
7415
+ " api.emit('gem:hero-slideshow:change', { source: 'gem-runtime', id: root.id || undefined, index: active, count: slides.length });",
7416
+ " };",
7417
+ " const stop = () => { if (timer) { window.clearInterval(timer); timer = null; } };",
7418
+ " const start = () => { stop(); if (autoplay && !paused) timer = window.setInterval(() => { show(active + 1); }, interval); };",
7419
+ " if (canListen(previous)) previous.addEventListener('click', (e) => { if (e.preventDefault) e.preventDefault(); show(active - 1); start(); });",
7420
+ " if (canListen(next)) next.addEventListener('click', (e) => { if (e.preventDefault) e.preventDefault(); show(active + 1); start(); });",
7421
+ " dots.forEach((d, i) => { if (canListen(d)) d.addEventListener('click', () => { show(Number((d.dataset && d.dataset.slideIndex) || i)); start(); }); });",
7422
+ " const focusDot = (i) => { const n = dots.length; if (!n) return; const k = ((i % n) + n) % n; show(k); start(); if (dots[k].focus) dots[k].focus(); };",
7423
+ " dots.forEach((d, i) => { if (canListen(d)) d.addEventListener('keydown', (e) => {",
7424
+ " const k = e.key;",
7425
+ " if (k === 'ArrowRight' || k === 'ArrowDown') { if (e.preventDefault) e.preventDefault(); focusDot(i + 1); }",
7426
+ " else if (k === 'ArrowLeft' || k === 'ArrowUp') { if (e.preventDefault) e.preventDefault(); focusDot(i - 1); }",
7427
+ " else if (k === 'Home') { if (e.preventDefault) e.preventDefault(); focusDot(0); }",
7428
+ " else if (k === 'End') { if (e.preventDefault) e.preventDefault(); focusDot(dots.length - 1); }",
7429
+ " }); });",
7430
+ " // The visible label follows the state like the section's own behavior did: the button's own text while playing",
7431
+ " // (kept, so a translated label survives), data-gem-hero-play-label (or 'Play') while paused. Only a text-only button.",
7432
+ " const pauseText = pause && pause.childElementCount === 0 ? pause.textContent : null;",
7433
+ " const playText = (pause && pause.getAttribute && pause.getAttribute('data-gem-hero-play-label')) || 'Play';",
7434
+ " if (canListen(pause)) pause.addEventListener('click', () => { paused = !paused; if (pause.setAttribute) pause.setAttribute('aria-pressed', String(paused)); if (pauseText !== null) pause.textContent = paused ? playText : pauseText; start(); });",
7435
+ " show(active); start(); bound += 1;",
7436
+ " });",
7437
+ " api.emit('gem:hero-slideshow:ready', { source: 'gem-runtime', count: bound });",
7438
+ " });"
7439
+ ];
7440
+ var legacyQuickViewLines = [
7441
+ ` const triggers = document.querySelectorAll('[data-gem-quick-view], [data-gem-action="quick-view"]');`,
7442
+ ` const surface = document.querySelector('[data-gem-quick-view-surface], .gem-quick-view, .gem-modal[data-gem-component="quick-view"]');`,
7443
+ " let bound = 0;",
7444
+ " const close = (reason = 'close-control') => {",
7445
+ " if (surface) {",
7446
+ " surface.dataset.gemQuickViewState = 'closed';",
7447
+ " surface.removeAttribute && surface.removeAttribute('open');",
7448
+ " surface.setAttribute && surface.setAttribute('aria-hidden', 'true');",
7449
+ " }",
7450
+ " api.emit('gem:quick-view:close', { source: 'gem-runtime', reason });",
7451
+ " };",
7452
+ " const openSurface = (detail) => {",
7453
+ " if (surface) {",
7454
+ " surface.dataset.gemQuickViewState = 'open';",
7455
+ " surface.dataset.gemProductHandle = detail.productHandle || '';",
7456
+ " surface.dataset.gemProductUrl = detail.productUrl || '';",
7457
+ " surface.setAttribute && surface.setAttribute('open', '');",
7458
+ " surface.setAttribute && surface.setAttribute('aria-hidden', 'false');",
7459
+ " surface.setAttribute && surface.setAttribute('role', 'dialog');",
7460
+ " surface.setAttribute && surface.setAttribute('aria-modal', 'true');",
7461
+ " const content = surface.querySelector && surface.querySelector('[data-gem-quick-view-content]');",
7462
+ " if (content && content.textContent !== undefined) content.textContent = 'Quick view for ' + (detail.productHandle || 'product');",
7463
+ " else if (surface.textContent !== undefined) surface.textContent = 'Quick view for ' + (detail.productHandle || 'product');",
7464
+ " const closeButton = surface.querySelector && surface.querySelector('[data-gem-quick-view-close]');",
7465
+ " if (closeButton && closeButton.focus) closeButton.focus();",
7466
+ " }",
7467
+ " api.emit('gem:quick-view:open', { source: 'gem-runtime', ...detail, hasSurface: Boolean(surface) });",
7468
+ " };",
7469
+ " triggers.forEach((trigger) => {",
7470
+ " if (!mark(trigger, 'quick-view')) return;",
7471
+ " if (!canListen(trigger)) return;",
7472
+ " trigger.addEventListener('click', (event) => {",
7473
+ " const productUrl = trigger.getAttribute && (trigger.getAttribute('data-gem-product-url') || trigger.getAttribute('href'));",
7474
+ " const productHandle = trigger.getAttribute && trigger.getAttribute('data-gem-product-handle');",
7475
+ " if (!productUrl && !productHandle) return;",
7476
+ " event.preventDefault && event.preventDefault();",
7477
+ " openSurface({ productUrl, productHandle });",
7478
+ " });",
7479
+ " bound += 1;",
7480
+ " });",
7481
+ " if (surface && mark(surface, 'quick-view-surface')) {",
7482
+ " surface.dataset.gemQuickViewState = 'closed';",
7483
+ " surface.setAttribute && surface.setAttribute('aria-hidden', 'true');",
7484
+ " if (canListen(surface)) surface.addEventListener('click', (event) => {",
7485
+ " const target = event.target;",
7486
+ " if (target && target.closest && target.closest('[data-gem-quick-view-close]')) close('close-control');",
7487
+ " });",
7488
+ " if (canListen(surface)) surface.addEventListener('keydown', (event) => { if (event.key === 'Escape') close('escape'); });",
7489
+ " }",
7490
+ // A mount on one late trigger (gem-runtime.js binds a section added after boot on its own) finds no surface: it keeps the
7491
+ // page's api.quickView.
7492
+ " if (surface || !api.quickView) api.quickView = { open: (detail = {}) => openSurface({ ...detail }), close };"
7493
+ ];
7494
+ var runtimeChunks = {
7495
+ ...gemMotionChunks({
7496
+ hook: '[data-gem-quick-view], [data-gem-action="quick-view"]',
7497
+ lines: legacyQuickViewLines,
7498
+ readyDetail: "count: bound + count, hasSurface: Boolean(surface)"
7499
+ }),
7500
+ ...gemPodChunks()
7501
+ };
7502
+ var promoPopupPluginRegistrationLines = [
7503
+ " registerPlugin('promo-popup', ({ api }) => {",
7504
+ // Only the explicit hook opts in. Every promo-popup section carries the archetype class .gem-promo-popup, and
7505
+ // binding by class took over variants that run their own behavior.js (GemFactory gap #16, 03/10/2026).
7506
+ // A section that has the hook AND its own behavior.js (promo-popup.email-capture.v1) renders the plugin's bound
7507
+ // mark, data-gem-bound-promo-popup, so mark() below skips it (gem-framework 0.8.3).
7508
+ " const popups = document.querySelectorAll('[data-gem-promo-popup]');",
7509
+ " let bound = 0;",
7510
+ " const shown = [];",
7511
+ " const storagePrefix = 'gem:promo-popup-dismissed:';",
7512
+ " const now = () => Date.now ? Date.now() : new Date().getTime();",
7513
+ // ONE storage key, two meanings in the wild (gem-framework 0.8.0). gemsite's port stores WHEN the shopper
7514
+ // dismissed; runtimes up to 0.7 stored when the dismissal ENDS. Read both: a value in the future can only be an
7515
+ // end, so it holds until then (and is rewritten once as a dismissal time); a value in the past is a dismissal time and
7516
+ // the popup's own window applies. Written from now on: the dismissal time (gemsite's meaning), which also lets a
7517
+ // seller who shortens the window shorten it for past dismissals. An end written by 0.7 that has already passed
7518
+ // reads as a dismissal time: that popup stays away one more window at most, never less than the shopper asked.
7519
+ " const readDismissal = (key) => {",
7520
+ " try { const value = Number(target.localStorage && target.localStorage.getItem(storagePrefix + key) || '0'); return Number.isFinite(value) && value > 0 ? value : 0; }",
7521
+ " catch { return 0; }",
7522
+ " };",
7523
+ " const writeDismissedAt = (key, timestamp) => {",
7524
+ " try { if (target.localStorage) target.localStorage.setItem(storagePrefix + key, String(timestamp)); }",
7525
+ " catch { /* storage can be unavailable in private contexts */ }",
7526
+ " };",
7527
+ " const isDismissed = (key, windowMs) => {",
7528
+ " const stored = readDismissal(key);",
7529
+ " if (!stored) return false;",
7530
+ " const at = now();",
7531
+ // Converted ONCE: only when the end minus this window is already past (it is then the dismissal time). An end
7532
+ // further out than one window (the 0.7 window was longer) is left as it is and holds until it passes.
7533
+ " if (stored > at) { if (stored - windowMs <= at) writeDismissedAt(key, stored - windowMs); return true; }",
7534
+ " return at - stored < windowMs;",
7535
+ " };",
7536
+ " popups.forEach((popup, index) => {",
7537
+ " if (!mark(popup, 'promo-popup')) return;",
7538
+ " const key = popup.id || popup.getAttribute && popup.getAttribute('data-gem-section-library-id') || String(index);",
7539
+ " const delaySeconds = Number(popup.getAttribute && (popup.getAttribute('data-gem-promo-delay') || '0'));",
7540
+ " const frequencyDays = Number(popup.getAttribute && (popup.getAttribute('data-gem-promo-frequency-days') || '7'));",
7541
+ " const windowMs = (Number.isFinite(frequencyDays) ? Math.max(1, frequencyDays) : 7) * 86400000;",
7542
+ " const dialog = popup.querySelector && popup.querySelector('[data-gem-promo-popup-dialog]');",
7543
+ " const overlay = popup.querySelector && popup.querySelector('[data-gem-promo-popup-overlay]');",
7544
+ " const show = (reason) => {",
7545
+ " popup.hidden = false;",
7546
+ " if (dialog) dialog.hidden = false;",
7547
+ " if (overlay) overlay.hidden = false;",
7548
+ " popup.dataset.gemPromoPopupState = 'open';",
7549
+ " api.emit('gem:promo-popup:open', { source: 'gem-runtime', id: popup.id || undefined, reason });",
7550
+ " };",
7551
+ " const open = (reason = 'timer') => {",
7552
+ " if (isDismissed(key, windowMs)) { popup.dataset.gemPromoPopupState = 'dismissed'; return false; }",
7553
+ " show(reason);",
7554
+ " return true;",
7555
+ " };",
7556
+ " const close = (reason = 'dismiss') => {",
7557
+ " writeDismissedAt(key, now());",
7558
+ " if (dialog) dialog.hidden = true;",
7559
+ " if (overlay) overlay.hidden = true;",
7560
+ " popup.dataset.gemPromoPopupState = 'closed';",
7561
+ " api.emit('gem:promo-popup:close', { source: 'gem-runtime', id: popup.id || undefined, reason });",
7562
+ " };",
7563
+ " popup.querySelectorAll && popup.querySelectorAll('[data-gem-promo-popup-dismiss]').forEach((button) => {",
7564
+ " if (!canListen(button)) return;",
7565
+ " button.addEventListener('click', (event) => { event.preventDefault && event.preventDefault(); close('dismiss'); });",
7566
+ " });",
7567
+ " if (canListen(overlay)) overlay.addEventListener('click', () => close('overlay'));",
7568
+ " popup.dataset.gemPromoPopupState = 'ready';",
7569
+ " const delay = Math.max(0, Number.isFinite(delaySeconds) ? delaySeconds : 0) * 1000;",
7570
+ // WHEN it opens, gemsite's contract (components/behaviors/promoPopup.js): data-gem-promo-trigger = "delay"
7571
+ // (default, and any unknown value) | "exit_intent" (the pointer leaves through the top 10px of the window; a
7572
+ // touch screen has no pointer to leave with, so it falls back to the delay) | "scroll" (the shopper has scrolled
7573
+ // data-gem-promo-scroll-percent of the page, 10 to 90, default 50; a page that still cannot scroll at the delay
7574
+ // opens then). It opens once per page view, whichever fires first. data-gem-promo-armed says which one is live.
7575
+ // The delay keeps the runtime's default of 0 seconds (gemsite's port defaults to 6).
7576
+ " const trigger = String(popup.getAttribute && popup.getAttribute('data-gem-promo-trigger') || 'delay').trim();",
7577
+ " const rawPercent = String(popup.getAttribute && popup.getAttribute('data-gem-promo-scroll-percent') || '').trim();",
7578
+ " const parsedPercent = rawPercent ? Number(rawPercent) : NaN;",
7579
+ " const scrollPercent = Number.isFinite(parsedPercent) ? Math.min(90, Math.max(10, parsedPercent)) : 50;",
7580
+ " const canHover = () => { try { return Boolean(target.matchMedia && target.matchMedia('(hover: hover) and (pointer: fine)').matches); } catch { return false; } };",
7581
+ " const armed = trigger === 'exit_intent' && canHover() ? 'exit_intent' : trigger === 'scroll' ? 'scroll' : 'delay';",
7582
+ " if (popup.setAttribute) popup.setAttribute('data-gem-promo-armed', armed);",
7583
+ " let opened = false;",
7584
+ " const disarm = [];",
7585
+ " const openOnce = (reason) => { if (opened) return; opened = true; disarm.splice(0).forEach((off) => off()); open(reason); };",
7586
+ // An explicit open (api.promoPopup.open) counts as THE open of this page view: it disarms the pending trigger, so the
7587
+ // delay or exit intent does not show the popup a second time (code review of #675), and it does nothing on a popup
7588
+ // that is already open.
7589
+ " shown.push((reason) => { opened = true; disarm.splice(0).forEach((off) => off()); if (popup.dataset.gemPromoPopupState !== 'open') show(reason); });",
7590
+ " if (armed === 'exit_intent') {",
7591
+ " const onMouseOut = (event) => { if (event.relatedTarget || event.clientY >= 10) return; openOnce('exit_intent'); };",
7592
+ " if (canListen(document)) {",
7593
+ " document.addEventListener('mouseout', onMouseOut);",
7594
+ " disarm.push(() => document.removeEventListener && document.removeEventListener('mouseout', onMouseOut));",
7595
+ " }",
7596
+ " } else if (armed === 'scroll') {",
7597
+ " const room = () => Number(document.documentElement && document.documentElement.scrollHeight || 0) - Number(target.innerHeight || 0);",
7598
+ " const check = () => { const r = room(); if (r > 0 && (Number(target.scrollY || target.pageYOffset || 0) / r) * 100 >= scrollPercent) openOnce('scroll'); };",
7599
+ " let ticking = false;",
7600
+ " const onScroll = () => {",
7601
+ " if (ticking) return;",
7602
+ " ticking = true;",
7603
+ " const frame = target.requestAnimationFrame ? (fn) => target.requestAnimationFrame(fn) : (fn) => target.setTimeout && target.setTimeout(fn, 16);",
7604
+ " frame(() => { ticking = false; check(); });",
7605
+ " };",
7606
+ " if (canListen(target)) {",
7607
+ " target.addEventListener('scroll', onScroll, { passive: true });",
7608
+ " target.addEventListener('resize', check);",
7609
+ " target.addEventListener('load', check);",
7610
+ " disarm.push(() => { if (!target.removeEventListener) return; target.removeEventListener('scroll', onScroll); target.removeEventListener('resize', check); target.removeEventListener('load', check); });",
7611
+ " }",
7612
+ " if (target.setTimeout) {",
7613
+ " const fallback = target.setTimeout(() => { if (room() <= 0) openOnce('timer'); }, delay);",
7614
+ " disarm.push(() => target.clearTimeout && target.clearTimeout(fallback));",
7615
+ " }",
7616
+ " check();",
7617
+ " } else {",
7618
+ " target.setTimeout ? target.setTimeout(() => openOnce('timer'), delay) : openOnce('timer');",
7619
+ " }",
7620
+ " bound += 1;",
7621
+ " });",
7622
+ // The popups THIS plugin bound, never every [data-gem-promo-popup]: one a section's own behavior.js drives carries the
7623
+ // bound mark already (email-capture renders data-gem-bound-promo-popup="section") and is not the plugin's to open. An
7624
+ // explicit open (a teaser, a test) shows the dialog itself, not only the root, and ignores the dismissal window,
7625
+ // which is about opening on its own. Before 0.8.3 it unhid the root only, so the dialog stayed hidden.
7626
+ " api.promoPopup = { open: () => shown.forEach((show) => show('api')) };",
7627
+ " api.emit('gem:promo-popup:ready', { source: 'gem-runtime', count: bound });",
7628
+ " });"
7629
+ ];
7630
+ var stickyAtcPluginRegistrationLines = [
7631
+ " registerPlugin('sticky-atc', ({ api }) => {",
7632
+ " const bars = document.querySelectorAll('[data-gem-sticky-atc], .gem-sticky-atc');",
7633
+ " let bound = 0;",
7634
+ " const setState = (bar, state) => {",
7635
+ " if (!bar || !bar.dataset) return;",
7636
+ " if (bar.dataset.gemStickyAtcState === state) return;",
7637
+ " bar.dataset.gemStickyAtcState = state;",
7638
+ " api.emit(state === 'visible' ? 'gem:sticky-atc:show' : 'gem:sticky-atc:hide', { source: 'gem-runtime', id: bar.id || undefined });",
7639
+ " };",
7640
+ " bars.forEach((bar) => {",
7641
+ " if (!mark(bar, 'sticky-atc')) return;",
7642
+ // Two attribute names (gem-framework 0.8.0), each with its own default. data-gem-sticky-threshold is the
7643
+ // runtime's (default 240). data-gem-sticky-atc-threshold is gemsite's (StickyAtcCompactBar.tsx): when the bar
7644
+ // carries it, gemsite's rule decides: that number, else the --gem-sticky-atc-threshold CSS variable, else 360.
7645
+ // A bar carrying both follows gemsite's attribute.
7646
+ " const cssThreshold = () => { try { return target.getComputedStyle ? Number(target.getComputedStyle(bar).getPropertyValue('--gem-sticky-atc-threshold')) : 0; } catch { return 0; } };",
7647
+ " const threshold = bar.hasAttribute && bar.hasAttribute('data-gem-sticky-atc-threshold')",
7648
+ " ? (Number(bar.getAttribute('data-gem-sticky-atc-threshold')) || cssThreshold() || 360)",
7649
+ " : Number(bar.getAttribute && (bar.getAttribute('data-gem-sticky-threshold') || '240'));",
7650
+ " const update = () => {",
7651
+ " const scrollY = Number(target.scrollY || target.pageYOffset || 0);",
7652
+ " setState(bar, scrollY >= threshold ? 'visible' : 'hidden');",
7653
+ " };",
7654
+ " update();",
7655
+ " if (canListen(target)) target.addEventListener('scroll', update, { passive: true });",
7656
+ // The price follows the variant <select> (each <option> carries its own data-gem-variant-price), as the section's
7657
+ // behavior.js did: otherwise the bar shows the first variant's price whatever the shopper picked.
7658
+ " const priceEl = bar.querySelector && bar.querySelector('[data-gem-sticky-atc-price]');",
7659
+ " const select = bar.querySelector && bar.querySelector('select');",
7660
+ " const syncPrice = () => { if (!priceEl || !select) return; const opt = select.options && select.selectedIndex > -1 ? select.options[select.selectedIndex] : null; const price = opt && opt.getAttribute && opt.getAttribute('data-gem-variant-price'); if (price) priceEl.textContent = price; };",
7661
+ " if (canListen(select)) select.addEventListener('change', syncPrice);",
7662
+ " syncPrice();",
7663
+ // A form WITHOUT an action (the headless bar) has nowhere to post: the plugin adds to cart by dispatching
7664
+ // gem:cart:add with the SELECTED variant (the button's data-gem-variant-id is only the first one), which the
7665
+ // storefront's cart bridge consumes. A form WITH an action (a Liquid /cart/add form) is left to the browser.
7666
+ " const form = bar.querySelector && bar.querySelector('form');",
7667
+ " if (canListen(form)) form.addEventListener('submit', (event) => {",
7668
+ " if (!(form.hasAttribute && form.hasAttribute('action'))) {",
7669
+ " if (event && event.preventDefault) event.preventDefault();",
7670
+ " const choice = form.querySelector && form.querySelector('select');",
7671
+ " const trigger = bar.querySelector && bar.querySelector('[data-gem-variant-id]');",
7672
+ " const variantId = (choice && choice.value) || (trigger && trigger.getAttribute('data-gem-variant-id')) || undefined;",
7673
+ " if (bar.dispatchEvent && typeof window.CustomEvent === 'function') bar.dispatchEvent(new window.CustomEvent('gem:cart:add', { bubbles: true, detail: { variantId, quantity: 1 } }));",
7674
+ " }",
7675
+ " api.emit('gem:sticky-atc:submit', { source: 'gem-runtime', id: bar.id || undefined });",
7676
+ " });",
7677
+ " bound += 1;",
7678
+ " });",
7679
+ " api.emit('gem:sticky-atc:ready', { source: 'gem-runtime', count: bound });",
7680
+ " });"
7681
+ ];
7682
+ var beforeAfterPluginRegistrationLines = [
7683
+ " registerPlugin('before_after', ({ api }) => {",
7684
+ " const frames = document.querySelectorAll('[data-gem-before-after-frame], .gem-before-after__frame');",
7685
+ " let bound = 0;",
7686
+ " const clamp = (value) => Math.max(0, Math.min(100, Number.isFinite(value) ? value : 50));",
7687
+ " frames.forEach((frame) => {",
7688
+ " if (!mark(frame, 'before_after')) return;",
7689
+ ` const range = frame.querySelector && frame.querySelector('[data-gem-before-after-range], input[type="range"]');`,
7690
+ " const setPosition = (value, reason = 'init') => {",
7691
+ " const position = clamp(Number(value));",
7692
+ " if (frame.style && frame.style.setProperty) frame.style.setProperty('--gem-before-after-position', position + '%');",
7693
+ " frame.dataset.gemBeforeAfterPosition = String(position);",
7694
+ " if (range && range.value !== undefined) range.value = String(position);",
7695
+ " api.emit('gem:before-after:change', { source: 'gem-runtime', position, reason, id: frame.id || undefined });",
7696
+ " };",
7697
+ " const initial = range && range.value !== undefined ? range.value : frame.getAttribute && frame.getAttribute('data-gem-before-after-position');",
7698
+ " setPosition(initial || 50);",
7699
+ " if (canListen(range)) range.addEventListener('input', () => setPosition(range.value, 'input'));",
7700
+ " bound += 1;",
7701
+ " });",
7702
+ " api.emit('gem:before-after:ready', { source: 'gem-runtime', count: bound });",
7703
+ " });"
7704
+ ];
5430
7705
  var COUNTDOWN_END_RUNTIME_LINES = [
5431
7706
  " const COUNTDOWN_ISO = /^(\\d{4})-(\\d{2})-(\\d{2})(?:[T ](\\d{2}):(\\d{2})(?::(\\d{2})(?:\\.\\d+)?)?(Z|[+-]\\d{2}:?\\d{2})?)?$/i;",
5432
7707
  " const countdownEnd = (value) => {",
@@ -5612,6 +7887,141 @@ var countdownPluginRegistrationLines = [
5612
7887
  " api.emit('gem:countdown:ready', { source: 'gem-runtime', count: bound });",
5613
7888
  " });"
5614
7889
  ];
7890
+ var revealOnScrollPluginRegistrationLines = [
7891
+ " registerPlugin('reveal-on-scroll', ({ api }) => {",
7892
+ " const nodes = document.querySelectorAll('[data-gem-reveal], .gem-reveal');",
7893
+ " let bound = 0;",
7894
+ " const reveal = (node) => { node.dataset.gemRevealState = 'in'; };",
7895
+ " // Reduced motion and no IntersectionObserver land on the same answer: show everything now (from gemsite revealOnScroll.js).",
7896
+ " const reduce = typeof target.matchMedia === 'function' && target.matchMedia('(prefers-reduced-motion: reduce)').matches;",
7897
+ " if (reduce || !target.IntersectionObserver) { nodes.forEach(reveal); api.emit('gem:reveal:ready', { source: 'gem-runtime', count: 0 }); return; }",
7898
+ " // Above the fold the browser has already painted the section: writing 'ready' would blink it out and back in.",
7899
+ ' // `none` and `default` are the knob\'s "apply nothing" values; the attribute is still written, so skip them here.',
7900
+ " const off = (node) => { const v = node.getAttribute ? node.getAttribute('data-gem-reveal') : null; return v === 'none' || v === 'default'; };",
7901
+ " const inViewport = (node) => { if (!node.getBoundingClientRect) return true; const r = node.getBoundingClientRect(); const h = target.innerHeight || (document.documentElement && document.documentElement.clientHeight) || 0; return r.top < h && r.bottom > 0; };",
7902
+ " const observer = new target.IntersectionObserver((entries, obs) => {",
7903
+ " entries.forEach((entry) => {",
7904
+ " if (!entry.isIntersecting) return;",
7905
+ " reveal(entry.target);",
7906
+ " obs.unobserve(entry.target);",
7907
+ " api.emit('gem:reveal:in', { source: 'gem-runtime', id: entry.target.id || undefined });",
7908
+ " });",
7909
+ " }, { threshold: 0.12, rootMargin: '0px 0px -40px 0px' });",
7910
+ " nodes.forEach((node) => {",
7911
+ " if (!mark(node, 'reveal-on-scroll')) return;",
7912
+ " if (off(node) || inViewport(node)) { reveal(node); bound += 1; return; }",
7913
+ " if (!node.dataset.gemRevealState) node.dataset.gemRevealState = 'ready';",
7914
+ " observer.observe(node);",
7915
+ " bound += 1;",
7916
+ " });",
7917
+ " api.emit('gem:reveal:ready', { source: 'gem-runtime', count: bound });",
7918
+ " });"
7919
+ ];
7920
+ var imgInPluginRegistrationLines = [
7921
+ " registerPlugin('img-in', ({ api }) => {",
7922
+ " const STATE = 'data-gem-img-in-state';",
7923
+ " const BOUND = 'data-gem-bound-img-in';",
7924
+ " const reduce = typeof target.matchMedia === 'function' && target.matchMedia('(prefers-reduced-motion: reduce)').matches;",
7925
+ " const style = (img) => (typeof target.getComputedStyle === 'function' ? target.getComputedStyle(img) : null);",
7926
+ " const owned = (s) => Boolean(s && ((s.opacity && s.opacity !== '1') || (s.filter && s.filter !== 'none') || (s.animationName && s.animationName !== 'none')));",
7927
+ " const overridden = (s) => Boolean(s && ((s.opacity && s.opacity !== '0.6') || (s.filter && s.filter.indexOf('blur') === -1)));",
7928
+ " const settle = (event) => {",
7929
+ " const img = event.target;",
7930
+ " if (!img || img.tagName !== 'IMG' || !img.hasAttribute(BOUND)) return;",
7931
+ " const state = img.getAttribute(STATE);",
7932
+ " if (event.type === 'load' || event.type === 'error') {",
7933
+ " if (state !== 'loading') return;",
7934
+ " if (event.type === 'load' && !reduce && !overridden(style(img))) img.setAttribute(STATE, 'in');",
7935
+ " else img.removeAttribute(STATE);",
7936
+ " return;",
7937
+ " }",
7938
+ " if (state === 'in' && event.animationName === 'gem-img-in') img.removeAttribute(STATE);",
7939
+ " };",
7940
+ " ['load', 'error', 'animationend', 'animationcancel'].forEach((type) => document.addEventListener(type, settle, true));",
7941
+ " const candidates = [];",
7942
+ " document.querySelectorAll('img').forEach((img) => {",
7943
+ " if (!img.closest('[data-gem-img-in]')) return;",
7944
+ " if (img.complete) { if (!img.hasAttribute(BOUND)) img.removeAttribute(STATE); return; }",
7945
+ " if (mark(img, 'img-in')) candidates.push(img);",
7946
+ " });",
7947
+ " const own = candidates.map((img) => owned(style(img)));",
7948
+ " let bound = 0;",
7949
+ " candidates.forEach((img, i) => { if (own[i]) return; img.setAttribute(STATE, 'loading'); bound += 1; });",
7950
+ " api.emit('gem:img-in:ready', { source: 'gem-runtime', count: bound });",
7951
+ " });"
7952
+ ];
7953
+ var recentlyViewedPluginRegistrationLines = [
7954
+ " registerPlugin('recently-viewed', ({ api }) => {",
7955
+ " const storageKey = 'gem:recently-viewed-products';",
7956
+ " const readItems = () => {",
7957
+ " try { return JSON.parse(target.localStorage && target.localStorage.getItem(storageKey) || '[]'); }",
7958
+ " catch { return []; }",
7959
+ " };",
7960
+ " const writeItems = (items) => {",
7961
+ " try { if (target.localStorage) target.localStorage.setItem(storageKey, JSON.stringify(items.slice(0, 12))); }",
7962
+ " catch { /* storage can be unavailable in private contexts */ }",
7963
+ " };",
7964
+ ` const escapeHtml = (value) => String(value || '').replace(/[&<>'"]/g, (char) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', "'": '&#39;', '"': '&quot;' }[char]));`,
7965
+ " const renderItem = (item) => {",
7966
+ " const url = item && item.url ? String(item.url) : '#';",
7967
+ " const label = item && (item.title || item.handle || item.url) ? String(item.title || item.handle || item.url) : 'Recently viewed product';",
7968
+ ` return '<a class="gem-card gem-recently-viewed__item" href="' + escapeHtml(url) + '"><strong>' + escapeHtml(label) + '</strong></a>';`,
7969
+ " };",
7970
+ " const current = document.querySelector('[data-gem-current-product], [data-gem-product-handle]');",
7971
+ " if (current) {",
7972
+ " const handle = current.getAttribute && current.getAttribute('data-gem-product-handle');",
7973
+ " const url = current.getAttribute && (current.getAttribute('data-gem-product-url') || current.getAttribute('href'));",
7974
+ " const title = current.getAttribute && current.getAttribute('data-gem-product-title');",
7975
+ " if (handle || url) {",
7976
+ " const nextItem = { handle, url, title, recordedAt: new Date().toISOString() };",
7977
+ " const items = [nextItem, ...readItems().filter((item) => item && item.handle !== handle && item.url !== url)];",
7978
+ " writeItems(items);",
7979
+ " api.emit('gem:recently-viewed:record', { source: 'gem-runtime', handle, url });",
7980
+ " }",
7981
+ " }",
7982
+ " const rails = document.querySelectorAll('[data-gem-recently-viewed], .gem-recently-viewed');",
7983
+ " let bound = 0;",
7984
+ " rails.forEach((rail) => {",
7985
+ " if (!mark(rail, 'recently-viewed')) return;",
7986
+ " const limit = Number(rail.getAttribute && (rail.getAttribute('data-gem-recently-viewed-limit') || '4'));",
7987
+ " const items = readItems().slice(0, Number.isFinite(limit) ? limit : 4);",
7988
+ " const list = rail.querySelector && (rail.querySelector('[data-gem-recently-viewed-list]') || rail);",
7989
+ " const empty = rail.querySelector && rail.querySelector('[data-gem-recently-viewed-empty]');",
7990
+ " rail.dataset.gemRecentlyViewedCount = String(items.length);",
7991
+ " if (items.length > 0) {",
7992
+ " rail.dataset.gemRecentlyViewedState = 'ready';",
7993
+ " if (list) {",
7994
+ " list.hidden = false;",
7995
+ " if (list.dataset) list.dataset.gemRecentlyViewedCount = String(items.length);",
7996
+ " if ('innerHTML' in list) list.innerHTML = items.map(renderItem).join('');",
7997
+ " else if ('textContent' in list) list.textContent = items.map((item) => item && (item.title || item.handle || item.url) || 'Recently viewed product').join(', ');",
7998
+ " }",
7999
+ " if (empty) empty.hidden = true;",
8000
+ " } else {",
8001
+ " rail.dataset.gemRecentlyViewedState = 'empty';",
8002
+ " if (empty) empty.hidden = false;",
8003
+ " }",
8004
+ " bound += 1;",
8005
+ " });",
8006
+ " api.recentlyViewed = { list: readItems };",
8007
+ " api.emit('gem:recently-viewed:ready', { source: 'gem-runtime', count: bound });",
8008
+ " });"
8009
+ ];
8010
+ var runtimeSlotLoaderPluginRegistrationLines = [
8011
+ " registerPlugin('runtime-slot-loader', ({ api }) => {",
8012
+ " const slots = document.querySelectorAll('[data-gem-runtime-slot], [data-gem-footer-block-builder], .gem-navigation-shell__layout');",
8013
+ " let bound = 0;",
8014
+ " slots.forEach((slot, index) => {",
8015
+ " if (!mark(slot, 'runtime-slot-loader')) return;",
8016
+ " if (slot.dataset) {",
8017
+ " slot.dataset.gemRuntimeSlotState = 'ready';",
8018
+ " if (!slot.dataset.gemRuntimeSlotIndex) slot.dataset.gemRuntimeSlotIndex = String(index);",
8019
+ " }",
8020
+ " bound += 1;",
8021
+ " });",
8022
+ " api.emit('gem:slot:ready', { source: 'gem-runtime', count: bound });",
8023
+ " });"
8024
+ ];
5615
8025
  var offerEnginePluginRegistrationLines = [
5616
8026
  " registerPlugin('offer-engine', ({ api }) => {",
5617
8027
  " " + offerEngineRuntimeScript(),
@@ -5628,6 +8038,61 @@ var bundlePluginRegistrationLines = [
5628
8038
  " " + bundleEngineRuntimeScript(),
5629
8039
  " });"
5630
8040
  ];
8041
+ var sizeGuidePluginRegistrationLines = [
8042
+ " registerPlugin('size-guide', ({ api }) => {",
8043
+ " const page = document.documentElement && typeof document.documentElement.querySelectorAll === 'function' ? document.documentElement : document;",
8044
+ " const guides = Array.prototype.slice.call(page.querySelectorAll('[data-gem-size-guide]'));",
8045
+ " const opens = (target) => guides.some((guide) => !target || guide.getAttribute('data-gem-size-guide') === target || guide.id === target);",
8046
+ " let bound = 0;",
8047
+ " document.querySelectorAll('[data-gem-size-guide-trigger]').forEach((trigger) => {",
8048
+ " const target = trigger.getAttribute('data-gem-size-guide-trigger') || '';",
8049
+ " if (!opens(target)) { trigger.hidden = true; return; }",
8050
+ " if (!mark(trigger, 'size-guide')) return;",
8051
+ " trigger.hidden = false;",
8052
+ " if (!canListen(trigger)) return;",
8053
+ " trigger.addEventListener('click', (event) => {",
8054
+ " event.preventDefault && event.preventDefault();",
8055
+ " const holder = trigger.closest ? trigger.closest('[data-product-id]') : null;",
8056
+ " const detail = { source: 'gem-runtime', sectionId: target || null, productId: holder ? holder.getAttribute('data-product-id') : null };",
8057
+ " if (typeof window.CustomEvent === 'function') window.dispatchEvent(new window.CustomEvent('gem:size-guide:open', { detail }));",
8058
+ " api.emit('gem:size-guide:open', detail);",
8059
+ " });",
8060
+ " bound += 1;",
8061
+ " });",
8062
+ " api.emit('gem:size-guide:ready', { source: 'gem-runtime', count: bound, guides: guides.length });",
8063
+ " });"
8064
+ ];
8065
+ var runtimePluginSourceLines = [
8066
+ ["cart-drawer", cartDrawerPluginRegistrationLines],
8067
+ ["mobile-menu", mobileMenuPluginRegistrationLines],
8068
+ ["quantity-input", quantityInputPluginRegistrationLines],
8069
+ ["variant-selects", variantSelectsPluginRegistrationLines],
8070
+ ["predictive-search", predictiveSearchPluginRegistrationLines],
8071
+ ["localization", localizationPluginRegistrationLines],
8072
+ ["disclosure", disclosurePluginRegistrationLines],
8073
+ ["tabs", tabsPluginRegistrationLines],
8074
+ ["media-gallery", mediaGalleryPluginRegistrationLines],
8075
+ ["carousel", carouselPluginRegistrationLines],
8076
+ ["hero-slideshow", heroSlideshowPluginRegistrationLines],
8077
+ ["quick-view", runtimeChunks["quick-view"].body],
8078
+ ["promo-popup", promoPopupPluginRegistrationLines],
8079
+ ["sticky-atc", stickyAtcPluginRegistrationLines],
8080
+ ["before_after", beforeAfterPluginRegistrationLines],
8081
+ ["countdown", countdownPluginRegistrationLines],
8082
+ ["reveal-on-scroll", revealOnScrollPluginRegistrationLines],
8083
+ ["img-in", imgInPluginRegistrationLines],
8084
+ ["recently-viewed", recentlyViewedPluginRegistrationLines],
8085
+ ["runtime-slot-loader", runtimeSlotLoaderPluginRegistrationLines],
8086
+ ["offer-engine", offerEnginePluginRegistrationLines],
8087
+ ["reviews", reviewsPluginRegistrationLines],
8088
+ ["bundle", bundlePluginRegistrationLines],
8089
+ ["size-guide", sizeGuidePluginRegistrationLines],
8090
+ ["orbit", runtimeChunks.orbit.body],
8091
+ ["rail", runtimeChunks.rail.body],
8092
+ ["personalize", runtimeChunks.personalize.body],
8093
+ ["print-preview", runtimeChunks["print-preview"].body],
8094
+ ["delivery-estimate", runtimeChunks["delivery-estimate"].body]
8095
+ ];
5631
8096
 
5632
8097
  // ../../packages/gem-framework/src/validation.ts
5633
8098
  var validationRulesByCode = new Map(gemFrameworkValidationRules.map((rule) => [rule.code, rule]));
@@ -5639,7 +8104,7 @@ var canonicalCssVariableNames = /* @__PURE__ */ new Set([
5639
8104
  const namespace = group === "typography" ? "font" : group === "spacing" ? "space" : group;
5640
8105
  return keys.map((key) => `--gem-${namespace}-${slugify(key)}`);
5641
8106
  }),
5642
- // The governed type ramp (--gem-h1…h6, --gem-body, --gem-text-sm, --gem-type-base/-scale). The
8107
+ // The governed type ramp (--gem-h1…h6, --gem-body, --gem-text-sm, --gem-text-xs, --gem-type-base/-scale). The
5643
8108
  // framework stylesheet declares it on every page, but it is not a token GROUP, so it was missing here
5644
8109
  // and a section sizing its heading with `var(--gem-h2)` got `unknown_gem_css_variable`. Read from the
5645
8110
  // function that emits it, so the ramp and this list cannot drift.
@@ -5658,7 +8123,15 @@ var canonicalCssVariableNames = /* @__PURE__ */ new Set([
5658
8123
  // The row primitive's variables (primitives.ts GEM_ROW_CSS). Sections SET these on their grid.
5659
8124
  ...GEM_ROW_CSS_VARIABLES,
5660
8125
  // The form set's variables (form-set.ts): set from a plan's `tokens.form`, read by fields under [data-gem-form-set].
5661
- ...GEM_FORM_CSS_VARIABLES
8126
+ ...GEM_FORM_CSS_VARIABLES,
8127
+ // The motion engine's (motion-engine.ts, 0.19.0), all of them: a section copies the base CSS VERBATIM into its own CSS,
8128
+ // internals (--gem-orbit-a, --gem-rail-t) included, and sets --gem-orbit-i / -n, the settings and --gem-motion-tint.
8129
+ ...new Set([GEM_ORBIT_BASE_CSS, GEM_RAIL_BASE_CSS, GEM_ORBIT_RUNTIME_CSS, GEM_RAIL_RUNTIME_CSS, GEM_QUICK_VIEW_CSS].join("\n").match(/--gem-[a-z0-9-]+/g) ?? []),
8130
+ // …and every setting an effect reads from a CSS variable (`css: true`): a section sets it in its stylesheet, per breakpoint.
8131
+ ...gemMotionEffectList.flatMap((effect) => Object.entries(effect.settings).filter(([, setting]) => setting.css).map(([name]) => `--gem-${effect.key}-${name}`)),
8132
+ // The POD buy box's (pod-engine.ts, 0.20.0): the print area and lines gemsite sets on a preview, the swatch colour of an
8133
+ // option, the chunks' sentinels, and --gem-font-accent, the accent font a skin may set (the preview falls back to heading).
8134
+ ...GEM_POD_CSS_VARIABLES
5662
8135
  ]);
5663
8136
  var TOKENIZED_PROPERTIES = [
5664
8137
  "background",
@@ -5766,14 +8239,22 @@ function validateGemFrameworkMarkup(markup, artifactPath, options = {}) {
5766
8239
  }
5767
8240
  return result(issues);
5768
8241
  }
8242
+ var NAMESPACE_URL = String.raw`https?:\/\/www\.w3\.org\/[\w.\/#:~%+-]*`;
8243
+ var SHARE_NETWORK = "facebook|twitter|x|pinterest|whatsapp|threads";
8244
+ var AS_A_LINK = String.raw`(?<=(?:<a\s[^<>]{0,400}?(?<![\w-])href=\{?|[{,]\s{0,40}["']?(?:${SHARE_NETWORK})["']?\s{0,8}:\s{0,8})["'\x60])`;
8245
+ var SHARE_INTENT = String.raw`https?:\/\/(?:(?:www\.)?facebook\.com\/sharer\/sharer\.php|(?:twitter|x)\.com\/intent\/(?:tweet|post)|(?:www\.)?pinterest\.com\/pin\/create\/button\/|wa\.me\/\d*|(?:www\.)?threads\.(?:net|com)\/intent\/post)\?`;
8246
+ var NEVER_FETCHED_URL = new RegExp(`${NAMESPACE_URL}|${AS_A_LINK}${SHARE_INTENT}`, "gi");
8247
+ var JSON_LD = /<script\b[^>]*type=["']application\/ld\+json["'][^>]*>[\s\S]*?<\/script>/gi;
8248
+ function namesExternalDependency(withoutJsonLd) {
8249
+ return /https?:\/\/|\/\/cdn\./i.test(withoutJsonLd.replace(NEVER_FETCHED_URL, ""));
8250
+ }
5769
8251
  function validateSharedSafety(content, artifactPath) {
5770
8252
  const issues = [];
5771
- const contentWithoutJsonLd = content.replace(/<script\b[^>]*type=["']application\/ld\+json["'][^>]*>[\s\S]*?<\/script>/gi, "");
5772
- const contentWithoutNamespaces = contentWithoutJsonLd.replace(/https?:\/\/www\.w3\.org\/[^\s"'<>]*/gi, "");
5773
- if (/<script\b/i.test(contentWithoutJsonLd)) {
8253
+ const withoutJsonLd = content.replace(JSON_LD, "");
8254
+ if (/<script\b/i.test(withoutJsonLd)) {
5774
8255
  issues.push(ruleIssue("inline_script", "Gem Framework artifacts must not include inline script tags.", artifactPath));
5775
8256
  }
5776
- if (/https?:\/\/|\/\/cdn\./i.test(contentWithoutNamespaces)) {
8257
+ if (namesExternalDependency(withoutJsonLd)) {
5777
8258
  issues.push(ruleIssue("external_dependency", "Gem Framework artifacts must not include external runtime or CDN dependencies.", artifactPath));
5778
8259
  }
5779
8260
  return issues;
@@ -5802,6 +8283,9 @@ function gemFrameworkPublicCssVariables() {
5802
8283
  String(createSectionCss()),
5803
8284
  String(createShellCss()),
5804
8285
  String(createGemSectionRuntimeContractCss()),
8286
+ // popup-shell.css (its own file since 0.8.3, so not in createPrimitiveCss): a popup section sets
8287
+ // --gem-popup-drawer-width (0.18.17, drawer-end) and must not be told the framework's own variable is unknown.
8288
+ GEM_POPUP_SHELL_CSS,
5805
8289
  // The button case rule only exists when a token sets it; a section may still read its variables (a CTA rule
5806
8290
  // beats the zero-specificity rule, so opting in means `text-transform: var(--gem-font-button-transform, none)`).
5807
8291
  gemButtonCaseCss({ typography: { buttonTransform: "none", buttonLetterSpacing: "normal" } })
@@ -5812,11 +8296,13 @@ function gemFrameworkPublicCssVariables() {
5812
8296
  }
5813
8297
  var ROOT_SELECTOR = /^\s*(?::root|html|:host)(?:\[[^\]]*\]|:[a-z-]+(?:\([^()]*\))?)*\s*$/;
5814
8298
  var EDITION_ROOT_SELECTOR = /^\s*:where\(\s*\[data-gem-edition=(["'])\d+\1\](?:\s*,\s*\[data-gem-edition=(["'])\d+\2\])*\s*\)\s*$/;
8299
+ var KNOB_SCOPE_SELECTOR = /^\s*\[data-gem-[a-z0-9-]+(?:=(["'])[^"']*\1)?\]\s*$/;
5815
8300
  function gemPublicCssVariablesFrom(stylesheet) {
5816
8301
  const css = stylesheet.replace(/\/\*[\s\S]*?\*\//g, " ");
5817
8302
  const declaredOnRoot = /* @__PURE__ */ new Set();
5818
8303
  const declaredElsewhere = /* @__PURE__ */ new Set();
5819
8304
  for (const [, selector, body] of css.matchAll(/([^{}]+)\{([^{}]*)\}/g)) {
8305
+ if (selector.split(",").every((part) => KNOB_SCOPE_SELECTOR.test(part))) continue;
5820
8306
  const onRoot = EDITION_ROOT_SELECTOR.test(selector) || selector.split(",").every((part) => ROOT_SELECTOR.test(part));
5821
8307
  for (const [, name] of body.matchAll(/(--gem-[a-z0-9-]+)\s*:/g)) (onRoot ? declaredOnRoot : declaredElsewhere).add(name);
5822
8308
  }
@@ -6137,9 +8623,9 @@ var LABEL_LEAD = /^(?:(?:grand\s+|sub\s*)?total|subtotal|price|unless\s+noted|t
6137
8623
  var QUESTION_END = /[??❓❔][\s\p{Extended_Pictographic}\uFE0F]*$/u;
6138
8624
  var MENTIONS_SHIPPING = /ship|deliver|vận chuyển|giao hàng/iu;
6139
8625
  var SHIPPING_ZERO_ENTRIES = Object.entries(SHIPPING_ZERO_SENTENCES);
6140
- function sentencesOf(text) {
8626
+ function sentencesOf(text2) {
6141
8627
  const out = [];
6142
- for (const line of text.split(/\n+|\s[|•·]\s|\s(?=[✓✔☑]\s)/u)) {
8628
+ for (const line of text2.split(/\n+|\s[|•·]\s|\s(?=[✓✔☑]\s)/u)) {
6143
8629
  let cur = "";
6144
8630
  for (const piece of line.split(/(?<=[.!?;])\s+/u)) {
6145
8631
  cur = cur ? `${cur} ${piece}` : piece;
@@ -6154,8 +8640,8 @@ function sentencesOf(text) {
6154
8640
  }
6155
8641
  function shippingZeroPromiseIn(raw) {
6156
8642
  if (!MENTIONS_SHIPPING.test(raw)) return null;
6157
- const text = raw.normalize("NFC").replace(/\\n/g, "\n").replace(/\\(['"’])/g, "$1");
6158
- for (const sentence of sentencesOf(text)) {
8643
+ const text2 = raw.normalize("NFC").replace(/\\n/g, "\n").replace(/\\(['"’])/g, "$1");
8644
+ for (const sentence of sentencesOf(text2)) {
6159
8645
  if (QUESTION_END.test(sentence)) continue;
6160
8646
  const whole = sentence.replace(DECOR_START, "").replace(DECOR_END, "").trim();
6161
8647
  const clauses = LABEL_LEAD.test(whole) ? [whole] : [whole, ...whole.split(/,\s+|\s+[—–]\s+/u)].map((c) => c.replace(DECOR_START, "").replace(DECOR_END, "").trim());
@@ -6209,26 +8695,26 @@ function demoValues(src) {
6209
8695
  return out;
6210
8696
  }
6211
8697
  var PRICE_ANY = new RegExp(`[$\xA3\u20AC]\\s?\\d|\\b\\d[\\d.,]*\\s?(?:USD|EUR|GBP)\\b|${PROMISE_VI_PARTS.price}`, "iu");
6212
- function isQuestion(text) {
6213
- const t = text.trim();
8698
+ function isQuestion(text2) {
8699
+ const t = text2.trim();
6214
8700
  if (!/\?$/.test(t) || /[.!?]\s+\S/.test(t)) return false;
6215
8701
  return /^(?:can|do|does|did|how|what|when|where|why|who|which|is|are|will|would|should|could|may)\b/i.test(t) || /(?:không|chưa|nào|sao|gì|đâu|bao lâu|bao nhiêu|mấy|được không|à|hả|nhỉ)\s*\?$/iu.test(t) || /^(?:làm sao|tại sao|vì sao|khi nào|bao lâu|có)(?![\p{L}])/iu.test(t);
6216
8702
  }
6217
8703
  function claimIn(raw) {
6218
- const text = raw.normalize("NFC");
6219
- if (CLAIM_TEXT.test(text) || CLAIM_TEXT_VI.test(text)) return "hardcoded claim";
6220
- if (INVENTED_PERSON.test(`"${text}"`) || INVENTED_PERSON_VI.test(text)) return "invented person";
6221
- if (isQuestion(text)) return PRICE_ANY.test(text) || PRICE_K_VI.test(text) ? "promise the merchant never made" : null;
6222
- if (PRICE_K_VI.test(text) && /^\s*\d[\d.,]*\s?k\s*\+?\s*$/.test(text)) return "hardcoded figure";
6223
- if (PROMISE.test(text) || PROMISE_VI.test(text) || shippingZeroPromiseIn(text) || PRICE_K_VI.test(text)) return "promise the merchant never made";
8704
+ const text2 = raw.normalize("NFC");
8705
+ if (CLAIM_TEXT.test(text2) || CLAIM_TEXT_VI.test(text2)) return "hardcoded claim";
8706
+ if (INVENTED_PERSON.test(`"${text2}"`) || INVENTED_PERSON_VI.test(text2)) return "invented person";
8707
+ if (isQuestion(text2)) return PRICE_ANY.test(text2) || PRICE_K_VI.test(text2) ? "promise the merchant never made" : null;
8708
+ if (PRICE_K_VI.test(text2) && /^\s*\d[\d.,]*\s?k\s*\+?\s*$/.test(text2)) return "hardcoded figure";
8709
+ if (PROMISE.test(text2) || PROMISE_VI.test(text2) || shippingZeroPromiseIn(text2) || PRICE_K_VI.test(text2)) return "promise the merchant never made";
6224
8710
  return null;
6225
8711
  }
6226
8712
  function demoInvents(value) {
6227
8713
  const v = value.trim();
6228
8714
  if (/^-?\d/.test(v)) return "number";
6229
8715
  if (/^[{[]/.test(v) && /(?:^|[:[,])\s*-?\d/.test(v.slice(1))) return "number";
6230
- for (const text of stringLiteralsIn(v)) {
6231
- const kind = claimIn(text);
8716
+ for (const text2 of stringLiteralsIn(v)) {
8717
+ const kind = claimIn(text2);
6232
8718
  if (kind) return kind;
6233
8719
  }
6234
8720
  return null;
@@ -6281,12 +8767,12 @@ function fabricatedProofInSource(raw) {
6281
8767
  caught.add(`${at}\0key\0${key}`);
6282
8768
  out.push({ line: at, why: `plan literal ${key}: ${num}`, text: line.trim() });
6283
8769
  }
6284
- for (const text of stringLiteralsIn(line)) {
6285
- if (text.includes("${")) continue;
6286
- const kind = claimIn(text);
8770
+ for (const text2 of stringLiteralsIn(line)) {
8771
+ if (text2.includes("${")) continue;
8772
+ const kind = claimIn(text2);
6287
8773
  if (!kind) continue;
6288
- caught.add(`${at}\0str\0${text}`);
6289
- out.push({ line: at, why: `${kind} ${JSON.stringify(text)}`, text: line.trim() });
8774
+ caught.add(`${at}\0str\0${text2}`);
8775
+ out.push({ line: at, why: `${kind} ${JSON.stringify(text2)}`, text: line.trim() });
6290
8776
  }
6291
8777
  });
6292
8778
  const code = src.replace(/(["'])((?:\\.|(?!\1)[^\\\n])*)\1/g, (_m, q, body) => q + body.replace(/[^\n]/g, " ") + q);
@@ -6295,7 +8781,7 @@ function fabricatedProofInSource(raw) {
6295
8781
  out.push({ line: at, why: `verified by default (${m[0].replace(/\s+/g, " ").trim()}); badge only on verified === true`, text: (lines[at - 1] ?? "").trim() });
6296
8782
  }
6297
8783
  for (const r of demoReads) {
6298
- const onDemoLine = caught.has(`${r.entry.line}\0key\0${r.key}`) || stringLiteralsIn(r.entry.value).some((text) => caught.has(`${r.entry.line}\0str\0${text}`));
8784
+ const onDemoLine = caught.has(`${r.entry.line}\0key\0${r.key}`) || stringLiteralsIn(r.entry.value).some((text2) => caught.has(`${r.entry.line}\0str\0${text2}`));
6299
8785
  if (!onDemoLine) out.push({ line: r.at, why: r.why, text: r.text });
6300
8786
  }
6301
8787
  return out.sort((a, b) => a.line - b.line);