@gemboss/gem-framework 0.16.1 → 0.18.13

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.
package/dist/index.js CHANGED
@@ -19,6 +19,137 @@ function gemPartClass(part) {
19
19
  return GEM_PART_CLASS[part] ?? null;
20
20
  }
21
21
 
22
+ // ../../packages/gem-framework/src/section-dividers.ts
23
+ var spikes = (() => {
24
+ const teeth = [];
25
+ for (let x = 95; x > 0; x -= 5) teeth.push(`L${x} ${teeth.length % 2 === 0 ? 10 : 3}`);
26
+ return `M0 0 H100 V3 ${teeth.join(" ")} L0 ${teeth.length % 2 === 0 ? 10 : 3} Z`;
27
+ })();
28
+ var GEM_SECTION_DIVIDER_PATHS = Object.freeze({
29
+ wave: "M0 0 H100 V3 C80 9 70 -1 50 4 C30 9 20 1 0 4 Z",
30
+ slope: "M0 0 H100 V10 Z",
31
+ spikes,
32
+ arch: "M0 0 H100 V2 Q50 12 0 2 Z",
33
+ blob: "M0 0 H100 V4 C85 11 72 0 58 5 C44 10 30 2 16 6 C10 8 5 5 0 6 Z"
34
+ });
35
+ function gemSectionDividers(props) {
36
+ const layers = [];
37
+ for (const edge of ["top", "bottom"]) {
38
+ const shape = props?.[`divider_${edge}`];
39
+ if (typeof shape === "string" && Object.hasOwn(GEM_SECTION_DIVIDER_PATHS, shape)) {
40
+ const known = shape;
41
+ layers.push({ edge, shape: known, path: GEM_SECTION_DIVIDER_PATHS[known] });
42
+ }
43
+ }
44
+ return layers;
45
+ }
46
+ function height(value) {
47
+ if (typeof value !== "number" && typeof value !== "string") return void 0;
48
+ if (typeof value === "string" && !/^\d+(?:\.\d+)?$/.test(value.trim())) return void 0;
49
+ const n = Number(value);
50
+ return Number.isFinite(n) && n >= 0 && n <= 320 ? n : void 0;
51
+ }
52
+ var NAMED_COLORS = /* @__PURE__ */ new Set([
53
+ "transparent",
54
+ "currentcolor",
55
+ "inherit",
56
+ "initial",
57
+ "unset",
58
+ "black",
59
+ "silver",
60
+ "gray",
61
+ "grey",
62
+ "white",
63
+ "maroon",
64
+ "red",
65
+ "purple",
66
+ "fuchsia",
67
+ "green",
68
+ "lime",
69
+ "olive",
70
+ "yellow",
71
+ "navy",
72
+ "blue",
73
+ "teal",
74
+ "aqua",
75
+ "orange"
76
+ ]);
77
+ var NUMBER = "[+-]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:e[+-]?\\d+)?";
78
+ var CHANNEL = new RegExp(`^${NUMBER}%?$`, "i");
79
+ var PERCENT = new RegExp(`^${NUMBER}%$`, "i");
80
+ var ANGLE = new RegExp(`^${NUMBER}(?:deg|grad|rad|turn)?$`, "i");
81
+ var finite = (value) => Number.isFinite(Number.parseFloat(value));
82
+ var numeric = (value) => CHANNEL.test(value) && finite(value);
83
+ var percentage = (value) => PERCENT.test(value) && finite(value);
84
+ var angle = (value) => ANGLE.test(value) && finite(value);
85
+ function functionalColor(value) {
86
+ const match = /^(rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch)\(([^()]*)\)$/i.exec(value);
87
+ if (!match) return false;
88
+ const name = match[1].toLowerCase(), body = match[2].trim();
89
+ let channels, alpha;
90
+ if (body.includes(",")) {
91
+ if (!["rgb", "rgba", "hsl", "hsla"].includes(name) || body.includes("/")) return false;
92
+ const parts = body.split(",").map((part) => part.trim());
93
+ if (parts.length !== 3 && parts.length !== 4) return false;
94
+ channels = parts.slice(0, 3);
95
+ alpha = parts[3];
96
+ if ((name === "rgb" || name === "rgba") && new Set(channels.map((part) => part.endsWith("%"))).size !== 1) return false;
97
+ } else {
98
+ const parts = body.split("/");
99
+ if (parts.length > 2) return false;
100
+ channels = parts[0].trim().split(/\s+/);
101
+ alpha = parts[1]?.trim();
102
+ }
103
+ if (channels.length !== 3 || alpha !== void 0 && !numeric(alpha)) return false;
104
+ if (name === "rgb" || name === "rgba") return channels.every(numeric);
105
+ if (name === "hsl" || name === "hsla" || name === "hwb") {
106
+ return angle(channels[0]) && percentage(channels[1]) && percentage(channels[2]);
107
+ }
108
+ if (name === "lch" || name === "oklch") return numeric(channels[0]) && numeric(channels[1]) && angle(channels[2]);
109
+ return channels.every(numeric);
110
+ }
111
+ function color(value) {
112
+ if (typeof value !== "string") return void 0;
113
+ const v = value.trim();
114
+ if (!v || v.length > 128 || /[;{}<>\\]/.test(v)) return void 0;
115
+ if (/^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(v) || NAMED_COLORS.has(v.toLowerCase())) return v;
116
+ if (functionalColor(v)) return v;
117
+ const token = /^var\(\s*--[A-Za-z0-9_-]+\s*(?:,\s*([^()]*))?\)$/.exec(v);
118
+ if (token && (token[1] === void 0 || color(token[1]))) return v;
119
+ return void 0;
120
+ }
121
+ function gemSectionDividerStyle(props, bandBackground) {
122
+ if (!gemSectionDividers(props).length) return {};
123
+ const value = props?.divider_flip;
124
+ const flip = value === true || value === "true" || value === 1 || value === "1";
125
+ return {
126
+ "--gem-element-divider-height": `${height(props?.divider_height) ?? 48}px`,
127
+ "--gem-element-divider-color": color(props?.divider_color) ?? color(bandBackground) ?? "var(--gem-section-bg, var(--gem-color-background))",
128
+ "--gem-element-divider-scale-x": flip ? "-1" : "1"
129
+ };
130
+ }
131
+ function gemSectionDividerResponsiveStyle(props) {
132
+ const n = height(props?.divider_height);
133
+ return n === void 0 ? {} : { "--gem-element-divider-height": `${n}px` };
134
+ }
135
+ var GEM_ELEMENT_SECTION_DIVIDER_CSS = [
136
+ ".gem-el-section[data-gem-element-dividers] { position: relative; }",
137
+ // A configured edge must escape the band's parallax clip. The media keeps its own clipped
138
+ // wrapper so releasing the root cannot expose the deliberately oversized moving background.
139
+ ".gem-el-section[data-gem-parallax][data-gem-element-dividers] { overflow: visible; isolation: auto; }",
140
+ ".gem-el-section[data-gem-element-dividers] > .gem-el-section__parallax-clip { position: absolute; inset: 0; overflow: hidden; border-radius: inherit; pointer-events: none; z-index: 0; }",
141
+ // Edge paint may extend into a neighbour with little padding. Only content, never its background,
142
+ // stacks above it. Guard on the current document's opted-in markup: a retained CSS resource after
143
+ // navigation must not change a page with no native dividers.
144
+ // An ordinary neighbouring parallax band also isolates its content by default. Release that
145
+ // stacking context on divider documents, retaining its existing media clip and direct markup.
146
+ "body:has(.gem-el-section[data-gem-element-dividers]) .gem-el-section[data-gem-parallax] { isolation: auto; }",
147
+ "body:has(.gem-el-section[data-gem-element-dividers]) .gem-el-section > .gem-el-section__inner { position: relative; z-index: 2; }",
148
+ ".gem-el-section > .gem-el-section__divider { position: absolute; display: block; inset-inline: 0; width: 100%; max-width: none; height: var(--gem-element-divider-height, 48px); color: var(--gem-element-divider-color, var(--gem-color-background)); overflow: hidden; pointer-events: none; z-index: 1; transform: scaleX(var(--gem-element-divider-scale-x, 1)); }",
149
+ ".gem-el-section > .gem-el-section__divider--bottom { top: 100%; }",
150
+ ".gem-el-section > .gem-el-section__divider--top { bottom: 100%; transform: scaleX(var(--gem-element-divider-scale-x, 1)) scaleY(-1); }"
151
+ ].join("\n");
152
+
22
153
  // ../../packages/gem-framework/src/form-set.ts
23
154
  var GEM_FORM_TOKEN_KEYS = [
24
155
  "radius",
@@ -76,7 +207,7 @@ var GEM_FORM_DEFAULTS = {
76
207
  checkboxAccent: "var(--gem-color-accent)",
77
208
  checkboxSize: "24px"
78
209
  };
79
- var GEM_FORM_VAR_SCOPE = "[data-gem-form-set], [data-gem-form-set] :is(input, select, textarea, button, form)";
210
+ var GEM_FORM_VAR_SCOPE = "[data-gem-form-set], [data-gem-form-set] :is(input, select, textarea, button, form, output)";
80
211
  function gemFormCssBlock(form) {
81
212
  const vars = gemFormCssVars(form);
82
213
  return vars.length ? `${GEM_FORM_VAR_SCOPE} {
@@ -130,6 +261,30 @@ var GEM_FORM_SETS = [
130
261
  }
131
262
  ];
132
263
  var gemFormSetsFor = (posture) => GEM_FORM_SETS.filter((s) => s.postures.includes(posture));
264
+ var GEM_SOFT_FIELD_FAMILIES = ["natural", "beauty", "home", "food"];
265
+ function deriveFormSetId(source) {
266
+ const norm = (v) => typeof v === "string" ? v.trim().toLowerCase() : "";
267
+ const radius = norm(source?.global_settings?.button_radius);
268
+ const style = norm(source?.global_settings?.button_style);
269
+ if (radius === "sharp" && style === "outline") return "hairline-underline";
270
+ if (radius === "pill" && style !== "soft") return "pill-joined";
271
+ if (style === "soft" || GEM_SOFT_FIELD_FAMILIES.includes(norm(source?.family))) return "soft-filled";
272
+ return "crisp-outline";
273
+ }
274
+ function gemPlanFormSet(plan) {
275
+ const tokens = plan?.tokens;
276
+ if (tokens && typeof tokens === "object" && Object.prototype.hasOwnProperty.call(tokens, "form")) {
277
+ const own = tokens.form;
278
+ if (own === null) return null;
279
+ if (own && typeof own === "object" && !Array.isArray(own)) {
280
+ if (hasForm(own)) return own;
281
+ const byId = GEM_FORM_SETS.find((s) => s.id === own.id);
282
+ if (byId) return { ...byId.form };
283
+ }
284
+ }
285
+ const set = GEM_FORM_SETS.find((s) => s.id === deriveFormSetId({ family: plan?.style_pack?.family, global_settings: plan?.global_settings }));
286
+ return set ? { ...set.form } : null;
287
+ }
133
288
 
134
289
  // ../../packages/gem-framework/src/plan-shape.ts
135
290
  function gemStyleSpaceFor(plan) {
@@ -230,13 +385,13 @@ function mixHex(from, to, t) {
230
385
  const channel = (x, y) => Math.round(x * (1 - t) + y * t).toString(16).padStart(2, "0");
231
386
  return `#${channel(a.r, b.r)}${channel(a.g, b.g)}${channel(a.b, b.b)}`;
232
387
  }
233
- function aaTextColor(color, backgrounds, min = MIN_TEXT_CONTRAST) {
234
- const from = normalizeHex(color);
388
+ function aaTextColor(color2, backgrounds, min = MIN_TEXT_CONTRAST) {
389
+ const from = normalizeHex(color2);
235
390
  const bgs = backgrounds.map(normalizeHex).filter((bg) => Boolean(bg));
236
- if (!from || !bgs.length) return color;
391
+ if (!from || !bgs.length) return color2;
237
392
  const worst = (c) => Math.min(...bgs.map((bg) => hexContrastRatio(c, bg)));
238
- let best = { color, score: worst(from) };
239
- if (best.score >= min) return color;
393
+ let best = { color: color2, score: worst(from) };
394
+ if (best.score >= min) return color2;
240
395
  let pass;
241
396
  for (const toward of ["#000000", "#ffffff"]) {
242
397
  for (let step = 1; step <= 50; step++) {
@@ -280,7 +435,7 @@ function gemButtonGradientEndCss(accent = "var(--gem-color-accent)", accentText
280
435
  }
281
436
 
282
437
  // ../../packages/gem-framework/src/tokens.ts
283
- var gemFrameworkVersion = "0.16.1";
438
+ var gemFrameworkVersion = "0.18.13";
284
439
  var GEM_FOCUS_RING_CSS_VAR = "--gem-focus-ring: 2px solid var(--gem-color-accent);";
285
440
  var GEM_TAP_MIN_CSS_VAR = "--gem-tap-min: 44px;";
286
441
  var GEM_FOCUS_OFFSET_CSS_VAR = "--gem-focus-offset: 3px;";
@@ -443,17 +598,17 @@ function gemBandScheme(c, band) {
443
598
  danger: aaTextColor(c.danger, grounds)
444
599
  };
445
600
  }
446
- function gemBandSchemeColors(color = {}) {
601
+ function gemBandSchemeColors(color2 = {}) {
447
602
  const c = Object.fromEntries(
448
603
  requiredGemTokenKeys.color.map((key) => {
449
- const value = color?.[key];
604
+ const value = color2?.[key];
450
605
  return [key, typeof value === "string" && value.trim() ? stripImportant(value) : defaultGemTokens.color[key]];
451
606
  })
452
607
  );
453
608
  return { dark: gemBandScheme(c, "dark"), accent: gemBandScheme(c, "accent") };
454
609
  }
455
- function gemBandSchemeCssVars(color = {}) {
456
- const schemes = gemBandSchemeColors(color);
610
+ function gemBandSchemeCssVars(color2 = {}) {
611
+ const schemes = gemBandSchemeColors(color2);
457
612
  return GEM_BAND_SCHEMES.flatMap(
458
613
  (band) => requiredGemTokenKeys.color.map((key) => `--gem-scheme-${band}-${slugify(key)}: ${schemes[band][key]};`)
459
614
  );
@@ -749,12 +904,12 @@ function gemMobileTokenCssVars(tokens, phone = readPhoneTokens(tokens)) {
749
904
  var rampKey = (k) => k.toLowerCase().replace(/[^a-z0-9]/g, "");
750
905
  var isRampKey = (k) => GEM_TYPE_RAMP_KEYS.some((r) => rampKey(r) === rampKey(k));
751
906
  var LENGTH = /^\d*\.?\d+(px|rem|em)$/;
752
- var NUMBER = /^\d*\.?\d+$/;
907
+ var NUMBER2 = /^\d*\.?\d+$/;
753
908
  var typeRampValue = (typography, key) => {
754
909
  const hit = Object.entries(typography ?? {}).find(([k]) => rampKey(k) === rampKey(key));
755
910
  const v = hit == null || hit[1] == null ? "" : stripImportant(hit[1]).trim();
756
911
  if (!v) return null;
757
- return (key === "scale" ? NUMBER.test(v) && Number(v) > 0 : LENGTH.test(v)) ? v : null;
912
+ return (key === "scale" ? NUMBER2.test(v) && Number(v) > 0 : LENGTH.test(v)) ? v : null;
758
913
  };
759
914
  var GEM_TYPE_MOBILE_MAX = "749px";
760
915
  function gemTypeRampCssVars(typography) {
@@ -833,6 +988,14 @@ var GEM_EDITION_SUPPORT_MONTHS = 6;
833
988
  var EDITION_2_CSS = [
834
989
  `:where(&) { ${GEM_HEADING_SIZE_CSS_VARS.filter((decl) => !decl.startsWith("--gem-section-heading-size-mobile") && !decl.startsWith("--gem-hero-heading-size-mobile")).join(" ")} }`
835
990
  ];
991
+ var EDITION_3_ELEMENTS = [
992
+ `:where(&) .gem-product-card-grid--quick-view-grid .gem-product-card-grid__card { border: 0; border-radius: 0; box-shadow: none; background: transparent; padding: 0; }`,
993
+ `:where(&) .gem-product-card-grid--quick-view-grid .gem-product-card__media > .gem-product-card__badges { display: flex; }`,
994
+ `@media (max-width: 989px) { :where(&) .gem-product-card-grid--quick-view-grid .gem-product-card-grid__grid[data-gem-product-grid] { grid-template-columns: repeat(var(--gem-product-card-grid-mobile-columns, 2), minmax(0, 1fr)); } }`,
995
+ `:where(&) .gem-hero.gem-hero-banner--compact-collection .gem-hero__overlay:not(.has-media) { position: relative; inset: auto; z-index: auto; opacity: 1; width: min(var(--gem-layout-page, 1200px), calc(100% - var(--gem-layout-gutter, 32px))); margin: clamp(24px, 5vw, 48px) auto 0; background: none; color: var(--gem-color-text); border-radius: 0; }`,
996
+ `:where(&) .gem-hero.gem-hero-banner--compact-collection .gem-hero__overlay:not(.has-media) .gem-hero__heading { color: var(--gem-color-text); }`,
997
+ `:where(&) .gem-section.gem-hero.gem-hero-banner--compact-collection:not(:has(> .gem-hero__bg)) { padding-block-end: 0; }`
998
+ ];
836
999
  var GEM_FRAMEWORK_EDITIONS = [
837
1000
  { edition: 1, since: "0.1.0", released: "2026-07-04", changes: [], css: [] },
838
1001
  {
@@ -841,6 +1004,18 @@ var GEM_FRAMEWORK_EDITIONS = [
841
1004
  released: "2026-10-10",
842
1005
  changes: ["Section headings and hero titles follow your type scale on tablets and computers. Phones keep their current size."],
843
1006
  css: EDITION_2_CSS
1007
+ },
1008
+ {
1009
+ // Edition 3. Collection pages at Dawn's tiers (12/10/2026, measured on a "Start from scratch" theme): EDITION_3_ELEMENTS.
1010
+ edition: 3,
1011
+ since: "0.17.0",
1012
+ released: "2026-10-12",
1013
+ changes: [
1014
+ "Collection grids: each product photo fills its column, phones and tablets show your mobile column count, and Sale and Sold out badges appear.",
1015
+ "The collection title sits on the page, lined up with the products, in your text colour."
1016
+ ],
1017
+ css: [],
1018
+ elements: EDITION_3_ELEMENTS
844
1019
  }
845
1020
  ];
846
1021
  var LATEST_GEM_FRAMEWORK_EDITION = GEM_FRAMEWORK_EDITIONS[GEM_FRAMEWORK_EDITIONS.length - 1].edition;
@@ -862,6 +1037,9 @@ function addEditionSupportMonths(released) {
862
1037
  function gemFrameworkEditionScope(edition) {
863
1038
  return GEM_FRAMEWORK_EDITIONS.filter((e) => e.edition >= edition).map((e) => `[data-gem-edition='${e.edition}']`).join(", ");
864
1039
  }
1040
+ function gemFrameworkEditionElementCss() {
1041
+ return GEM_FRAMEWORK_EDITIONS.flatMap((e) => (e.elements ?? []).map((rule) => rule.replaceAll("&", gemFrameworkEditionScope(e.edition)))).join("\n");
1042
+ }
865
1043
  function gemFrameworkEditionCss() {
866
1044
  return GEM_FRAMEWORK_EDITIONS.flatMap((e) => e.css.map((rule) => rule.replaceAll("&", gemFrameworkEditionScope(e.edition)))).join("\n");
867
1045
  }
@@ -1425,18 +1603,8 @@ var GEM_POPUP_SHELL_CSS = GEM_POPUP_SHELL_CSS_LINES.join("\n");
1425
1603
 
1426
1604
  // ../../packages/gem-framework/src/primitives.ts
1427
1605
  var dividerMask = (path) => `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 10' preserveAspectRatio='none'%3E%3Cpath fill='%23fff' d='${path}'/%3E%3C/svg%3E")`;
1428
- var spikePath = (() => {
1429
- const teeth = [];
1430
- for (let x = 95; x > 0; x -= 5) teeth.push(`L${x} ${teeth.length % 2 === 0 ? 10 : 3}`);
1431
- return `M0 0 H100 V3 ${teeth.join(" ")} L0 ${teeth.length % 2 === 0 ? 10 : 3} Z`;
1432
- })();
1433
- var DIVIDER_SHAPES = {
1434
- wave: "M0 0 H100 V3 C80 9 70 -1 50 4 C30 9 20 1 0 4 Z",
1435
- slope: "M0 0 H100 V10 Z",
1436
- spikes: spikePath,
1437
- arch: "M0 0 H100 V2 Q50 12 0 2 Z",
1438
- blob: "M0 0 H100 V4 C85 11 72 0 58 5 C44 10 30 2 16 6 C10 8 5 5 0 6 Z"
1439
- };
1606
+ var knownValues = (attr, values) => `:where(${values.map((v) => `[${attr}='${v}']`).join(", ")})`;
1607
+ var DIVIDER_SHAPES = Object.keys(GEM_SECTION_DIVIDER_PATHS);
1440
1608
  var GEM_DIVIDER_CSS_LINES = [
1441
1609
  // The clip is WIDENED, not removed. `.gem-section` sets `overflow: clip` and the wedge is drawn
1442
1610
  // at `top: 100%` / `bottom: 100%` — entirely outside the box — so the parent deleted every
@@ -1447,7 +1615,8 @@ var GEM_DIVIDER_CSS_LINES = [
1447
1615
  // it entirely would paint square corners over that radius. A browser without
1448
1616
  // `overflow-clip-margin` keeps plain `clip` — the divider stays hidden, which is today's
1449
1617
  // behaviour rather than a broken layout.
1450
- ":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-divider-top], :is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-divider-bottom] { position: relative; overflow: clip; overflow-clip-margin: var(--gem-divider-height, 48px); }",
1618
+ // Only for a shape that has a mask (knownValues): a "none" edge used to widen the clip by 48px for nothing.
1619
+ `:is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-divider-top]${knownValues("data-gem-divider-top", DIVIDER_SHAPES)}, :is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-divider-bottom]${knownValues("data-gem-divider-bottom", DIVIDER_SHAPES)} { position: relative; overflow: clip; overflow-clip-margin: var(--gem-divider-height, 48px); }`,
1451
1620
  // Shared geometry. `top: 100%` / `bottom: 100%` put it outside the box; z-index 1 keeps it above the
1452
1621
  // neighbouring band's background. pointer-events: none so it never eats a click near the seam.
1453
1622
  // Qualified by SHAPE. Unqualified, any value paints a solid 48px slab — including the literal
@@ -1457,7 +1626,7 @@ var GEM_DIVIDER_CSS_LINES = [
1457
1626
  ":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-divider-bottom]::after { top: 100%; }",
1458
1627
  // The top edge reuses the same masks, mirrored, so five shapes cover both edges.
1459
1628
  ":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-divider-top]::before { bottom: 100%; transform: scaleY(-1); }",
1460
- ...Object.entries(DIVIDER_SHAPES).flatMap(([name, path]) => {
1629
+ ...Object.entries(GEM_SECTION_DIVIDER_PATHS).flatMap(([name, path]) => {
1461
1630
  const mask = dividerMask(path);
1462
1631
  return [
1463
1632
  `:is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-divider-bottom='${name}']::after { -webkit-mask-image: ${mask}; mask-image: ${mask}; }`,
@@ -1466,8 +1635,10 @@ var GEM_DIVIDER_CSS_LINES = [
1466
1635
  })
1467
1636
  ];
1468
1637
  var GEM_DIVIDER_CSS = GEM_DIVIDER_CSS_LINES.join("\n");
1638
+ var GEM_CARD_STYLES = ["outline", "shadow", "filled"];
1469
1639
  var GEM_CARD_CSS_LINES = [
1470
- ":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-card] :is(.gem-section__inner, [class*='__inner']) { border-radius: var(--gem-radius-md); padding: var(--gem-space-xl); }",
1640
+ // Only for a style that draws (knownValues): "none", the knob's revert value, arrives here as an attribute too.
1641
+ `:is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-card]${knownValues("data-gem-card", GEM_CARD_STYLES)} :is(.gem-section__inner, [class*='__inner']) { border-radius: var(--gem-radius-md); padding: var(--gem-space-xl); }`,
1471
1642
  ":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-card='outline'] :is(.gem-section__inner, [class*='__inner']) { border: 1px solid var(--gem-color-border); }",
1472
1643
  ":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-card='shadow'] :is(.gem-section__inner, [class*='__inner']) { background: var(--gem-color-surface); box-shadow: var(--gem-shadow-md); }",
1473
1644
  // `filled` tints the section's own ground rather than reaching for a fixed colour, so it stays
@@ -1550,6 +1721,23 @@ var GEM_FORM_SET_CSS_LINES = [
1550
1721
  " inline-size: var(--gem-checkbox-size);",
1551
1722
  " block-size: var(--gem-checkbox-size);",
1552
1723
  " accent-color: var(--gem-checkbox-accent);",
1724
+ "}",
1725
+ // THE QUANTITY STEPPER IS A FIELD OF THE SET (20/10/2026): its box takes the set's height, edge and corner (capped at
1726
+ // 22px, so a pill set draws a round stepper, not a 999px lozenge), its -/+ and number the set's fill and text size, and
1727
+ // the underline shape paints no cell, as it paints no field. The storefront carried these (gemsite #1001/#1007, with its
1728
+ // own element stepper in the :is() lists); the package did not, so every section the kho previewed drew a square 44px
1729
+ // stepper beside 48px pill fields (GemFactory Framework › Controls, measured 20/10/2026).
1730
+ "[data-gem-form-set] :is(.gem-quantity-input) {",
1731
+ " min-height: var(--gem-input-height);",
1732
+ " border: var(--gem-input-border-width) solid var(--gem-input-border-color);",
1733
+ " border-radius: min(var(--gem-input-radius), 22px);",
1734
+ "}",
1735
+ "[data-gem-form-set] :is(.gem-quantity-input__button, .gem-quantity-input__field) {",
1736
+ " background: var(--gem-input-bg);",
1737
+ " font-size: var(--gem-input-font-size);",
1738
+ "}",
1739
+ '[data-gem-form-set][data-gem-field-shape="underline"] :is(.gem-quantity-input__button, .gem-quantity-input__field) {',
1740
+ " background: transparent;",
1553
1741
  "}"
1554
1742
  ];
1555
1743
  var GEM_FORM_SET_CSS = GEM_FORM_SET_CSS_LINES.join("\n");
@@ -1761,7 +1949,7 @@ function createGemBaseCssParts(profile) {
1761
1949
  ].join("\n");
1762
1950
  return {
1763
1951
  // Edition rules close the primitives (editions.ts): custom properties on <html>, scoped :where([data-gem-edition]).
1764
- primitives: [reset, "", createPrimitiveCss(), "", gemFrameworkEditionCss(), "", ""].join("\n"),
1952
+ primitives: [reset, "", createPrimitiveCss(), "", gemFrameworkEditionElementCss(), "", gemFrameworkEditionCss(), "", ""].join("\n"),
1765
1953
  sections: [createShellCss(), "", createSectionCss(), "", createThemeProfileCss(profile)].join("\n")
1766
1954
  };
1767
1955
  }
@@ -1771,7 +1959,11 @@ function createThemeProfileCss(profile = "dawn-parity") {
1771
1959
  function createMorphProfileCss() {
1772
1960
  return [
1773
1961
  "/* Gem morph profile: style packs and section presets provide visual transformation above core primitives. */",
1774
- ":root { --gem-profile-mode: morph; }"
1962
+ ":root { --gem-profile-mode: morph; }",
1963
+ // A morph theme's quick-view card is a box drawn from its card tokens (the .gem-card primitive). Edition 3 unboxes this
1964
+ // card for Dawn's collection page; restated here one class higher, so a morph theme keeps its box and its card knobs on
1965
+ // every edition (review of #764). The same values as .gem-card: nothing changes for a morph store on edition 1 or 2.
1966
+ ".gem-product-card-grid--quick-view-grid .gem-card.gem-product-card-grid__card { border: var(--gem-border-subtle, 1px solid var(--gem-color-border)); border-radius: var(--gem-card-radius, var(--gem-radius-md)); background: var(--gem-card-background, var(--gem-color-surface)); padding: var(--gem-component-card-padding, var(--gem-space-md)); box-shadow: var(--gem-shadow-sm, none); }"
1775
1967
  ].join("\n");
1776
1968
  }
1777
1969
  function createPrimitiveCss() {
@@ -1994,8 +2186,9 @@ function createPrimitiveCss() {
1994
2186
  " width: auto;",
1995
2187
  " min-width: min(10rem, 50%);",
1996
2188
  "}",
1997
- // A wrapped field gets the join's height too: the dawn-parity profile sets .gem-input { min-height: 4.5rem }, and
1998
- // only the direct-child rule above outranked it, so a wrapped input stood 72px next to a 44px one (02/10/2026).
2189
+ // A wrapped field gets the join's height too: the dawn-parity profile's 4.5rem floor named .gem-input until 0.18.12, and
2190
+ // only the direct-child rule above outranked it, so a wrapped input stood 72px next to a 44px one (02/10/2026). The
2191
+ // floor no longer names it; this keeps a wrapped field at the join's height whatever a profile floors it at.
1999
2192
  ".gem-field-join > .gem-form-field > :is(.gem-input, .gem-select) {",
2000
2193
  " width: 100%;",
2001
2194
  " height: 100%;",
@@ -2033,6 +2226,32 @@ function createPrimitiveCss() {
2033
2226
  " accent-color: var(--gem-color-accent);",
2034
2227
  "}",
2035
2228
  ...GEM_FORM_SET_CSS_LINES,
2229
+ // HOVER AND ERROR (§9.2 state matrix, 20/10/2026): GemFactory Framework › Controls measured both INERT, so a field a
2230
+ // shopper points at, or one a form marked invalid, looked like any other. Written once for every field, with or
2231
+ // without a form set.
2232
+ // HOVER thickens the edge by 1px in the field's own ink, as Dawn does (`box-shadow: 0 0 0 calc(border + .1rem)`): a
2233
+ // colour change alone was invisible under every form set, whose edge is already the ink (measured #1f2224 → #202223).
2234
+ // The ring follows the box and its corner; an underline or filled field has only a bottom rule, so only that thickens.
2235
+ // ERROR (`aria-invalid="true"`, set by the form) takes the danger colour mixed 70/30 with that ink, which keeps the
2236
+ // edge at 3:1 or more on a white field (9.7:1), an underline field on a dark band (4.2:1) and on an accent band
2237
+ // (3.0:1); plain #b42318 there was 2.4:1 and 1.8:1. Only the edge colour changes, so every shape keeps its edges.
2238
+ // Hover skips a disabled or invalid field (an error must not lose its colour under the pointer), a focused one (the
2239
+ // focus halo on a dark or accent band is a box-shadow, and a click focuses a text field as :focus-visible, so the
2240
+ // pointer still on it would wipe the halo: review) and touch screens, where a tap would leave it stuck.
2241
+ // Specificity: hover (0,6,0; 0,8,0 per shape) outranks the set's field rule (0,3,0); error (0,3,0) ties it and wins by
2242
+ // coming later in the sheet.
2243
+ "@media (hover: hover) {",
2244
+ ' :is(.gem-input, .gem-select, .gem-textarea, .gem-shell-footer__newsletter .gem-input):where(:not(.gem-search-form .gem-input)):hover:not(:disabled):not([aria-invalid="true"]):not(:focus-visible) {',
2245
+ " border-color: currentColor;",
2246
+ " box-shadow: 0 0 0 1px currentColor;",
2247
+ " }",
2248
+ ' [data-gem-form-set]:is([data-gem-field-shape="underline"], [data-gem-field-shape="filled"]) :is(.gem-input, .gem-select, .gem-textarea, .gem-shell-footer__newsletter .gem-input):where(:not(.gem-search-form .gem-input)):hover:not(:disabled):not([aria-invalid="true"]):not(:focus-visible) {',
2249
+ " box-shadow: 0 1px 0 0 currentColor;",
2250
+ " }",
2251
+ "}",
2252
+ ':is(.gem-input, .gem-select, .gem-textarea, .gem-shell-footer__newsletter .gem-input):where(:not(.gem-search-form .gem-input))[aria-invalid="true"] {',
2253
+ " border-color: color-mix(in srgb, var(--gem-color-danger, #b42318) 70%, currentColor);",
2254
+ "}",
2036
2255
  ".gem-submit {",
2037
2256
  " cursor: pointer;",
2038
2257
  "}",
@@ -2217,17 +2436,25 @@ function createPrimitiveCss() {
2217
2436
  // .gem-rating above is a type rule for a text label; every actual star row in the theme was a
2218
2437
  // hardcoded five-glyph literal, so a captured 4-of-5 (or a half star) had nowhere to land. The
2219
2438
  // fill is a percentage-width overlay, so fractional values render exactly instead of rounding.
2439
+ // SVG stars (GemFactory gap #45, 0.18.11): real review widgets draw each star as an SVG (solid + outline), and the
2440
+ // eight product-reviews sections rebuilt in GemFactory put those SVGs in __base / __fill. Two things broke the fill
2441
+ // (measured 12/10/2026): the base `svg { max-width: 100% }` shrank the SVGs in the clipped __fill to the fill's
2442
+ // width (4.2 drew as five squeezed stars), and a .gem-stars that is a stretched grid / flex item computed its
2443
+ // percentage against the stretched width (3 of 5 drew as 5). The row is as wide as its stars, always, and a star
2444
+ // keeps its own size inside the clip.
2220
2445
  ".gem-stars {",
2221
2446
  " --gem-stars-total: 5;",
2222
2447
  " --gem-stars-value: 5;",
2223
2448
  " position: relative;",
2224
2449
  " display: inline-block;",
2450
+ " width: max-content;",
2225
2451
  " line-height: 1;",
2226
2452
  " font-size: var(--gem-text-sm, 0.8125rem);",
2227
2453
  " letter-spacing: 0.1em;",
2228
2454
  " color: var(--gem-color-border);",
2229
2455
  "}",
2230
2456
  ".gem-stars__base, .gem-stars__fill { display: block; white-space: nowrap; }",
2457
+ ".gem-stars :is(svg, img) { max-width: none; flex: none; }",
2231
2458
  ".gem-stars__fill {",
2232
2459
  " position: absolute;",
2233
2460
  " inset: 0;",
@@ -2584,6 +2811,12 @@ function createPrimitiveCss() {
2584
2811
  " color: var(--gem-color-text);",
2585
2812
  " text-align: center;",
2586
2813
  "}",
2814
+ // The number in the middle of its cell, as the -/+ beside it (a <button> centres its label; an <output> is inline and
2815
+ // put the digit 8-9px above the middle; Framework › Controls, 20/10/2026).
2816
+ ".gem-quantity-input__field {",
2817
+ " display: grid;",
2818
+ " place-items: center;",
2819
+ "}",
2587
2820
  ".gem-quantity-input__button {",
2588
2821
  " font-weight: 750;",
2589
2822
  "}",
@@ -3733,7 +3966,7 @@ function createDawnParityCss() {
3733
3966
  ".gem-card, .gem-product-card { border: 0; border-radius: 0; box-shadow: none; background: transparent; padding: 0; }",
3734
3967
  ".gem-product-card__media, .gem-media { border: 0; border-radius: 0; box-shadow: none; background: rgb(244 244 244); }",
3735
3968
  ".gem-product-card__media { display: block; position: relative; width: 100%; height: 0; min-block-size: 0; padding-bottom: 100%; aspect-ratio: auto; overflow: hidden; }",
3736
- ".gem-featured-collection--editorial-grid .gem-product-card__media, .gem-product-card-grid--quick-view-grid .gem-product-card__media, .gem-main-search .gem-product-card__media, .gem-cart-recommendations--dawn-empty .gem-product-card__media { min-block-size: 0 !important; }",
3969
+ ".gem-featured-collection--editorial-grid .gem-product-card__media, .gem-product-card-grid--quick-view-grid .gem-product-card__media, .gem-main-search .gem-product-card__media, .gem-cart-recommendations--dawn-empty .gem-product-card__media, .gem-recommended-products--product-rail .gem-product-card__media, .gem-cart-recommendations--product-rail .gem-product-card__media { min-block-size: 0 !important; }",
3737
3970
  ".gem-product-card__media > img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; }",
3738
3971
  // Home product grids show generated/lifestyle imagery (not catalog product-on-white shots), so
3739
3972
  // `contain` letterboxes them inside the square card (looks misaligned / floating off the box).
@@ -3750,7 +3983,7 @@ function createDawnParityCss() {
3750
3983
  ".gem-cart-recommendations--dawn-empty .gem-product-card__quick-actions { display: none; }",
3751
3984
  ".gem-featured-collection--editorial-grid > .gem-section__inner > .gem-section__heading, .gem-featured-collection--editorial-grid > .gem-section__inner > .gem-section__body { display: none; }",
3752
3985
  ".gem-featured-collection--editorial-grid { padding-block: 28px 36px; }",
3753
- ".gem-featured-collection--editorial-grid .gem-section__grid, .gem-product-card-grid--quick-view-grid .gem-section__grid, .gem-main-search .gem-section__grid { grid-template-columns: repeat(var(--gem-product-card-grid-columns, 4), minmax(0, 1fr)); gap: 8px; }",
3986
+ ".gem-featured-collection--editorial-grid .gem-section__grid, .gem-product-card-grid--quick-view-grid .gem-section__grid, .gem-main-search .gem-section__grid, .gem-recommended-products--product-rail .gem-section__grid, .gem-cart-recommendations--product-rail .gem-section__grid { grid-template-columns: repeat(var(--gem-product-card-grid-columns, 4), minmax(0, 1fr)); gap: 8px; }",
3754
3987
  ".gem-product-card-grid--quick-view-grid { padding-top: 0; }",
3755
3988
  ".gem-product-card-grid--quick-view-grid .gem-section__heading:empty { display: none; }",
3756
3989
  // Hide a compare-at only when the price ISN'T actually on sale. The rule used to hide it
@@ -3920,14 +4153,16 @@ function createDawnParityMobileCss() {
3920
4153
  " .gem-hero-banner--compact-collection { padding-block: 25px; }",
3921
4154
  " .gem-hero-banner--compact-collection .gem-section__heading { font-size: 30px; line-height: 39px; }",
3922
4155
  " .gem-product-card-grid--quick-view-grid > .gem-section__inner, .gem-main-search > .gem-section__inner { width: calc(100% - 30px); }",
3923
- " .gem-product-card-grid--quick-view-grid .gem-product-card-grid__grid { gap: 4px !important; }",
4156
+ // 8px on a phone too, as the featured collection and the two product rails: the cards are Dawn's flat cards, and with
4157
+ // no padding of their own a 4px gap left two columns of titles 4px apart at 320 (review of gemsite #951).
4158
+ " .gem-product-card-grid--quick-view-grid .gem-product-card-grid__grid { gap: 8px !important; }",
3924
4159
  " .gem-product-card__content { padding-top: 8px; gap: 4px; }",
3925
4160
  " .gem-product-card__title { font-size: 15px; line-height: 24px; }",
3926
4161
  " .gem-product-card__price, .gem-product-card__price .gem-price { font-size: 16px; line-height: 24px; }",
3927
4162
  // Fallback = the profile's own phone heading, declared once as --gem-heading-phone-cap on the :root above.
3928
4163
  " .gem-section__heading { font-size: var(--gem-section-heading-size-mobile, var(--gem-heading-phone-cap)); }",
3929
4164
  " .gem-hero-banner h1, .gem-hero-banner h2 { font-size: var(--gem-hero-heading-size-mobile, var(--gem-heading-phone-cap)); }",
3930
- " .gem-featured-collection--editorial-grid .gem-section__grid, .gem-product-card-grid--quick-view-grid .gem-section__grid, .gem-main-search .gem-section__grid { grid-template-columns: repeat(var(--gem-product-card-grid-mobile-columns, 2), minmax(0, 1fr)); }",
4165
+ " .gem-featured-collection--editorial-grid .gem-section__grid, .gem-product-card-grid--quick-view-grid .gem-section__grid, .gem-main-search .gem-section__grid, .gem-recommended-products--product-rail .gem-section__grid, .gem-cart-recommendations--product-rail .gem-section__grid { grid-template-columns: repeat(var(--gem-product-card-grid-mobile-columns, 2), minmax(0, 1fr)); }",
3931
4166
  " .gem-product-main[data-gem-product-main-layout='dawn-main'] > .gem-section__inner { width: calc(100% - 30px); }",
3932
4167
  " .gem-product-main[data-gem-product-main-layout='dawn-main'] .gem-product-media-gallery__frame { display: flex; gap: 0; overflow-x: auto; scroll-snap-type: x mandatory; }",
3933
4168
  " .gem-product-main[data-gem-product-main-layout='dawn-main'] .gem-product-media-gallery__item { flex: 0 0 100%; scroll-snap-align: start; aspect-ratio: 1 / 1; }",
@@ -4178,7 +4413,10 @@ function createShellCss() {
4178
4413
  ".newsletter-form__field-wrapper, .field {",
4179
4414
  " position: relative;",
4180
4415
  "}",
4181
- ".field__input, .field input, .gem-input {",
4416
+ // Dawn's field floor, for Dawn's own field classes. It named .gem-input too, and 4.5rem is Dawn's 45px only on Dawn's
4417
+ // 10px root: on this 16px root a .gem-input stood 72px beside a 44px select and stepper whenever no form set ruled it
4418
+ // (GemFactory Framework › Controls, 20/10/2026). A .gem-input takes the primitive's floor, as every field does.
4419
+ ".field__input, .field input {",
4182
4420
  " min-height: 4.5rem;",
4183
4421
  " border-radius: var(--inputs-radius, 0);",
4184
4422
  "}",
@@ -6344,7 +6582,8 @@ var PREVIEW_MEDIA_CSS = [
6344
6582
  function renderGemPreviewDocument(plan, options = {}) {
6345
6583
  const page = options.page ?? "home";
6346
6584
  const profile = plan.theme_profile ?? "dawn-parity";
6347
- const baseCss = createGemThemeCss(plan.tokens, ":root", { profile, ...gemPlanShapeKnobs(plan) });
6585
+ const formSet = gemPlanFormSet(plan);
6586
+ const baseCss = createGemThemeCss({ ...plan.tokens, form: formSet ?? void 0 }, ":root", { profile, ...gemPlanShapeKnobs(plan) });
6348
6587
  const overflowGuardCss = "html,body{max-width:100%;overflow-x:hidden}*,*::before,*::after{box-sizing:border-box}";
6349
6588
  const css = [baseCss, overflowGuardCss, CHROME_VARIANT_CSS, options.sectionCss, PREVIEW_MEDIA_CSS].filter(Boolean).join("\n");
6350
6589
  const runtimeJs = options.includeRuntime === false ? "" : createGemRuntimeBootstrap(planToRuntimeBootstrapConfig(plan));
@@ -6352,7 +6591,7 @@ function renderGemPreviewDocument(plan, options = {}) {
6352
6591
  const footer = renderGemShellFooter(plan);
6353
6592
  const sectionsHtml = renderGemPageSections(plan, page, options.sectionMarkup ?? {});
6354
6593
  const title = options.title ?? `${plan.name} preview`;
6355
- const formAttrs = Object.entries(gemFormAttributes(plan.tokens.form)).map(([k, v]) => ` ${k}="${escapeAttribute(v)}"`).join("");
6594
+ const formAttrs = Object.entries(gemFormAttributes(formSet ?? void 0)).map(([k, v]) => ` ${k}="${escapeAttribute(v)}"`).join("");
6356
6595
  const bodyClass = ["gem-theme", `gem-theme--${slugify(profile)}`, options.bodyClassName].filter(Boolean).join(" ");
6357
6596
  const edition = gemFrameworkEditionOf(plan.framework_edition);
6358
6597
  const editionAttr = edition > 1 ? ` data-gem-edition="${edition}"` : "";
@@ -6590,7 +6829,7 @@ function renderGemSectionMarkup(section) {
6590
6829
  const widthRaw = settings.gem_content_width;
6591
6830
  const width = widthRaw === "full" || widthRaw === "contained" || widthRaw === "narrow" ? widthRaw : "";
6592
6831
  const heightRaw = settings.gem_section_height;
6593
- const height = heightRaw === "short" || heightRaw === "normal" || heightRaw === "tall" ? heightRaw : "";
6832
+ const height2 = heightRaw === "short" || heightRaw === "normal" || heightRaw === "tall" ? heightRaw : "";
6594
6833
  const columnsRaw = settings.gem_grid_columns;
6595
6834
  const columns = columnsRaw === "2" || columnsRaw === "3" || columnsRaw === "4" ? columnsRaw : "";
6596
6835
  const bgRaw = settings.gem_section_background;
@@ -6615,7 +6854,7 @@ function renderGemSectionMarkup(section) {
6615
6854
  media ? ` data-gem-media-position="${media}"` : "",
6616
6855
  spacing ? ` data-gem-content-spacing="${spacing}"` : "",
6617
6856
  width ? ` data-gem-content-width="${width}"` : "",
6618
- height ? ` data-gem-section-height="${height}"` : "",
6857
+ height2 ? ` data-gem-section-height="${height2}"` : "",
6619
6858
  columns ? ` data-gem-grid-columns="${columns}"` : "",
6620
6859
  background ? ` data-gem-section-background="${background}"` : "",
6621
6860
  priceColor ? ` data-gem-price-color="${priceColor}"` : "",
@@ -7422,6 +7661,7 @@ export {
7422
7661
  GEM_BUTTON_TOKEN_SHAPES,
7423
7662
  GEM_COUNTDOWN_ENDED_CSS,
7424
7663
  GEM_EDITION_SUPPORT_MONTHS,
7664
+ GEM_ELEMENT_SECTION_DIVIDER_CSS,
7425
7665
  GEM_FIELD_JOINS,
7426
7666
  GEM_FIELD_SHAPES,
7427
7667
  GEM_FORM_CSS_VARIABLES,
@@ -7444,6 +7684,8 @@ export {
7444
7684
  GEM_ROW_CSS,
7445
7685
  GEM_ROW_CSS_VARIABLES,
7446
7686
  GEM_ROW_GAP_CSS,
7687
+ GEM_SECTION_DIVIDER_PATHS,
7688
+ GEM_SOFT_FIELD_FAMILIES,
7447
7689
  LATEST_GEM_FRAMEWORK_EDITION,
7448
7690
  MIN_TEXT_CONTRAST,
7449
7691
  MODERN_MINERAL_FAMILY_PACK_ID,
@@ -7473,6 +7715,7 @@ export {
7473
7715
  createThemeProfileCss,
7474
7716
  defaultGemTokens,
7475
7717
  defaultSelection,
7718
+ deriveFormSetId,
7476
7719
  gemBandSchemeColors,
7477
7720
  gemBandSchemeCssVars,
7478
7721
  gemBandSchemeDeclarations,
@@ -7484,6 +7727,7 @@ export {
7484
7727
  gemFormCssVars,
7485
7728
  gemFormSetsFor,
7486
7729
  gemFrameworkEditionCss,
7730
+ gemFrameworkEditionElementCss,
7487
7731
  gemFrameworkEditionOf,
7488
7732
  gemFrameworkEditionScope,
7489
7733
  gemFrameworkEditionSupportedUntil,
@@ -7495,9 +7739,13 @@ export {
7495
7739
  gemMotionScaleCssVars,
7496
7740
  gemPartClass,
7497
7741
  gemPhoneTokenTarget,
7742
+ gemPlanFormSet,
7498
7743
  gemPlanShapeKnobs,
7499
7744
  gemRuntimePluginModuleKeys,
7500
7745
  gemRuntimePluginSources,
7746
+ gemSectionDividerResponsiveStyle,
7747
+ gemSectionDividerStyle,
7748
+ gemSectionDividers,
7501
7749
  getGemFrameworkValidationRule,
7502
7750
  hexContrastRatio,
7503
7751
  hexRelativeLuminance,