@gemboss/gem-framework 0.4.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.
@@ -0,0 +1,1199 @@
1
+ // ../../packages/gem-framework/src/tokens.ts
2
+ var GEM_FOCUS_RING_CSS_VAR = "--gem-focus-ring: 2px solid var(--gem-color-accent);";
3
+ var GEM_TAP_MIN_CSS_VAR = "--gem-tap-min: 44px;";
4
+ var GEM_ROW_CSS_VARIABLES = [
5
+ "--gem-row-cols",
6
+ "--gem-row-cols-md",
7
+ "--gem-row-cols-sm",
8
+ "--gem-row-tracks",
9
+ "--gem-row-tracks-md",
10
+ "--gem-row-tracks-sm",
11
+ "--gem-row-gap",
12
+ "--gem-row-gap-y"
13
+ ];
14
+ var requiredGemTokenKeys = {
15
+ color: ["background", "surface", "text", "muted", "accent", "accentText", "border", "success", "warning", "danger"],
16
+ typography: ["body", "heading", "mono"],
17
+ spacing: ["xs", "sm", "md", "lg", "xl", "2xl", "3xl"],
18
+ radius: ["sm", "md", "lg", "pill"],
19
+ border: ["subtle", "strong"],
20
+ shadow: ["sm", "md"],
21
+ motion: ["duration", "easing"],
22
+ layout: ["page", "gutter", "section"],
23
+ component: ["headerHeight", "cardPadding", "buttonPaddingInline", "buttonPaddingBlock", "inputHeight"],
24
+ morphology: ["campaignDensity", "mediaRhythm", "proofIntensity", "commerceEmphasis", "policyTone"]
25
+ };
26
+ function gemTypeRampCssVars() {
27
+ return [
28
+ "--gem-type-base: 1rem;",
29
+ "--gem-type-scale: 1.22;",
30
+ "--gem-text-sm: calc(var(--gem-type-base) / var(--gem-type-scale));",
31
+ "--gem-body: var(--gem-type-base);",
32
+ "--gem-h6: calc(var(--gem-type-base) * var(--gem-type-scale));",
33
+ "--gem-h5: calc(var(--gem-h6) * var(--gem-type-scale));",
34
+ "--gem-h4: calc(var(--gem-h5) * var(--gem-type-scale));",
35
+ "--gem-h3: calc(var(--gem-h4) * var(--gem-type-scale));",
36
+ "--gem-h2: calc(var(--gem-h3) * var(--gem-type-scale));",
37
+ "--gem-h1: calc(var(--gem-h2) * var(--gem-type-scale));"
38
+ ];
39
+ }
40
+ function slugify(value) {
41
+ return value.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
42
+ }
43
+
44
+ // ../../packages/gem-framework/src/manifest.ts
45
+ var gemFrameworkPrimitiveGroups = [
46
+ {
47
+ group: "layout",
48
+ // `gem-product-grid` is the shared product-card grid six sections lay their cards out on
49
+ // (featured-collection, product-card-grid, product-bundle, recently-viewed, and both
50
+ // recommended-products variants). It is styled once, globally, in the headless storefront —
51
+ // it was never section-scoped — but it had no entry here, so every section using it failed
52
+ // markup validation with `gem_framework_unknown_gem_class`.
53
+ // `gem-row` is the column primitive (primitives.ts GEM_ROW_CSS): counts and ratios per breakpoint
54
+ // through --gem-row-* variables, shared by section CSS and the element-tree Row.
55
+ classes: ["gem-container", "gem-stack", "gem-cluster", "gem-grid", "gem-row", "gem-product-grid"]
56
+ },
57
+ {
58
+ group: "actions",
59
+ classes: ["gem-button", "gem-button--primary", "gem-button--secondary", "gem-button--soft", "gem-button--link"]
60
+ },
61
+ {
62
+ group: "surfaces",
63
+ classes: ["gem-card", "gem-media", "gem-badge", "gem-accordion", "gem-divider"]
64
+ },
65
+ {
66
+ group: "forms",
67
+ classes: [
68
+ "gem-form-field",
69
+ "gem-input",
70
+ "gem-checkbox",
71
+ "gem-select",
72
+ "gem-textarea",
73
+ "gem-submit",
74
+ "gem-newsletter-form"
75
+ ]
76
+ },
77
+ {
78
+ group: "content",
79
+ classes: [
80
+ "gem-heading",
81
+ "gem-text",
82
+ // Centred, max-width reading column for long-form copy — styled globally in the headless
83
+ // storefront alongside gem-section__ctas.
84
+ "gem-prose",
85
+ "gem-icon",
86
+ "gem-icon-list",
87
+ "gem-icon-list__item",
88
+ "gem-video",
89
+ "gem-marquee",
90
+ "gem-marquee__track",
91
+ "gem-marquee__viewport",
92
+ "gem-marquee--reverse",
93
+ "gem-carousel",
94
+ "gem-carousel__viewport",
95
+ "gem-carousel__track",
96
+ "gem-carousel__slide",
97
+ "gem-carousel__controls",
98
+ "gem-article-card",
99
+ "gem-article-card__media",
100
+ "gem-article-card__meta",
101
+ "gem-article-card__title",
102
+ "gem-article-card__excerpt",
103
+ "gem-article-card__content",
104
+ // measured editorial atoms mapped by parts.ts (GEM_PART_CLASS) — styled primitives a gemcapture
105
+ // rebuild pours copy into; declared here so they don't read as unknown_gem_class.
106
+ "gem-pull-quote",
107
+ "gem-byline",
108
+ "gem-disclaimer",
109
+ "gem-subhead"
110
+ ]
111
+ },
112
+ {
113
+ group: "commerce",
114
+ classes: [
115
+ "gem-richtext",
116
+ "gem-price",
117
+ // BEM children + state of primitives that createPrimitiveCss ALREADY styles and runtime.ts / parts.ts
118
+ // already emit (parts.ts: compare_at_price → gem-price__compare, rating → gem-stars). Styled but
119
+ // unregistered, so a section using the framework's own price / stars / quantity markup failed
120
+ // `gem_framework_unknown_gem_class` — and at stable that warning is an error. Measured 28/09/2026
121
+ // porting four designlab (now gemlabs-gemfactory) sections: 75 warnings, every one of them a class/var this package defines.
122
+ "gem-price__regular",
123
+ "gem-price__compare",
124
+ "gem-price__unit",
125
+ "gem-price--on-sale",
126
+ "gem-rating",
127
+ "gem-proof",
128
+ "gem-product-card",
129
+ "gem-product-card__media",
130
+ // The framework's own CSS styles this (primitives.ts) and the Shopify adapter emits it, so it was a
131
+ // primitive everywhere except the list that decides what a primitive is. A section using it was
132
+ // refused by the validator for using a class the framework itself ships.
133
+ "gem-product-card__placeholder",
134
+ "gem-product-card__title",
135
+ "gem-product-card__price",
136
+ "gem-product-card__badges",
137
+ // Styled in primitives.ts alongside its siblings, but never listed here — so a section using
138
+ // it (product-card-grid.quick-view-grid) failed the gem-namespace check for a class the
139
+ // framework itself ships.
140
+ "gem-product-card__content",
141
+ "gem-product-card__quick-actions",
142
+ "gem-product-card__rating",
143
+ "gem-product-card__vendor",
144
+ "gem-product-card__description",
145
+ "gem-product-card__swatches",
146
+ "gem-product-card__secondary-image",
147
+ "gem-product-card--sold-out",
148
+ "gem-product-meta",
149
+ "gem-product-meta__item",
150
+ "gem-product-meta__label",
151
+ "gem-product-meta__value",
152
+ "gem-product-grid",
153
+ "gem-variant-picker",
154
+ "gem-swatch",
155
+ "gem-quantity-selector",
156
+ // What the quantity-input runtime plugin binds to (`[data-gem-quantity-input], .gem-quantity-input`).
157
+ "gem-quantity-input",
158
+ "gem-quantity-input__button",
159
+ "gem-quantity-input__field",
160
+ // What the media-gallery runtime plugin binds to (`.gem-product-media-gallery`).
161
+ "gem-product-media-gallery",
162
+ "gem-product-media-gallery__frame",
163
+ "gem-product-media-gallery__item",
164
+ "gem-product-media-gallery__thumbs",
165
+ "gem-product-media-gallery__thumb",
166
+ "gem-sticky-atc",
167
+ "gem-promo-banner",
168
+ "gem-promo-tile",
169
+ "gem-trust-list",
170
+ "gem-tabs",
171
+ "gem-breadcrumbs",
172
+ "gem-filter-bar",
173
+ "gem-drawer",
174
+ "gem-modal",
175
+ "gem-copy-code",
176
+ "gem-delivery-date",
177
+ "gem-stock-indicator",
178
+ "gem-popup",
179
+ "gem-back-to-top",
180
+ // measured commerce atoms mapped by parts.ts (GEM_PART_CLASS) alongside gem-price/gem-badge.
181
+ "gem-discount",
182
+ "gem-stars",
183
+ "gem-stars__base",
184
+ "gem-stars__fill",
185
+ "gem-review-count",
186
+ // The cart line: primitives.ts styles it (grid, media, details, meta, price, and the drawer's
187
+ // two-column override), and the Shopify adapter renders it inside SECTIONS
188
+ // (snippets/gem-cart-line-item.liquid in gem-cart-main-line-items and gem-main-order), yet it
189
+ // was never listed. GemFactory's cart-drawer used the framework's own line markup and got 9
190
+ // unknown_gem_class warnings; renaming it to a section-scoped class silenced them and dropped the
191
+ // framework layout (measured 28/09/2026: the screenshot changed). __variant / __plan are emitted
192
+ // by the adapter but not styled.
193
+ "gem-cart-line-item",
194
+ "gem-cart-line-item__media",
195
+ "gem-cart-line-item__details",
196
+ "gem-cart-line-item__title",
197
+ "gem-cart-line-item__variant",
198
+ "gem-cart-line-item__plan",
199
+ "gem-cart-line-item__meta",
200
+ "gem-cart-line-item__remove",
201
+ "gem-cart-line-item__quantity",
202
+ "gem-cart-line-item__price"
203
+ ]
204
+ },
205
+ {
206
+ group: "sections",
207
+ classes: [
208
+ "gem-section",
209
+ "gem-section__inner",
210
+ "gem-section__content",
211
+ "gem-section__eyebrow",
212
+ "gem-section__heading",
213
+ "gem-section__body",
214
+ // The header block above a section's body, and its two variants. Styled in the SHARED bridge
215
+ // stylesheet (`gem-dawn-bridge.css`, right beside `gem-section__body` which was already here)
216
+ // and used by 51 shipping Liquid sections — so it belongs to no single section and cannot be
217
+ // section-scoped. It was simply never added: this list enumerates BEM children by hand, and
218
+ // three of the seven `gem-section__*` classes the bridge styles had drifted off it. Sections
219
+ // using the framework correctly were failing promotion for it.
220
+ "gem-section__header",
221
+ "gem-section__header--center",
222
+ "gem-section__subheading",
223
+ "gem-section__grid",
224
+ // The CTA row under a section's copy. Styled globally in the headless storefront
225
+ // (`.gem-section__ctas`, plus an `:empty` rule), never section-scoped.
226
+ "gem-section__ctas",
227
+ // Model B: the shell modifier SectionTreeView puts on a section whose body is an element tree
228
+ // rather than a vendored component. One shared modifier for every such section, so it is a
229
+ // primitive here and could never be section-scoped.
230
+ "gem-section--elements"
231
+ ]
232
+ },
233
+ {
234
+ group: "shell",
235
+ classes: [
236
+ "gem-shell-header",
237
+ "gem-shell-header--sticky",
238
+ "gem-shell-header--editorial",
239
+ "gem-shell-header--search-led",
240
+ "gem-shell-header--mega-menu",
241
+ "gem-shell-header--split-nav",
242
+ "gem-shell-header--marketplace",
243
+ "gem-shell-header--promo-stack",
244
+ "gem-shell-header--sticky-search",
245
+ "gem-shell-header__announcement",
246
+ "gem-shell-header__utility",
247
+ "gem-shell-header__inner",
248
+ "gem-shell-header__logo",
249
+ "gem-shell-header__nav",
250
+ "gem-shell-header__nav--editorial",
251
+ "gem-shell-header__nav--mega",
252
+ "gem-shell-header__actions",
253
+ "gem-shell-header__action--search",
254
+ "gem-shell-header__action--cart",
255
+ "gem-shell-header__mega",
256
+ "gem-shell-header__departments",
257
+ "gem-shell-header__promo-stack",
258
+ "gem-shell-header__editorial-note",
259
+ "gem-shell-header__search",
260
+ "gem-shell-header__search--sticky",
261
+ "gem-shell-footer",
262
+ "gem-shell-footer--sitemap",
263
+ "gem-shell-footer--support-first",
264
+ "gem-shell-footer--community",
265
+ "gem-shell-footer--localization-rich",
266
+ "gem-shell-footer__inner",
267
+ "gem-shell-footer__newsletter",
268
+ "gem-shell-footer__grid",
269
+ "gem-shell-footer__story",
270
+ "gem-shell-footer__compliance",
271
+ "gem-shell-footer__offer",
272
+ "gem-shell-footer__support",
273
+ "gem-shell-footer__community",
274
+ "gem-shell-footer__markets",
275
+ "gem-shell-footer__trust"
276
+ ]
277
+ },
278
+ {
279
+ group: "utilities",
280
+ classes: [
281
+ "gem-u-hidden",
282
+ "gem-u-visually-hidden",
283
+ "gem-u-center",
284
+ "gem-u-muted",
285
+ "gem-u-surface",
286
+ "gem-u-accent",
287
+ "gem-u-stack-sm",
288
+ "gem-u-stack-md",
289
+ "gem-u-stack-lg"
290
+ // gem-u-grid-2/3/4 removed 28/09/2026: no section, theme or renderer in the fleet used them
291
+ // (gemtheme, gemsite, gemboss, gemcapture, GemFactory: 0), and they could not work alone — a
292
+ // grid-template with no display:grid, no gap and one breakpoint. Columns are `gem-row` now.
293
+ ]
294
+ }
295
+ ];
296
+ var gemFrameworkValidationRules = [
297
+ { code: "legacy_gs_class", severity: "error", surface: "artifact" },
298
+ { code: "non_gem_class", severity: "error", surface: "artifact" },
299
+ { code: "unknown_gem_class", severity: "warn", surface: "artifact" },
300
+ { code: "unknown_gem_css_variable", severity: "warn", surface: "css" },
301
+ { code: "untokenized_visual_value", severity: "warn", surface: "css" },
302
+ { code: "missing_data_gem_marker", severity: "warn", surface: "markup" },
303
+ { code: "inline_script", severity: "error", surface: "runtime" },
304
+ { code: "external_dependency", severity: "error", surface: "runtime" }
305
+ ];
306
+
307
+ // ../../packages/gem-framework/src/primitives.ts
308
+ 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")`;
309
+ var spikePath = (() => {
310
+ const teeth = [];
311
+ for (let x = 95; x > 0; x -= 5) teeth.push(`L${x} ${teeth.length % 2 === 0 ? 10 : 3}`);
312
+ return `M0 0 H100 V3 ${teeth.join(" ")} L0 ${teeth.length % 2 === 0 ? 10 : 3} Z`;
313
+ })();
314
+ var DIVIDER_SHAPES = {
315
+ wave: "M0 0 H100 V3 C80 9 70 -1 50 4 C30 9 20 1 0 4 Z",
316
+ slope: "M0 0 H100 V10 Z",
317
+ spikes: spikePath,
318
+ arch: "M0 0 H100 V2 Q50 12 0 2 Z",
319
+ blob: "M0 0 H100 V4 C85 11 72 0 58 5 C44 10 30 2 16 6 C10 8 5 5 0 6 Z"
320
+ };
321
+ var GEM_DIVIDER_CSS_LINES = [
322
+ // The clip is WIDENED, not removed. `.gem-section` sets `overflow: clip` and the wedge is drawn
323
+ // at `top: 100%` / `bottom: 100%` — entirely outside the box — so the parent deleted every
324
+ // divider on every track. Measured on the live storefront 2026-08-25: a band with the documented
325
+ // `data-gem-section-background="dark"` + `data-gem-divider-bottom="wave"` rendered a FLAT edge,
326
+ // while the attribute, the rule and the fallback colour were all present and correct.
327
+ // NOT `overflow: visible`: `.gem-hero` uses the clip to hold its own border-radius, so releasing
328
+ // it entirely would paint square corners over that radius. A browser without
329
+ // `overflow-clip-margin` keeps plain `clip` — the divider stays hidden, which is today's
330
+ // behaviour rather than a broken layout.
331
+ ":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow, .gem-footer)[data-gem-divider-top], :is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow, .gem-footer)[data-gem-divider-bottom] { position: relative; overflow: clip; overflow-clip-margin: var(--gem-divider-height, 48px); }",
332
+ // Shared geometry. `top: 100%` / `bottom: 100%` put it outside the box; z-index 1 keeps it above the
333
+ // neighbouring band's background. pointer-events: none so it never eats a click near the seam.
334
+ // Qualified by SHAPE. Unqualified, any value paints a solid 48px slab — including the literal
335
+ // "none", which the attribute writer forwards unchanged. The clip used to hide that; widening
336
+ // it above makes a stray value visible.
337
+ ":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow, .gem-footer)[data-gem-divider-bottom='wave']::after, :is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow, .gem-footer)[data-gem-divider-bottom='slope']::after, :is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow, .gem-footer)[data-gem-divider-bottom='spikes']::after, :is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow, .gem-footer)[data-gem-divider-bottom='arch']::after, :is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow, .gem-footer)[data-gem-divider-bottom='blob']::after, :is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow, .gem-footer)[data-gem-divider-top='wave']::before, :is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow, .gem-footer)[data-gem-divider-top='slope']::before, :is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow, .gem-footer)[data-gem-divider-top='spikes']::before, :is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow, .gem-footer)[data-gem-divider-top='arch']::before, :is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow, .gem-footer)[data-gem-divider-top='blob']::before { content: ''; position: absolute; left: 0; right: 0; z-index: 1; pointer-events: none; height: var(--gem-divider-height, 48px); background: var(--gem-section-bg, var(--gem-color-background)); -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat; -webkit-mask-size: 100% 100%; mask-size: 100% 100%; }",
338
+ ":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow, .gem-footer)[data-gem-divider-bottom]::after { top: 100%; }",
339
+ // The top edge reuses the same masks, mirrored, so five shapes cover both edges.
340
+ ":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow, .gem-footer)[data-gem-divider-top]::before { bottom: 100%; transform: scaleY(-1); }",
341
+ ...Object.entries(DIVIDER_SHAPES).flatMap(([name, path]) => {
342
+ const mask = dividerMask(path);
343
+ return [
344
+ `:is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow, .gem-footer)[data-gem-divider-bottom='${name}']::after { -webkit-mask-image: ${mask}; mask-image: ${mask}; }`,
345
+ `:is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow, .gem-footer)[data-gem-divider-top='${name}']::before { -webkit-mask-image: ${mask}; mask-image: ${mask}; }`
346
+ ];
347
+ })
348
+ ];
349
+ var GEM_DIVIDER_CSS = GEM_DIVIDER_CSS_LINES.join("\n");
350
+ var GEM_CARD_CSS_LINES = [
351
+ ":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow, .gem-footer)[data-gem-card] :is(.gem-section__inner, [class*='__inner']) { border-radius: var(--gem-radius-md); padding: var(--gem-space-xl); }",
352
+ ":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow, .gem-footer)[data-gem-card='outline'] :is(.gem-section__inner, [class*='__inner']) { border: 1px solid var(--gem-color-border); }",
353
+ ":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow, .gem-footer)[data-gem-card='shadow'] :is(.gem-section__inner, [class*='__inner']) { background: var(--gem-color-surface); box-shadow: var(--gem-shadow-md); }",
354
+ // `filled` tints the section's own ground rather than reaching for a fixed colour, so it stays
355
+ // legible on an accent or dark band instead of punching a light panel into it.
356
+ ":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow, .gem-footer)[data-gem-card='filled'] :is(.gem-section__inner, [class*='__inner']) { background: color-mix(in srgb, currentColor 6%, transparent); }"
357
+ ];
358
+ var GEM_CARD_CSS = GEM_CARD_CSS_LINES.join("\n");
359
+ var GEM_REVEAL_CSS_LINES = [
360
+ // Longhand, so the per-element duration/delay knobs can reach it. The shorthand hard-coded 600ms,
361
+ // while `gem_animation_duration` and `gem_animation_delay` are declared on ALL FIFTY kinds in the
362
+ // element seam ("Empty keeps the default (600ms). Capped at 5000."). gemsite honours them; this
363
+ // copy could not, so the knob worked on the headless storefront and did nothing on the exported
364
+ // Shopify theme — offered to every seller, inert on one of the two tracks.
365
+ //
366
+ // The fallbacks are the values this rule shipped with, so a theme where nothing writes the
367
+ // variables renders EXACTLY as before. Note what this does not finish: the exported theme has no
368
+ // writer for `--gem-el-motion-*` yet (gemsite writes them in element-motion.ts), so the knob stays
369
+ // inert there until one exists. This removes the CSS half of that gap, not the whole gap.
370
+ ".gem-reveal, [data-gem-reveal] { transition-property: opacity, transform, filter; transition-timing-function: cubic-bezier(0.16, 1, 0.3, 1); transition-duration: var(--gem-el-motion-duration, 600ms); transition-delay: var(--gem-el-motion-delay, 0ms); }",
371
+ // Bare `data-gem-reveal` (no value) keeps its original rise-and-fade — the candidate sections and
372
+ // gempages blocks that already opt in this way must not change behaviour.
373
+ "[data-gem-reveal][data-gem-reveal-state='ready'] { opacity: 0; transform: translateY(24px); }",
374
+ "[data-gem-reveal][data-gem-reveal-state='in'] { opacity: 1; transform: none; filter: none; }",
375
+ // The set_layout `reveal` knob writes a VALUE, and each value picks its own resting pose. Only the
376
+ // 'ready' state differs — 'in' above returns every variant to the same neutral end state.
377
+ "[data-gem-reveal='fade'][data-gem-reveal-state='ready'] { opacity: 0; transform: none; }",
378
+ "[data-gem-reveal='rise'][data-gem-reveal-state='ready'] { opacity: 0; transform: translateY(24px); }",
379
+ "[data-gem-reveal='zoom'][data-gem-reveal-state='ready'] { opacity: 0; transform: scale(0.94); }",
380
+ "[data-gem-reveal='blur'][data-gem-reveal-state='ready'] { opacity: 0; filter: blur(10px); transform: none; }",
381
+ "@media (prefers-reduced-motion: reduce) { [data-gem-reveal] { opacity: 1 !important; transform: none !important; filter: none !important; transition: none; } }"
382
+ ];
383
+ var GEM_REVEAL_CSS = GEM_REVEAL_CSS_LINES.join("\n");
384
+ var GEM_AMBIENT_CSS_LINES = [
385
+ "@keyframes gem-float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(calc(-1 * var(--gem-ambient-amp, 7px))); } }",
386
+ "@keyframes gem-sheen { 0%, 100% { opacity: 0.55; } 50% { opacity: 1; } }",
387
+ "@keyframes gem-marquee-scroll { from { transform: translateX(0); } to { transform: translateX(-50%); } }",
388
+ "[data-gem-ambient='float'] { animation: gem-float var(--gem-ambient-mid, 7s) var(--gem-ease-soft, ease-in-out) infinite; will-change: transform; }",
389
+ "[data-gem-ambient='sheen'] { animation: gem-sheen var(--gem-ambient-slow, 9s) var(--gem-ease-soft, ease-in-out) infinite; will-change: opacity; }",
390
+ "[data-gem-ambient='marquee'] { animation: gem-marquee-scroll var(--gem-ambient-fast, 5.5s) linear infinite; will-change: transform; }",
391
+ // IO gate — the runtime toggles this attribute; unobserved nodes simply never pause (safe default).
392
+ "[data-gem-offscreen] { animation-play-state: paused; }",
393
+ "@media (prefers-reduced-motion: reduce) {",
394
+ " [data-gem-ambient] { --gem-ambient-amp: 2px; animation-duration: 12s; }",
395
+ " [data-gem-reveal] { transition: none; opacity: 1; transform: none; }",
396
+ " .gem-marquee__track { animation-duration: calc(var(--gem-marquee-duration, 40s) * 2.5); }",
397
+ "}"
398
+ ];
399
+ var GEM_AMBIENT_CSS = GEM_AMBIENT_CSS_LINES.join("\n");
400
+ var GEM_ROW_GAP_CSS_LINES = [
401
+ ".gem-row { column-gap: var(--gem-row-gap, var(--gem-space-lg)); row-gap: var(--gem-row-gap-y, var(--gem-row-gap, var(--gem-space-lg))); }"
402
+ ];
403
+ var GEM_ROW_CSS_LINES = [
404
+ ".gem-row:where(:not([hidden])) { --gem-row-auto-sm: repeat(var(--gem-row-cols-sm), minmax(0, 1fr)); --gem-row-auto-md: repeat(var(--gem-row-cols-md), minmax(0, 1fr)); --gem-row-auto-lg: repeat(var(--gem-row-cols), minmax(0, 1fr)); display: grid; grid-template-columns: var(--gem-row-tracks-sm, var(--gem-row-auto-sm, minmax(0, 1fr))); }",
405
+ // :where() = specificity 0: any rule that sets a count on a nested row wins over this reset, whatever
406
+ // its specificity or load order; the reset only has to beat inheritance, and any declaration does.
407
+ `:where(.gem-row > *) { min-width: 0; ${GEM_ROW_CSS_VARIABLES.map((name) => `${name}: initial;`).join(" ")} }`,
408
+ "@media (min-width: 750px) { .gem-row { grid-template-columns: var(--gem-row-tracks-md, var(--gem-row-auto-md, var(--gem-row-tracks, var(--gem-row-auto-lg, repeat(2, minmax(0, 1fr)))))); } }",
409
+ "@media (min-width: 990px) { .gem-row { grid-template-columns: var(--gem-row-tracks, var(--gem-row-auto-lg, repeat(2, minmax(0, 1fr)))); } }"
410
+ ];
411
+ var GEM_ROW_GAP_CSS = GEM_ROW_GAP_CSS_LINES.join("\n");
412
+ var GEM_ROW_CSS = GEM_ROW_CSS_LINES.join("\n");
413
+ var GEM_GRID_COLUMNS_KNOB_CSS_LINES = [
414
+ ":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow, .gem-footer)[data-gem-grid-columns='2'] :is(.gem-section__grid, [class*='__grid']):not(.gem-row) { grid-template-columns: repeat(2, minmax(0, 1fr)); }",
415
+ ":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow, .gem-footer)[data-gem-grid-columns='2'] .gem-row:is(.gem-section__grid, [class*='__grid']):not(.gem-row .gem-row) { --gem-row-cols: 2; --gem-row-cols-md: 2; --gem-row-tracks: initial; --gem-row-tracks-md: initial; }",
416
+ ":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow, .gem-footer)[data-gem-grid-columns='3'] :is(.gem-section__grid, [class*='__grid']):not(.gem-row) { grid-template-columns: repeat(3, minmax(0, 1fr)); }",
417
+ ":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow, .gem-footer)[data-gem-grid-columns='3'] .gem-row:is(.gem-section__grid, [class*='__grid']):not(.gem-row .gem-row) { --gem-row-cols: 3; --gem-row-cols-md: 3; --gem-row-tracks: initial; --gem-row-tracks-md: initial; }",
418
+ ":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow, .gem-footer)[data-gem-grid-columns='4'] :is(.gem-section__grid, [class*='__grid']):not(.gem-row) { grid-template-columns: repeat(4, minmax(0, 1fr)); }",
419
+ ":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow, .gem-footer)[data-gem-grid-columns='4'] .gem-row:is(.gem-section__grid, [class*='__grid']):not(.gem-row .gem-row) { --gem-row-cols: 4; --gem-row-cols-md: 4; --gem-row-tracks: initial; --gem-row-tracks-md: initial; }"
420
+ ];
421
+ var GEM_GRID_COLUMNS_ROW_KNOB_CSS = GEM_GRID_COLUMNS_KNOB_CSS_LINES.filter((line) => line.includes(" .gem-row:is(")).join("\n");
422
+
423
+ // ../../packages/gem-framework/src/offer-engine.ts
424
+ function offerEngineRuntimeScript() {
425
+ return `(function(){
426
+ document.querySelectorAll('[data-gem-offer-engine]').forEach(function(root){
427
+ if(root.__gemOffer)return; root.__gemOffer=1;
428
+ var el=root.querySelector('[data-gem-offer-model]'); if(!el)return;
429
+ var model; try{model=JSON.parse(el.textContent);}catch(e){return;}
430
+ var dims=model.dimensions||{}, sel={};
431
+ Object.keys(dims).forEach(function(d){var o=(dims[d].options||[]),def=o.filter(function(x){return x.default;})[0]||o[0];sel[d]=(model.default_selection&&model.default_selection[d])||(def&&def.id);});
432
+ function m(w){return Object.keys(w).every(function(k){return sel[k]===w[k];});}
433
+ function sp(w){return Object.keys(w).length;}
434
+ function pick(list){return (list||[]).filter(function(r){return m(r.when);}).sort(function(a,b){return sp(b.when)-sp(a.when);})[0];}
435
+ function gift(g,t){if(!g||t==null)return null;var sym=g.currency_symbol||'$';var rem=Math.max(0,Math.round((g.threshold-t)*100)/100);var unl=rem<=0;var rs=sym+rem.toFixed(2);var pct=g.threshold>0?Math.max(0,Math.min(100,Math.round((t/g.threshold)*100))):100;var tpl=unl?(g.unlocked_text||'\\uD83C\\uDF81 {label} unlocked!'):(g.locked_text||'Add {remaining} more to unlock {label}');return {unlocked:unl,remaining:rs,remaining_amount:rem,percent:pct,text:tpl.replace('{remaining}',rs).replace('{label}',g.label)};}
436
+ function render(){
437
+ var row=pick(model.price_matrix)||{}, plan=pick(model.selling_plan_map);
438
+ var vis={}; Object.keys(dims).forEach(function(d){vis[d]=true;});
439
+ (model.conditions||[]).forEach(function(c){vis[c.show]=(sel[c.when_dim]===c.equals);});
440
+ (model.panels||[]).forEach(function(p){if(p.when)vis[p.id]=(sel[p.when.when_dim]===p.when.equals);});
441
+ var headline=row.total||row.price||'';
442
+ var gf=gift(model.gift,row.total_amount);
443
+ root.querySelectorAll('[data-gem-offer-out]').forEach(function(o){var k=o.getAttribute('data-gem-offer-out');
444
+ if(k==='price')o.textContent=row.price||'';
445
+ else if(k==='compare'){o.textContent=row.compare_at||'';o.style.display=row.compare_at?'':'none';}
446
+ else if(k==='save'){o.textContent=row.save_label||'';o.style.display=row.save_label?'':'none';}
447
+ else if(k==='gift'){o.textContent=gf?gf.text:'';if(o.classList)o.classList.toggle('unlocked',!!(gf&&gf.unlocked));}
448
+ else if(k==='cta')o.textContent=(model.cta_template||'Add to cart').replace('{price}',headline).replace('{total}',headline);});
449
+ root.querySelectorAll('[data-gem-offer-gift]').forEach(function(w){w.style.display=gf?'':'none';if(w.classList)w.classList.toggle('unlocked',!!(gf&&gf.unlocked));});
450
+ root.querySelectorAll('[data-gem-offer-gift-bar]').forEach(function(b){b.style.width=(gf?gf.percent:0)+'%';});
451
+ root.querySelectorAll('[data-gem-offer-group]').forEach(function(g){g.style.display=(vis[g.getAttribute('data-gem-offer-group')]===false)?'none':'';});
452
+ root.querySelectorAll('[data-gem-offer-dim]').forEach(function(b){var d=b.getAttribute('data-gem-offer-dim'),v=b.getAttribute('data-gem-offer-value');if(b.tagName==='OPTION'){b.selected=(sel[d]===v);}else{b.classList.toggle('sel',sel[d]===v);}});
453
+ var s=root.querySelector('[data-gem-offer-selling-plan]'); if(s)s.value=(plan&&plan.selling_plan_id)||'';
454
+ var ca=(plan&&plan.cart_attributes)||{};
455
+ root.querySelectorAll('[data-gem-offer-cart-field]').forEach(function(f){var n=f.getAttribute('data-gem-offer-cart-field');f.value=ca[n]||'';f.disabled=!(n in ca);});
456
+ root.dispatchEvent(new CustomEvent('gem:offer:change',{bubbles:true,detail:{selection:sel,row:row,selling_plan_id:(plan&&plan.selling_plan_id)||null,cart_attributes:ca,gift:gf}}));
457
+ }
458
+ root.addEventListener('click',function(e){var b=e.target.closest('[data-gem-offer-dim]');if(!b||b.tagName==='OPTION')return;sel[b.getAttribute('data-gem-offer-dim')]=b.getAttribute('data-gem-offer-value');render();});
459
+ root.addEventListener('change',function(e){var s=e.target.closest('select[data-gem-offer-select]');if(!s)return;var o=s.options[s.selectedIndex];sel[s.getAttribute('data-gem-offer-select')]=o.getAttribute('data-gem-offer-value');render();});
460
+ render();
461
+ });
462
+ })();`;
463
+ }
464
+
465
+ // ../../packages/gem-framework/src/runtime.ts
466
+ var offerEnginePluginRegistrationLines = [
467
+ " registerPlugin('offer-engine', ({ api }) => {",
468
+ " " + offerEngineRuntimeScript(),
469
+ " api.emit('gem:offer:ready', { source: 'gem-runtime' });",
470
+ " });"
471
+ ];
472
+
473
+ // ../../packages/gem-framework/src/validation.ts
474
+ var validationRulesByCode = new Map(gemFrameworkValidationRules.map((rule) => [rule.code, rule]));
475
+ var primitiveClassNames = new Set(
476
+ gemFrameworkPrimitiveGroups.flatMap((group) => group.classes)
477
+ );
478
+ var canonicalCssVariableNames = /* @__PURE__ */ new Set([
479
+ ...Object.entries(requiredGemTokenKeys).flatMap(([group, keys]) => {
480
+ const namespace = group === "typography" ? "font" : group === "spacing" ? "space" : group;
481
+ return keys.map((key) => `--gem-${namespace}-${slugify(key)}`);
482
+ }),
483
+ // The governed type ramp (--gem-h1…h6, --gem-body, --gem-text-sm, --gem-type-base/-scale). The
484
+ // framework stylesheet declares it on every page, but it is not a token GROUP, so it was missing here
485
+ // and a section sizing its heading with `var(--gem-h2)` got `unknown_gem_css_variable`. Read from the
486
+ // function that emits it, so the ramp and this list cannot drift.
487
+ ...gemTypeRampCssVars().map((line) => line.split(":")[0].trim()),
488
+ // --gem-focus-ring: emitted with the tokens, not a group (tokens.ts GEM_FOCUS_RING_CSS_VAR).
489
+ GEM_FOCUS_RING_CSS_VAR.split(":")[0].trim(),
490
+ GEM_TAP_MIN_CSS_VAR.split(":")[0].trim(),
491
+ // The row primitive's variables (primitives.ts GEM_ROW_CSS). Sections SET these on their grid.
492
+ ...GEM_ROW_CSS_VARIABLES
493
+ ]);
494
+ var TOKENIZED_PROPERTIES = [
495
+ "background",
496
+ "background-color",
497
+ "border",
498
+ "border-color",
499
+ "box-shadow",
500
+ "color",
501
+ "font-family",
502
+ "gap",
503
+ "margin",
504
+ "margin-block",
505
+ "margin-inline",
506
+ "padding",
507
+ "padding-block",
508
+ "padding-inline",
509
+ "border-radius",
510
+ // The row's spacing variables end up as column-gap / row-gap: a raw length there must be caught the same
511
+ // way `gap: 37px` is, or moving a grid onto .gem-row quietly switches the token check off (#589 review).
512
+ "--gem-row-gap",
513
+ "--gem-row-gap-y"
514
+ ];
515
+ function validateGemFrameworkArtifact(artifact, options = {}) {
516
+ if (artifact.path.endsWith(".css")) {
517
+ return validateGemFrameworkCss(artifact.content, artifact.path, options);
518
+ }
519
+ if (/\.(liquid|html|tsx)$/.test(artifact.path)) {
520
+ return validateGemFrameworkMarkup(artifact.content, artifact.path, options);
521
+ }
522
+ if (artifact.path.endsWith(".js")) {
523
+ return result(validateSharedSafety(artifact.content, artifact.path));
524
+ }
525
+ return result([]);
526
+ }
527
+ function validateGemFrameworkCss(css, artifactPath, options = {}) {
528
+ const issues = validateSharedSafety(css, artifactPath);
529
+ css = css.replace(/\/\*[\s\S]*?\*\//g, " ");
530
+ if (/\bgs-/.test(css)) {
531
+ issues.push(ruleIssue("legacy_gs_class", "Gem Framework CSS must not use legacy gs-* classes.", artifactPath));
532
+ }
533
+ const classNames = [...css.matchAll(/\.([_A-Za-z-][_A-Za-z0-9-]*)/g)].map((match) => match[1]);
534
+ for (const className of classNames) {
535
+ const audit = auditGemFrameworkClassName(className, options);
536
+ if (audit.kind === "non_gem") {
537
+ issues.push(ruleIssue("non_gem_class", `CSS class .${className} must use the gem-* namespace.`, artifactPath, {
538
+ class_name: className
539
+ }));
540
+ }
541
+ if (audit.kind === "unknown_gem") {
542
+ issues.push(ruleIssue("unknown_gem_class", `CSS class .${className} is not a manifest primitive or section-scoped class.`, artifactPath, {
543
+ class_name: className
544
+ }));
545
+ }
546
+ }
547
+ const cssVariables = [...css.matchAll(/--gem-[a-z0-9-]+/g)].map((match) => match[0]);
548
+ for (const variableName of [...new Set(cssVariables)]) {
549
+ if (isKnownGemCssVariable(variableName, options)) continue;
550
+ issues.push(ruleIssue("unknown_gem_css_variable", `${variableName} is not a canonical Gem token or allowed section-scoped CSS variable.`, artifactPath, {
551
+ value: variableName
552
+ }));
553
+ }
554
+ const declarations = [...css.matchAll(/([a-z-]+)\s*:\s*([^;{}]+);/gi)];
555
+ for (const [, property, value] of declarations) {
556
+ if (!TOKENIZED_PROPERTIES.includes(property)) continue;
557
+ if (isAllowedUntokenizedValue(property, value)) continue;
558
+ if (!value.includes("var(--gem-") && hasRawVisualValue(value)) {
559
+ issues.push(ruleIssue("untokenized_visual_value", `${property}: ${value.trim()} should prefer semantic --gem-* tokens.`, artifactPath, {
560
+ property,
561
+ value: value.trim()
562
+ }));
563
+ }
564
+ }
565
+ return result(issues);
566
+ }
567
+ function validateGemFrameworkMarkup(markup, artifactPath, options = {}) {
568
+ const issues = validateSharedSafety(markup, artifactPath);
569
+ if (/\bgs-/.test(markup)) {
570
+ issues.push(ruleIssue("legacy_gs_class", "Gem Framework markup must not use legacy gs-* classes.", artifactPath));
571
+ }
572
+ const classAttributes = [...markup.matchAll(/\bclass(?:Name)?=(["'`])([^"'`]+)\1/g)].map((match) => match[2]);
573
+ for (const classAttribute of classAttributes) {
574
+ if (classAttribute.includes("{{") || classAttribute.includes("{%") || classAttribute.includes("${")) continue;
575
+ for (const className of classAttribute.split(/\s+/).filter(Boolean)) {
576
+ const audit = auditGemFrameworkClassName(className, options);
577
+ if (audit.kind === "non_gem") {
578
+ issues.push(ruleIssue("non_gem_class", `Markup class ${className} must use the gem-* namespace.`, artifactPath, {
579
+ class_name: className
580
+ }));
581
+ }
582
+ if (audit.kind === "unknown_gem") {
583
+ issues.push(ruleIssue("unknown_gem_class", `Markup class ${className} is not a manifest primitive or section-scoped class.`, artifactPath, {
584
+ class_name: className
585
+ }));
586
+ }
587
+ }
588
+ }
589
+ if (!/data-gem-[a-z0-9-]+=/.test(markup) && /<section\b/i.test(markup)) {
590
+ issues.push(ruleIssue("missing_data_gem_marker", "Section markup should expose a data-gem-* marker for provenance or runtime targeting.", artifactPath));
591
+ }
592
+ return result(issues);
593
+ }
594
+ function validateSharedSafety(content, artifactPath) {
595
+ const issues = [];
596
+ const contentWithoutJsonLd = content.replace(/<script\b[^>]*type=["']application\/ld\+json["'][^>]*>[\s\S]*?<\/script>/gi, "");
597
+ const contentWithoutNamespaces = contentWithoutJsonLd.replace(/https?:\/\/www\.w3\.org\/[^\s"'<>]*/gi, "");
598
+ if (/<script\b/i.test(contentWithoutJsonLd)) {
599
+ issues.push(ruleIssue("inline_script", "Gem Framework artifacts must not include inline script tags.", artifactPath));
600
+ }
601
+ if (/https?:\/\/|\/\/cdn\./i.test(contentWithoutNamespaces)) {
602
+ issues.push(ruleIssue("external_dependency", "Gem Framework artifacts must not include external runtime or CDN dependencies.", artifactPath));
603
+ }
604
+ return issues;
605
+ }
606
+ function isAllowedUntokenizedValue(property, value) {
607
+ const trimmed = value.trim();
608
+ if (property === "color" && ["inherit", "currentColor"].includes(trimmed)) return true;
609
+ if (property === "border" && trimmed === "0") return true;
610
+ if (property.startsWith("margin") && ["0", "auto"].includes(trimmed)) return true;
611
+ if (property.startsWith("padding") && trimmed === "0") return true;
612
+ if ((property === "gap" || property.startsWith("--gem-row-gap")) && trimmed === "0") return true;
613
+ return false;
614
+ }
615
+ function hasRawVisualValue(value) {
616
+ return /#[0-9a-f]{3,8}\b|rgb\(|hsl\(|\b\d+(?:\.\d+)?(?:px|rem|em)\b/i.test(value);
617
+ }
618
+ function isKnownGemCssVariable(variableName, options) {
619
+ if (canonicalCssVariableNames.has(variableName)) return true;
620
+ const allowedPrefixes = [
621
+ "--gem-seasonal-",
622
+ // Global style-variant knobs (--gem-style-space/radius/text/weight/stroke): the
623
+ // style-adaptivity family a section reads to adapt airy↔dense / thin↔bold.
624
+ // Global style-variant knobs (--gem-style-space/weight/radius/border/stroke):
625
+ // section-adaptivity tokens a style pack sets on :root; sections consume them via
626
+ // calc(... * var(--gem-style-*, 1)) with safe fallbacks. Canonical, not section-scoped.
627
+ "--gem-style-",
628
+ ...options.allowedCssVariablePrefixes ?? [],
629
+ ...(options.sectionClassPrefixes ?? []).map((prefix) => `--${prefix}-`)
630
+ ];
631
+ return allowedPrefixes.some((prefix) => variableName.startsWith(prefix));
632
+ }
633
+ function getGemFrameworkValidationRule(code) {
634
+ return validationRulesByCode.get(code);
635
+ }
636
+ function auditGemFrameworkClassName(className, options = {}) {
637
+ if ((options.allowedClassNames ?? []).includes(className)) return { class_name: className, kind: "primitive" };
638
+ if (primitiveClassNames.has(className)) return { class_name: className, kind: "primitive" };
639
+ if (className === "gem-el" || className.startsWith("gem-el-")) return { class_name: className, kind: "primitive" };
640
+ if (className.startsWith("section-gem-")) return { class_name: className, kind: "section_scoped" };
641
+ if (!className.startsWith("gem-")) return { class_name: className, kind: "non_gem" };
642
+ if (!options.enforceKnownClasses) return { class_name: className, kind: "section_scoped" };
643
+ const sectionClassPrefixes = options.sectionClassPrefixes ?? [];
644
+ if (sectionClassPrefixes.some((prefix) => isSectionScopedClassName(className, prefix))) {
645
+ return { class_name: className, kind: "section_scoped" };
646
+ }
647
+ return { class_name: className, kind: "unknown_gem" };
648
+ }
649
+ function isSectionScopedClassName(className, prefix) {
650
+ return className === prefix || className.startsWith(`${prefix}__`) || className.startsWith(`${prefix}--`);
651
+ }
652
+ function ruleIssue(code, message, artifactPath, metadata = {}) {
653
+ const rule = getGemFrameworkValidationRule(code);
654
+ if (!rule) throw new Error(`Gem Framework validation rule is missing from the manifest: ${code}`);
655
+ return {
656
+ severity: rule.severity,
657
+ code,
658
+ message,
659
+ artifact_path: artifactPath,
660
+ ...metadata
661
+ };
662
+ }
663
+ function result(issues) {
664
+ return {
665
+ valid: !issues.some((item) => item.severity === "error"),
666
+ issues
667
+ };
668
+ }
669
+
670
+ // ../../packages/gem-framework/src/preview-document.ts
671
+ var PREVIEW_MEDIA_CSS = [
672
+ 'body[data-gem-preview-page] :is([class*="__placeholder"], .gem-media:empty, .gem-product-card__media) {',
673
+ " background-color: color-mix(in srgb, var(--gem-color-accent) 9%, var(--gem-color-surface, var(--gem-color-background))) !important;",
674
+ // Two soft, percentage-sized colour pools (one accent, one neutral) over a base
675
+ // diagonal wash — reads as abstract brand art and scales to any tile, so even large
676
+ // empty heroes feel designed rather than blank.
677
+ " background-image:",
678
+ // Soft lit centre (studio product-shot backdrop) over two ambient colour pools and a
679
+ // base diagonal wash — reads as a styled set rather than a blank box, and scales to any tile.
680
+ " radial-gradient(58% 48% at 50% 40%, color-mix(in srgb, var(--gem-color-surface, var(--gem-color-background)) 78%, #fff) 0%, transparent 72%),",
681
+ " radial-gradient(72% 60% at 22% 26%, color-mix(in srgb, var(--gem-color-accent) 34%, var(--gem-color-surface, var(--gem-color-background))) 0%, transparent 70%),",
682
+ " radial-gradient(68% 70% at 84% 82%, color-mix(in srgb, var(--gem-color-text) 14%, var(--gem-color-surface, var(--gem-color-background))) 0%, transparent 66%),",
683
+ " linear-gradient(135deg, color-mix(in srgb, var(--gem-color-accent) 16%, var(--gem-color-surface, var(--gem-color-background))), color-mix(in srgb, var(--gem-color-accent) 6%, var(--gem-color-surface, var(--gem-color-background)))) !important;",
684
+ "}",
685
+ // Niche product-tile motif (injected line-art): centred, soft, in a palette-derived ink.
686
+ // Every empty media box (hero/collage/video/card) is position:relative + inked so the
687
+ // fallback motif (previewMediaImg) centres and reads as "image here" — never dead white space.
688
+ 'body[data-gem-preview-page] [class*="__placeholder"] { position: relative; min-height: 140px; color: color-mix(in srgb, var(--gem-color-accent) 55%, var(--gem-color-text)); }',
689
+ "body[data-gem-preview-page] .gem-product-card__media { position: relative; color: color-mix(in srgb, var(--gem-color-accent) 55%, var(--gem-color-text)); }",
690
+ "body[data-gem-preview-page] .gem-pv-motif { position: absolute; top: 50%; left: 50%; width: clamp(44px, 13%, 132px); height: auto; aspect-ratio: 1; transform: translate(-50%, -50%); opacity: 0.5; }",
691
+ // Real reference photo (preview-only) covers the whole tile when present.
692
+ "body[data-gem-preview-page] .gem-pv-photo { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block; }",
693
+ // Hero overlay text readability (sketch-only): light hero copy on a light product/hero image
694
+ // renders invisible — a dark text-shadow keeps the heading/CTA legible on ANY image or variant,
695
+ // so the sketch's hero reads as a real hero (headline + CTA) rather than a bare image.
696
+ 'body[data-gem-preview-page] :is([class*="gem-hero"], [data-gem-section-archetype^="hero"]) :is(.gem-section__heading, .gem-section__body, .gem-section__eyebrow) { text-shadow: 0 2px 18px rgba(0, 0, 0, 0.6), 0 1px 4px rgba(0, 0, 0, 0.55); }',
697
+ 'body[data-gem-preview-page] :is([class*="gem-hero"], [data-gem-section-archetype^="hero"]) .gem-button--primary { text-shadow: 0 1px 6px rgba(0, 0, 0, 0.35); }'
698
+ ].join("\n");
699
+ var CHROME_VARIANT_CSS = `
700
+ .gem-shell-header__inner{display:flex;align-items:center;gap:24px}
701
+ /* The shell header markup carries Dawn's header--middle-left grid classes too; force the
702
+ logo to size to its content (the grid otherwise pins it to a narrow column + overflow:hidden,
703
+ clipping the brand to "Baseline S..."). */
704
+ [data-gem-shell-header] .gem-shell-header__logo{flex:0 0 auto;width:auto;max-width:none;min-width:0;overflow:visible;text-decoration:none}
705
+ [data-gem-shell-header] .gem-shell-header__logo .h2{margin:0;font-size:1.25rem;line-height:1.1;white-space:nowrap;overflow:visible;text-overflow:clip}
706
+ .gem-shell-header__nav{display:flex;align-items:center;gap:18px;flex-wrap:wrap}
707
+ .gem-shell-header__actions{display:flex;align-items:center;gap:16px;margin-left:auto}
708
+ .gem-shell-header__action{display:inline-flex;align-items:center;justify-content:center;position:relative;color:inherit;text-decoration:none}
709
+ .gem-shell-header__icon{display:block;width:20px;height:20px}
710
+ .gem-shell-header__action--cart .gem-shell-header__cart-count{position:absolute;top:-7px;right:-9px;min-width:16px;height:16px;padding:0 4px;border-radius:999px;background:#1f1e1d;color:#fff;font-size:10px;font-weight:600;line-height:16px;text-align:center;box-sizing:border-box}
711
+ .gem-shell-header__search-input{min-width:200px;padding:8px 12px}
712
+ /* editorial: centered logo with the nav on a row beneath it */
713
+ .gem-shell-header--editorial .gem-shell-header__inner{flex-direction:column;align-items:center;gap:10px;text-align:center}
714
+ .gem-shell-header--editorial .gem-shell-header__logo{order:1}
715
+ .gem-shell-header--editorial .gem-shell-header__actions{order:2;margin-left:0;position:absolute;right:24px;top:18px}
716
+ .gem-shell-header--editorial .gem-shell-header__nav{order:3;justify-content:center}
717
+ /* search-led: prominent search field takes the centre, actions stay right */
718
+ .gem-shell-header--search-led .gem-shell-header__nav{order:3;flex-basis:100%;justify-content:center}
719
+ .gem-shell-header--search-led .gem-shell-header__search{flex:1;display:flex}
720
+ .gem-shell-header--search-led .gem-shell-header__search-input{width:100%}
721
+ /* mega-menu: roomier nav with hover affordance */
722
+ .gem-shell-header__nav--mega{gap:26px;font-weight:600}
723
+ .gem-shell-header__nav--mega a::after{content:"";display:inline-block;width:6px;height:6px;margin-left:6px;border-right:1.5px solid currentColor;border-bottom:1.5px solid currentColor;transform:rotate(45deg);vertical-align:middle;opacity:.55}
724
+ /* minimal: tighter, lighter nav */
725
+ .gem-shell-header--minimal .gem-shell-header__nav{gap:14px;font-size:.92em}
726
+ /* conversion: dense commerce actions */
727
+ .gem-shell-header--conversion .gem-shell-header__actions{gap:18px;font-weight:600}
728
+ /* footer variants */
729
+ .gem-shell-footer__trust{display:flex;flex-wrap:wrap;gap:18px;justify-content:center;margin:0 0 8px}
730
+ .gem-shell-footer__trust-item{font-size:.86em;opacity:.85}
731
+ .gem-shell-footer__social{display:flex;gap:14px;justify-content:center;margin:18px 0 0;padding:0}
732
+ .gem-shell-footer__social-link{display:inline-flex;align-items:center;justify-content:center;width:36px;height:36px;border-radius:999px;color:inherit;opacity:.9;text-decoration:none}
733
+ .gem-shell-footer__social-link:hover{opacity:1}
734
+ .gem-shell-footer__social-link svg{display:block;width:20px;height:20px}
735
+ /* bottom bar: copyright left, payment icons right (Dawn footer DNA) */
736
+ .gem-shell-footer__bottom{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:16px;margin-top:24px;padding-top:18px;border-top:1px solid rgba(18,18,18,.12)}
737
+ .gem-shell-footer__payment{display:flex;flex-wrap:wrap;gap:6px;margin:0;padding:0}
738
+ .gem-shell-footer__payment-icon{display:inline-flex;align-items:center;justify-content:center;width:40px;height:26px;border-radius:4px;background:#fff;border:1px solid rgba(18,18,18,.14);box-shadow:0 1px 2px rgba(0,0,0,.06);color:#555;font-size:8px;font-weight:700;letter-spacing:-.02em;text-transform:uppercase}
739
+ .gem-shell-footer--newsletter-first .gem-shell-footer__newsletter{order:-1;text-align:center;padding-bottom:20px}
740
+ .gem-shell-footer--trust-heavy .gem-shell-footer__trust{order:-1;font-weight:600}
741
+ .gem-shell-footer--story-led .gem-shell-footer__grid{text-align:center}
742
+ .gem-shell-footer--brand-rich .gem-shell-footer__social{justify-content:flex-start}
743
+ `.trim();
744
+
745
+ // ../../packages/gem-sections/src/section-contract.ts
746
+ var LAYOUT_ATTRS = [
747
+ { attr: "content-alignment", setting: "gem_content_alignment", knob: "content_alignment" },
748
+ { attr: "text-position", setting: "gem_text_position", knob: "text_position" },
749
+ { attr: "media-position", setting: "gem_media_position", knob: "media_position" },
750
+ { attr: "content-spacing", setting: "gem_content_spacing", knob: "content_spacing" },
751
+ { attr: "content-width", setting: "gem_content_width", knob: "content_width" },
752
+ { attr: "section-height", setting: "gem_section_height", knob: "section_height" },
753
+ { attr: "grid-columns", setting: "gem_grid_columns", knob: "grid_columns" },
754
+ { attr: "section-background", setting: "gem_section_background", knob: "background" },
755
+ { attr: "price-color", setting: "gem_price_color", knob: "price_color" },
756
+ // Written from BOTH sides: the style pack sets it, and set_layout gained a `text_weight` knob.
757
+ { attr: "text-weight", setting: "gem_text_weight", knob: "text_weight" },
758
+ // Scroll-entrance: emitted only when the knob carries a value.
759
+ { attr: "reveal", setting: "gem_reveal", knob: "reveal" },
760
+ // Shaped edges: same emit-only-when-set rule.
761
+ { attr: "divider-top", setting: "gem_divider_top", knob: "divider_top" },
762
+ { attr: "divider-bottom", setting: "gem_divider_bottom", knob: "divider_bottom" },
763
+ // Boxes the section's content: styles .gem-section__inner, so the root attribute alone is not enough.
764
+ { attr: "card", setting: "gem_card", knob: "card", requires: "__inner" }
765
+ ];
766
+ function sectionLibraryIdOf(tsx) {
767
+ return /data-gem-section-library-id="([^"]+)"/.exec(tsx)?.[1] ?? "";
768
+ }
769
+ function layoutKnobCheck(tsx, knobExempt = /* @__PURE__ */ new Set()) {
770
+ const id = sectionLibraryIdOf(tsx);
771
+ const missing = LAYOUT_ATTRS.filter((a) => {
772
+ if (knobExempt.has(`${id}::${a.attr}`)) return false;
773
+ if (!tsx.includes(`data-gem-${a.attr}`)) return true;
774
+ return Boolean(a.requires) && !tsx.includes(a.requires);
775
+ }).map((a) => a.attr);
776
+ const tag = /<section[\s\S]*?>/.exec(tsx)?.[0] ?? "";
777
+ const emitted = [...tag.matchAll(/(data-gem-[a-z-]+)=/g)].map((m) => m[1]);
778
+ const dupes = [...new Set(emitted.filter((a, i) => emitted.indexOf(a) !== i))];
779
+ return { id, missing, dupes };
780
+ }
781
+ function slotsReadBy(raw) {
782
+ const source = raw.replace(/\/\*[\s\S]*?\*\//g, " ").replace(/(^|[^:])\/\/.*$/gm, "$1 ");
783
+ const names = /* @__PURE__ */ new Set(["slots"]);
784
+ for (const m of source.matchAll(/(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:section\.)?content_slots\b/g)) names.add(m[1]);
785
+ const alt = [...names].map((n) => n.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
786
+ const keys = /* @__PURE__ */ new Set();
787
+ for (const m of source.matchAll(new RegExp(`(?:${alt})\\.([A-Za-z_][A-Za-z0-9_]*)`, "g"))) keys.add(m[1]);
788
+ for (const m of source.matchAll(new RegExp(`(?:${alt})\\[["']([^"']+)["']\\]`, "g"))) keys.add(m[1]);
789
+ const prefixes = [];
790
+ for (const m of source.matchAll(new RegExp("(?:" + alt + ")\\[`([^`]*)`\\]", "g"))) {
791
+ const literal = m[1].split("${")[0];
792
+ if (literal.length >= 3) prefixes.push(literal);
793
+ }
794
+ return { keys, prefixes };
795
+ }
796
+ function slotSourceFromElementTree(treeJson) {
797
+ return [...treeJson.matchAll(/"\$slot"\s*:\s*"([A-Za-z0-9_]+)"/g)].map((m) => `slots.${m[1]}`).join("\n");
798
+ }
799
+ function slotContractDrift(source, manifest) {
800
+ const declaredSlots = new Set(manifest.content_slots ?? []);
801
+ const declaredSettings = new Set(manifest.settings ?? []);
802
+ const read = slotsReadBy(source);
803
+ const isRead = (key) => read.keys.has(key) || read.prefixes.some((p) => key.startsWith(p));
804
+ return {
805
+ ghost: [...declaredSlots].filter((s) => !isRead(s)).sort(),
806
+ undeclared: [...read.keys].filter((s) => !declaredSlots.has(s) && !declaredSettings.has(s)).sort(),
807
+ settingsExcused: [...read.keys].filter((s) => !declaredSlots.has(s) && declaredSettings.has(s)).sort()
808
+ };
809
+ }
810
+ var SHARED_FRAMEWORK_PREFIXES = [
811
+ "gem-section",
812
+ "gem-button",
813
+ "gem-card",
814
+ "gem-price",
815
+ "gem-badge",
816
+ "gem-prose",
817
+ "gem-reveal",
818
+ "gem-countdown"
819
+ ];
820
+ var SECTION_CLASS_RE = /^gem-[a-z0-9]+(?:-[a-z0-9]+)*(?:__[a-z0-9]+(?:-[a-z0-9]+)*)?(?:--[a-z0-9]+(?:-[a-z0-9]+)*)?$/;
821
+ function classesStyledBy(css) {
822
+ return new Set((css.match(/\.(-?[_a-zA-Z][\w-]*)/g) ?? []).map((s) => s.slice(1)));
823
+ }
824
+ function sectionClassesIn(tsx) {
825
+ const out = /* @__PURE__ */ new Set();
826
+ const src = tsx.replace(/\/\*[\s\S]*?\*\//g, " ").replace(/^\s*\/\/.*$/gm, " ");
827
+ const re = /"([^"\n]*)"|'([^'\n]*)'|`([^`]*)`/g;
828
+ let m;
829
+ while ((m = re.exec(src)) !== null) {
830
+ for (const tok of (m[1] ?? m[2] ?? m[3] ?? "").split(/\s+/)) {
831
+ if (!tok.startsWith("gem-") || tok.includes("$") || tok.includes("{")) continue;
832
+ if (SECTION_CLASS_RE.test(tok)) out.add(tok);
833
+ }
834
+ }
835
+ return out;
836
+ }
837
+ function isFrameworkOwnedClass(cls, frameworkClasses) {
838
+ if (frameworkClasses.has(cls)) return true;
839
+ return SHARED_FRAMEWORK_PREFIXES.some((p) => cls === p || cls.startsWith(`${p}-`) || cls.startsWith(`${p}_`));
840
+ }
841
+ function cssPairingOrphans(tsx, css, frameworkClasses, archetype) {
842
+ const arch = archetype ?? /data-gem-section-archetype="([a-z0-9-]+)"/.exec(tsx)?.[1];
843
+ const ownBlock = arch ? `gem-${arch}` : null;
844
+ const emitted = [...sectionClassesIn(tsx)].filter((c) => c !== ownBlock && !isFrameworkOwnedClass(c, frameworkClasses));
845
+ const styled = classesStyledBy(css);
846
+ return { emitted: emitted.length, orphans: emitted.filter((c) => !styled.has(c)).sort() };
847
+ }
848
+ var CLAIM_PLAN_KEY = /^ratings?\d*$|ratingvalue|ratingscore|ratingscount|ratingcount|reviewscount|reviewcount|customerscount|customercount|unitssold|soldcount|testimonialcount|starrating/i;
849
+ var planKey = (key) => CLAIM_PLAN_KEY.test(key.replace(/_/g, ""));
850
+ var CLAIM_KEY = /(rating|review|customer|sold|purchas|verified|score|star|testimonial|backer|subscriber)/i;
851
+ var INVENTED_PERSON = /"[A-Z][a-z]+ [A-Z]\."|\b(Verified (Buyer|Customer)|Happy Customer)\b/;
852
+ var PROMISE = /[$£€]\s?\d|\b\d[\d.,]*\s?(?:USD|EUR|GBP)\b|\bfree,?(?:\s+(?:standard|express|expedited|worldwide|international|domestic|nationwide|ground|priority|tracked|insured|fast|economy|local|same[- ]day|next[- ]day|two[- ]day|overnight|\d+[- ](?:day|hour)|u\.?s\.?|uk|us|eu|au|ca|and|&),?){0,3}\s+(?:shipping|delivery|returns?|exchanges?)\b|\b(?:easy|hassle[- ]free|no[- ]hassle|risk[- ]free|no[- ]questions?[- ]asked)\s+(?:returns?|exchanges?)\b(?!\s+(?:faq|policy|information|info|page|portal)\b)|\b(?:returns?|exchanges?|refunds?)(?:\s+[\w${}.-]+){0,3}?\s+(?:(?:are|is)\s+)?(?:accepted|issued|processed)?\s*(?:within|in)\s+\d+\s*(?:business\s+)?(?:days?|weeks?)\b|\b(?:returns?|exchanges?|shipping|delivery)\s+(?:are|is)\s+(?:always\s+)?free\b|\bships?\s+free\b|\bcomplimentary\s+(?:shipping|delivery|returns?)\b|\b\d+\s+days\s+to\s+(?:return|exchange)\b|\b(?:processed|dispatched?|shipped)\s+within\s+\d|\bmoney[- ]back\b|\b\d+[- ]day (?:guarantee|returns?|exchanges?|trial)\b|\bdispatched? within\b|\bships? within\b|\b(?:lifetime|\d+[- ](?:year|month)s?(?:[- ]limited)?)[- ]warranty\b|\bwarranty:?\s*\d+\s*(?:years?|months?)\b|\b(?:delivered|ships?|arrives?) (?:in|within) \d+\s*(?:-\s*\d+\s*)?(?:h|hrs?|hours?|days?|business days|weeks?)\b|\bfull refund(?!\s+polic)|\brefunds? you\b|\btry (?:it )?(?:free )?(?:for )?\d+ (?:days|nights)\b|\b\d+[- ]nights? (?:trial|risk[- ]free)\b|(?<!\b(?:cannot|can\\?['’]t|can\s+not|won\\?['’]t|not|non|no|unable\s+to|need\s+to)[\s-]+(?:be\s+)?(?:\w+\s+(?:or|and)\s+)?)\bcancel\w*\b(?!\s+(?:fees?|charges?|penalt)|\s+(?:the\s+)?filters?\b)(?![^.!?\n]{0,40}\b(?:call|contact|message|email|reach|chat)\b)[^.!?\n]{0,40}\b(?:any ?-?time|at any time|whenever)\b|\bno\s+cancel\w*\s+(?:fees?|charges?|penalt\w*)\b|(?<!\b(?:cannot|can\\?['’]t|not|no|never)\s+)\bguarantee\w*\b[^.!?\n,&]{0,20}\b(?:deliver\w*|ship\w*)\b|\b(?:deliver\w*|ship\w*)\b[^.!?\n]{0,20}(?<!\b(?:not|never|no)\s+(?:be\s+)?)\bguaranteed\b(?!\s+[a-z])/i;
853
+ var CLAIM_TEXT = /\b\d[\d,.]*[kKmM]?\s*(\+\s*)?((?:verified|happy|satisfied|loyal)\s+)?(reviews?|ratings?|customers?|shoppers?|buyers?|sold|five[- ]stars?)\b|\b\d[\d,.]*[kKmM]?\s*(\+\s*)?(?:\d|five)[- ]?stars?\s+(?:reviews?|ratings?)\b|\brated\s+\d/i;
854
+ var CLAIM_TEXT_VI = /\d[.,]\d+\s*\/\s*5(?!\p{N})|(?<![\p{L}\p{N}])\d\s*\/\s*5\s*(?:sao|điểm)(?![\p{L}\p{N}])|\d[\d.,]*\s*(?:nghìn|ngàn|triệu|k)?\s*\+?\s*(?:lượt\s+)?(?:đánh giá|nhận xét|khách hàng|khách|người mua|người dùng|người đã mua|sao|đơn hàng)(?![\p{L}\p{N}])|(?<![\p{L}\p{N}])đã bán\s+(?:hơn\s+)?\d|\d[\d.,]*\s*(?:nghìn|ngàn|triệu|k)?\s*\+?\s*đã bán(?![\p{L}\p{N}])|(?<![\p{L}\p{N}])được đánh giá\s+\d|\d(?:[.,]\d{1,2})?\s*[★⭐]|[★⭐]\uFE0F?\s*\d/iu;
855
+ var VI_START = "(?<![\\p{L}\\p{N}])";
856
+ var VI_END = "(?![\\p{L}\\p{N}])";
857
+ var VI_DURATION = `\\d+(?:[.,]\\d+)?\\s*(?:(?:[\u2013\u2014~-]|\u0111\u1EBFn|t\u1EDBi|ho\u1EB7c)\\s*\\d+\\s*)?(?:h(?:\\d{2})?|gi\u1EDD|ti\u1EBFng|ph\xFAt|ng\xE0y|tu\u1EA7n|th\xE1ng)${VI_END}`;
858
+ var VI_DELIVERY_SPAN = `(?:\\d+(?:[.,]\\d+)?\\s*(?:h|gi\u1EDD|ti\u1EBFng|ph\xFAt|ng\xE0y|tu\u1EA7n)|\\d+\\s*(?:[\u2013\u2014~-]|\u0111\u1EBFn|t\u1EDBi)\\s*\\d+\\s*(?:ng\xE0y|tu\u1EA7n))${VI_END}`;
859
+ var PROMISE_VI_PARTS = {
860
+ freeShipping: `${VI_START}mi\u1EC5n ph\xED\\s+(?:v\u1EADn chuy\u1EC3n|giao h\xE0ng|ship|\u0111\u1ED5i tr\u1EA3|\u0111\u1ED5i h\xE0ng|tr\u1EA3 h\xE0ng)${VI_END}`,
861
+ shippingFree: `${VI_START}(?:v\u1EADn chuy\u1EC3n|giao h\xE0ng|\u0111\u1ED5i tr\u1EA3|\u0111\u1ED5i h\xE0ng|tr\u1EA3 h\xE0ng)\\s+mi\u1EC5n ph\xED${VI_END}`,
862
+ freeship: `${VI_START}free\\s?ship${VI_END}`,
863
+ price: `\\d[\\d.,]*\\s?(?:\u0111|\u20AB|vn\u0111|vnd|\u0111\u1ED3ng(?!\\s*h\u1ED3))${VI_END}`,
864
+ daysThenPolicy: `\\d+\\s*ng\xE0y\\s+(?:\u0111\u1ED5i tr\u1EA3|\u0111\u1ED5i h\xE0ng|ho\xE0n ti\u1EC1n|d\xF9ng th\u1EED|tr\u1EA3 h\xE0ng)${VI_END}`,
865
+ policyWindow: `${VI_START}(?:ho\xE0n ti\u1EC1n|\u0111\u1ED5i tr\u1EA3|\u0111\u1ED5i h\xE0ng|tr\u1EA3 h\xE0ng)\\s+(?:mi\u1EC5n ph\xED\\s+)?(?:trong\\s+(?:v\xF2ng\\s+)?)?${VI_DURATION}`,
866
+ refundPercent: `${VI_START}ho\xE0n ti\u1EC1n\\s+\\d+\\s*%`,
867
+ // Giới từ + số là đủ (như 0.3.5), trừ khi số đó đếm bước/lần/món ("Nhận hàng trong 3 bước").
868
+ deliveryWithin: `${VI_START}(?:giao|nh\u1EADn)(?:\\s+h\xE0ng)?(?:\\s+nhanh)?\\s+(?:trong v\xF2ng|trong|sau|ch\u1EC9)\\s+\\d(?!\\d*\\s*(?:b\u01B0\u1EDBc|l\u1EA7n|c\xE1ch|m\xF3n|s\u1EA3n ph\u1EA9m|m\u1EE5c)${VI_END})`,
869
+ // Không giới từ thì chỉ "giao": "Nhận 10h sáng" là giờ hẹn. Loại khung giờ và nhịp ("7h-21h", "3 ngày/tuần", "một lần").
870
+ deliveryTime: `${VI_START}giao(?:\\s+h\xE0ng)?(?:\\s+nhanh)?\\s*:?\\s+${VI_DELIVERY_SPAN}(?!\\s*(?:[-\u2013\u2014~/]|\u0111\u1EBFn|t\u1EDBi|s\xE1ng|chi\u1EC1u|t\u1ED1i|tr\u01B0a|\u0111\xEAm|m\u1ED9t l\u1EA7n|1 l\u1EA7n|m\u1ED7i))`,
871
+ trial: `${VI_START}(?:d\xF9ng th\u1EED|tr\u1EA3i nghi\u1EC7m)\\s+(?:mi\u1EC5n ph\xED\\s+)?(?:trong\\s+(?:v\xF2ng\\s+)?)?\\d+\\s*(?:ng\xE0y|\u0111\xEAm|tu\u1EA7n|th\xE1ng)${VI_END}`,
872
+ warranty: `${VI_START}b\u1EA3o h\xE0nh(?:\\s+(?:ch\xEDnh h\xE3ng|1 \u0111\u1ED5i 1|\u0111\u1ED5i m\u1EDBi|l\xEAn (?:\u0111\u1EBFn|t\u1EDBi)|t\u1EDBi|\u0111\u1EBFn|trong(?: v\xF2ng)?))*\\s+(?:tr\u1ECDn \u0111\u1EDDi|\\d+(?:[.,]\\d+)?\\s*(?:ng\xE0y|th\xE1ng|n\u0103m))${VI_END}`,
873
+ // Chính sách huỷ gói và cam kết giao hàng, theo KHOẢNG CÁCH chứ không theo một cụm cố định: "Huỷ đăng ký bất cứ
874
+ // khi nào", "Cam kết giao hàng đúng hẹn" (review gemsite #694 và gemtheme #604, 29/09/2026).
875
+ // "Không thể / không được / không hỗ trợ / không cho phép (đổi hoặc) huỷ bất cứ lúc nào" là lời từ chối (review
876
+ // gemboss #3499, gemtheme #605). "Không ràng buộc, huỷ bất cứ lúc nào" vẫn là lời hứa.
877
+ // "Huỷ (bỏ) lọc/chọn" là nhãn bộ lọc; "huỷ đăng ký" là bỏ nhận tin, như "Unsubscribe anytime" bên tiếng Anh (huỷ GÓI
878
+ // mới là chính sách); và một động từ liên hệ ở giữa ("Hủy đơn, gọi chúng tôi bất cứ lúc nào") nói "bất cứ lúc nào" về
879
+ // việc gọi, không về việc huỷ (review gemfactory #68, gemtheme #606). Dấu phẩy thì KHÔNG cắt: "Hủy miễn phí, bất cứ
880
+ // lúc nào" vẫn là lời hứa.
881
+ cancelAnytime: `${VI_START}(?<!kh\xF4ng(?:\\s+(?:th\u1EC3|\u0111\u01B0\u1EE3c|h\u1ED7 tr\u1EE3|cho ph\xE9p))?(?:\\s+(?:\u0111\u1ED5i|tr\u1EA3)\\s+(?:ho\u1EB7c|hay))?\\s+)(?:hu\u1EF7|h\u1EE7y)(?!\\s+(?:b\u1ECF\\s+)?(?:l\u1ECDc|ch\u1ECDn|t\xECm)|\\s+\u0111\u0103ng k\xFD(?!\\s+g\xF3i))(?![^.!?\\n]{0,20}(?:g\u1ECDi|li\xEAn h\u1EC7|nh\u1EAFn|h\u1ECFi|chat))[^.!?\\n]{0,20}b\u1EA5t (?:c\u1EE9|k\u1EF3|k\xEC) (?:l\xFAc|khi) n\xE0o${VI_END}`,
882
+ // Loại TRỪ các từ ghép không phải giao hàng ("giao dịch", "giao diện", "giao tiếp"…), đừng liệt kê từ được phép:
883
+ // danh sách cho phép làm lọt "Cam kết giao sớm", "Cam kết giao toàn quốc" (review gemtheme #605).
884
+ guaranteedDelivery: `${VI_START}(?:\u0111\u1EA3m b\u1EA3o|b\u1EA3o \u0111\u1EA3m|cam k\u1EBFt)\\s+(?:giao(?!\\s*(?:d\u1ECBch|di\u1EC7n|ti\u1EBFp|l\u01B0u|th\xF4ng|ban|th\u1EEBa|h\u1EEFu|k\xE8o|ca|tranh|thoa|ph\xF3|h\u1EB9n|ho\xE0|h\xF2a))|ship|v\u1EADn chuy\u1EC3n)${VI_END}|${VI_START}(?:giao h\xE0ng|giao nhanh|ship nhanh|ship h\xE0ng|v\u1EADn chuy\u1EC3n)\\s+(?:\u0111\u1EA3m b\u1EA3o|b\u1EA3o \u0111\u1EA3m|cam k\u1EBFt)${VI_END}`,
885
+ // "được hoàn tiền" là lời hứa ("Khách hàng được hoàn tiền"), trừ "để được" (câu hỏi cách làm) và "không được" (từ chối).
886
+ refundCommitment: `${VI_START}(?:cam k\u1EBFt|b\u1EA3o \u0111\u1EA3m|\u0111\u1EA3m b\u1EA3o|s\u1EBD|(?<!(?:\u0111\u1EC3|kh\xF4ng)\\s)\u0111\u01B0\u1EE3c)\\s+ho\xE0n ti\u1EC1n${VI_END}`,
887
+ refundCondition: `${VI_START}ho\xE0n ti\u1EC1n\\s+(?:n\u1EBFu|khi(?!\\s+n\xE0o)|ngay|to\xE0n b\u1ED9|\u0111\u1EA7y \u0111\u1EE7|100)(?![\\p{L}])`,
888
+ // "Không hợp, Lumen hoàn tiền". "không hợp lệ" là "invalid", và "… thì không hoàn tiền" là lời từ chối.
889
+ // "Nếu muốn/cần hoàn tiền" là hướng dẫn, không phải lời hứa. Một "không" ngay trước ("không được hoàn tiền") là từ chối.
890
+ refundIfUnhappy: `${VI_START}(?:kh\xF4ng\\s+(?:h\u1EE3p(?!\\s+l\u1EC7)|h\xE0i l\xF2ng|v\u1EEBa \xFD|\u01B0ng \xFD|\u01B0ng)${VI_END}|n\u1EBFu(?!\\s+(?:b\u1EA1n\\s+|qu\xFD kh\xE1ch\\s+)?(?:mu\u1ED1n|c\u1EA7n)))[^.!?\\n]{0,40}(?<!kh\xF4ng(?:\\s+\\p{L}+)?\\s)ho\xE0n ti\u1EC1n${VI_END}`,
891
+ // "…nhắn cho Lumen để nhận lại toàn bộ tiền", "Hoàn lại 100% tiền": lời hứa hoàn tiền không có chữ "hoàn tiền"
892
+ // liền nhau, lọt mọi mẫu trên (thân cam kết của gemfactory guarantee-band, 29/09/2026). "Không (được/thể) …" là từ
893
+ // chối; "tiền thừa/thối/lẻ" là tiền trả lại khi thanh toán.
894
+ moneyBack: `${VI_START}(?<!kh\xF4ng(?:\\s+(?:\u0111\u01B0\u1EE3c|th\u1EC3))?\\s)(?:nh\u1EADn|ho\xE0n|tr\u1EA3|g\u1EEDi)\\s+l\u1EA1i\\s+(?:(?:to\xE0n b\u1ED9|\u0111\u1EE7|\u0111\u1EA7y \u0111\u1EE7|100\\s*%)\\s+)?(?:s\u1ED1\\s+)?ti\u1EC1n(?!\\s+(?:th\u1EEBa|th\u1ED1i|l\u1EBB))${VI_END}`
895
+ };
896
+ var PROMISE_VI = new RegExp(Object.values(PROMISE_VI_PARTS).join("|"), "iu");
897
+ var INVENTED_PERSON_VI = /^\s*[-–—]?\s*(?:(?:Chị|Anh|Cô|Chú|Bác|Em|Bạn|Ông|Bà|Nguyễn|Trần|Lê|Phạm|Hoàng|Huỳnh|Phan|Vũ|Võ|Đặng|Bùi|Đỗ|Ngô|Dương|Đinh|Trương|Đoàn|Lương|Trịnh|Lý)(?:\s+\p{Lu}\p{Ll}*){1,3}\s*(?:,|\(|\s[-–—·])\s*\S|(?:Nguyễn|Trần|Lê|Phạm|Hoàng|Huỳnh|Phan|Vũ|Võ|Đặng|Bùi|Đỗ|Ngô|Dương|Đinh|Trương|Đoàn|Lương|Trịnh|Lý)\s+(?:Thị|Văn)(?:\s+\p{Lu}\p{Ll}*){1,2}\s*$)/u;
898
+ var PRICE_K_VI = /(?<![\p{L}\p{N}.,])\d{2,3}(?:[.,]\d{3})*\s?k(?![\p{L}\p{N}])/u;
899
+ var DISPLAY_QUANTITY = /(_to_show|_per_page|_per_row|_limit|_max|_visible|_columns?)$/;
900
+ function blankComments(raw) {
901
+ return raw.replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, " ")).replace(/(^|[^:])\/\/.*$/gm, (_m, p) => p);
902
+ }
903
+ function demoValues(src) {
904
+ const out = /* @__PURE__ */ new Map();
905
+ const head = /\bconst\s+DEMO\s*(?::[^=]+)?=\s*\{/.exec(src);
906
+ if (!head) return out;
907
+ let depth = 1;
908
+ let entry = "";
909
+ let entryStart = head.index + head[0].length;
910
+ const flush = (next) => {
911
+ const m = /^\s*["']?([A-Za-z_]\w*)["']?\s*:\s*([\s\S]+?)\s*$/.exec(entry);
912
+ const keyAt = entryStart + (entry.length - entry.trimStart().length);
913
+ if (m) out.set(m[1], { value: m[2], line: src.slice(0, keyAt).split("\n").length });
914
+ entry = "";
915
+ entryStart = next;
916
+ };
917
+ for (let i = entryStart; i < src.length; i++) {
918
+ const c = src[i];
919
+ if (c === '"' || c === "'" || c === "`") {
920
+ let j = i + 1;
921
+ while (j < src.length && src[j] !== c) j += src[j] === "\\" ? 2 : 1;
922
+ entry += src.slice(i, j + 1);
923
+ i = j;
924
+ continue;
925
+ }
926
+ if (c === "{" || c === "[" || c === "(") depth++;
927
+ else if (c === "}" || c === "]" || c === ")") {
928
+ if (--depth === 0) {
929
+ flush(i + 1);
930
+ break;
931
+ }
932
+ } else if (c === "," && depth === 1) {
933
+ flush(i + 1);
934
+ continue;
935
+ }
936
+ entry += c;
937
+ }
938
+ return out;
939
+ }
940
+ var PRICE_ANY = new RegExp(`[$\xA3\u20AC]\\s?\\d|\\b\\d[\\d.,]*\\s?(?:USD|EUR|GBP)\\b|${PROMISE_VI_PARTS.price}`, "iu");
941
+ function isQuestion(text) {
942
+ const t = text.trim();
943
+ if (!/\?$/.test(t) || /[.!?]\s+\S/.test(t)) return false;
944
+ 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);
945
+ }
946
+ function claimIn(raw) {
947
+ const text = raw.normalize("NFC");
948
+ if (CLAIM_TEXT.test(text) || CLAIM_TEXT_VI.test(text)) return "hardcoded claim";
949
+ if (INVENTED_PERSON.test(`"${text}"`) || INVENTED_PERSON_VI.test(text)) return "invented person";
950
+ if (isQuestion(text)) return PRICE_ANY.test(text) || PRICE_K_VI.test(text) ? "promise the merchant never made" : null;
951
+ if (PRICE_K_VI.test(text) && /^\s*\d[\d.,]*\s?k\s*\+?\s*$/.test(text)) return "hardcoded figure";
952
+ if (PROMISE.test(text) || PROMISE_VI.test(text) || PRICE_K_VI.test(text)) return "promise the merchant never made";
953
+ return null;
954
+ }
955
+ function demoInvents(value) {
956
+ const v = value.trim();
957
+ if (/^-?\d/.test(v)) return "number";
958
+ if (/^[{[]/.test(v) && /(?:^|[:[,])\s*-?\d/.test(v.slice(1))) return "number";
959
+ for (const text of stringLiteralsIn(v)) {
960
+ const kind = claimIn(text);
961
+ if (kind) return kind;
962
+ }
963
+ return null;
964
+ }
965
+ var claimNamed = (key) => CLAIM_KEY.test(key) && !DISPLAY_QUANTITY.test(key);
966
+ function stringLiteralsIn(line) {
967
+ return [...line.matchAll(/(["'`])((?:\\.|(?!\1)[^\\])*)\1/g)].map((m) => m[2]);
968
+ }
969
+ function fabricatedProofInSource(raw) {
970
+ const src = blankComments(raw);
971
+ const out = [];
972
+ const demo = demoValues(src);
973
+ const lines = src.split("\n");
974
+ const caught = /* @__PURE__ */ new Set();
975
+ const demoReads = [];
976
+ const DANGLING = /(?:settings|slots|content_slots)\??\.([a-z_]+)\s*(?:\?\?|\|\|)\s*$/i;
977
+ lines.forEach((line, i) => {
978
+ const at = i + 1;
979
+ const slotKeys = [];
980
+ const carried = DANGLING.exec(lines[i - 1] ?? "")?.[1];
981
+ const reads = [...line.matchAll(/(?:settings|slots|content_slots)\??\.([a-z_]+)\s*(?:\?\?|\|\|)\s*([^;,)\]}]+)/gi)].map((m) => [m[1], m[2]]);
982
+ if (carried) reads.push([carried, /^\s*([^;,)\]}]+)/.exec(line)?.[1] ?? ""]);
983
+ for (const [key, rawFallback] of reads) {
984
+ slotKeys.push(key);
985
+ if (!claimNamed(key)) continue;
986
+ const fallback = rawFallback.trim();
987
+ const rest = fallback.replace(/\bDEMO\.\w+/g, "").trim();
988
+ if (/^(undefined|null|false|true|\[\]|\{\}|""|''|``)$/.test(rest)) continue;
989
+ if (!/\d/.test(rest)) continue;
990
+ out.push({ line: at, why: `${key} defaults to ${fallback}`, text: line.trim() });
991
+ }
992
+ for (const m of line.matchAll(/\bDEMO\.([A-Za-z_]\w*)/g)) {
993
+ const entry = demo.get(m[1]);
994
+ if (!entry) continue;
995
+ const kind = demoInvents(entry.value);
996
+ if (!kind) continue;
997
+ const slot = slotKeys.find(claimNamed);
998
+ if (kind === "number" && !claimNamed(m[1]) && !slot) continue;
999
+ demoReads.push({ at, key: m[1], entry, why: `${slot ?? m[1]} defaults to DEMO.${m[1]} = ${entry.value.trim()}`, text: line.trim() });
1000
+ }
1001
+ for (const m of line.matchAll(/(?<!\?\s*)["']?\b([A-Za-z_]\w*)["']?\s*:\s*["'`]?(-?\d(?:[\d.,]*\d)?)/g)) {
1002
+ const [, key, num] = m;
1003
+ if (!planKey(key)) continue;
1004
+ if (/^0(\.0+)?$/.test(num)) continue;
1005
+ caught.add(`${at}\0key\0${key}`);
1006
+ out.push({ line: at, why: `plan literal ${key}: ${num}`, text: line.trim() });
1007
+ }
1008
+ for (const text of stringLiteralsIn(line)) {
1009
+ if (text.includes("${")) continue;
1010
+ const kind = claimIn(text);
1011
+ if (!kind) continue;
1012
+ caught.add(`${at}\0str\0${text}`);
1013
+ out.push({ line: at, why: `${kind} ${JSON.stringify(text)}`, text: line.trim() });
1014
+ }
1015
+ });
1016
+ for (const r of demoReads) {
1017
+ 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}`));
1018
+ if (!onDemoLine) out.push({ line: r.at, why: r.why, text: r.text });
1019
+ }
1020
+ return out.sort((a, b) => a.line - b.line);
1021
+ }
1022
+ function fabricatedProofInSchema(doc) {
1023
+ const out = [];
1024
+ const visit = (node) => {
1025
+ if (Array.isArray(node)) return node.forEach(visit);
1026
+ if (!node || typeof node !== "object") return;
1027
+ const rec = node;
1028
+ const id = typeof rec.id === "string" ? rec.id : null;
1029
+ const zero = /^0(\.0+)?$/.test(String(rec.default ?? "").trim());
1030
+ if (id && CLAIM_KEY.test(id) && !DISPLAY_QUANTITY.test(id) && rec.default != null && !zero && /\d/.test(String(rec.default))) {
1031
+ out.push(`schema default ${id} = ${JSON.stringify(rec.default)}`);
1032
+ }
1033
+ Object.values(rec).forEach(visit);
1034
+ };
1035
+ visit(doc);
1036
+ return out;
1037
+ }
1038
+ var TESTIMONY_NAME = /^(DEMO|STARTER|SAMPLE|PLACEHOLDER|FALLBACK)_.*(QUOTE|REVIEW|TESTIMONIAL|RATING|STARS?|CUSTOMER|PROOF|BUYER)/;
1039
+ var TESTIMONY_PROPERTY = /(?:^|_)(?:quotes?|testimonials?|reviews?|ratings?|stars?|customers?|reviewers?|authors?|buyers?)(?:_|$|\d)/;
1040
+ var LABEL_PROPERTY = /(?:^|_)(?:heading|title|label|eyebrow|kicker|intro|subheading|cta|button|link|caption)(?:_|$)/;
1041
+ function inventedTestimonyIn(raw) {
1042
+ const src = blankComments(raw);
1043
+ const usedAsFallback = (name) => (
1044
+ // The else-branch of ANY ternary, wrapped or not (`bound ? [] : DEMO_REVIEWS` slipped past `[\\w.]+`,
1045
+ // gemfactory #62). `?.` is optional chaining, not a ternary.
1046
+ new RegExp(`\\?(?![.?])[^:;?]*:\\s*${name}\\b|\\?\\?\\s*${name}\\b|\\|\\|\\s*${name}\\b`).test(src)
1047
+ );
1048
+ const out = [];
1049
+ for (const m of src.matchAll(/const\s+([A-Z][A-Z0-9_]*)\s*=/g)) {
1050
+ const name = m[1];
1051
+ if (TESTIMONY_NAME.test(name) && usedAsFallback(name)) out.push(name);
1052
+ }
1053
+ for (const [prop] of demoValues(src)) {
1054
+ if (!TESTIMONY_PROPERTY.test(prop) || LABEL_PROPERTY.test(prop) || DISPLAY_QUANTITY.test(prop)) continue;
1055
+ if (new RegExp(`\\bDEMO\\.${prop}\\b`).test(src)) out.push(`DEMO.${prop}`);
1056
+ }
1057
+ return out;
1058
+ }
1059
+ function demoOutsidePreview(raw) {
1060
+ const src = blankComments(raw);
1061
+ const out = [];
1062
+ const GUARDS = "hasSlots|pageBound|demoMode|hasAuthoredColumnData";
1063
+ for (const m of src.matchAll(/\bconst\s+(hasSlots|pageBound)\s*=\s*([^;]+);/g)) {
1064
+ if (!/^\s*section\.demo\s*!==\s*true\s*\|\|/.test(m[2])) out.push(`\`${m[1]}\` must start \`section.demo !== true || \u2026\` so a live store counts as filled: ${m[0].replace(/\s+/g, " ").trim()}`);
1065
+ }
1066
+ for (const m of src.matchAll(/\bconst\s+demoMode\s*=\s*([^;]+);/g)) {
1067
+ if (!/&&\s*section\.demo\s*===\s*true\b|\bsection\.demo\s*===\s*true\s*&&/.test(m[1])) out.push(`\`demoMode\` must require \`section.demo === true\`: ${m[0].replace(/\s+/g, " ").trim()}`);
1068
+ }
1069
+ const guard = new RegExp(`\\b(?:${GUARDS})\\b|\\bsection\\.demo\\b`);
1070
+ for (const m of src.matchAll(/\bDEMO(?:_[A-Z0-9_]+\b|\.[A-Za-z_])/g)) {
1071
+ const at = m.index ?? 0;
1072
+ const lineStart = src.lastIndexOf("\n", at) + 1;
1073
+ if (/^\s*(?:export\s+)?const\s+DEMO/.test(src.slice(lineStart, at + m[0].length + 40))) continue;
1074
+ let depth = 0;
1075
+ let start = 0;
1076
+ for (let k = at - 1; k >= 0; k--) {
1077
+ const c = src[k];
1078
+ if (c === "}" || c === ")" || c === "]") depth++;
1079
+ else if (c === "{" || c === "(" || c === "[") {
1080
+ if (depth === 0 && c === "{") {
1081
+ start = k;
1082
+ break;
1083
+ }
1084
+ if (depth > 0) depth--;
1085
+ } else if (c === ";" && depth === 0) {
1086
+ start = k;
1087
+ break;
1088
+ }
1089
+ }
1090
+ if (!guard.test(src.slice(start, at))) {
1091
+ const lineNo = src.slice(0, at).split("\n").length;
1092
+ const lineEnd = src.indexOf("\n", at);
1093
+ out.push(`line ${lineNo} reads starter copy with no preview guard: ${src.slice(lineStart, lineEnd < 0 ? void 0 : lineEnd).trim().slice(0, 120)}`);
1094
+ }
1095
+ }
1096
+ return out;
1097
+ }
1098
+
1099
+ // ../../packages/gem-sections/src/section-artifacts.ts
1100
+ function issue(severity, code, message, sectionId, metadata = {}) {
1101
+ return {
1102
+ severity,
1103
+ code,
1104
+ message,
1105
+ section_id: sectionId,
1106
+ ...metadata
1107
+ };
1108
+ }
1109
+ function requiredArtifactPaths(definition) {
1110
+ const shopify = definition.adapters.includes("shopify");
1111
+ const headlessSurface = definition.paths?.element_tree ?? (definition.element_tree ? void 0 : definition.paths?.headless);
1112
+ return [
1113
+ definition.paths?.manifest,
1114
+ ...shopify ? [definition.paths?.shopify, definition.paths?.css, definition.paths?.preview] : [],
1115
+ ...definition.adapters.includes("headless") ? [headlessSurface] : [],
1116
+ ...definition.dependencies.length > 0 && definition.paths?.behavior ? [definition.paths.behavior] : []
1117
+ ].filter((path) => Boolean(path));
1118
+ }
1119
+ function isGemFrameworkArtifactPath(path) {
1120
+ return /\.(css|liquid|html|tsx|js)$/.test(path);
1121
+ }
1122
+ function validateSectionArtifacts(definition, artifacts) {
1123
+ const issues = [];
1124
+ const byPath = new Map(artifacts.map((artifact) => [artifact.path, artifact]));
1125
+ const marker = `data-gem-section-library-id="${definition.id}"`;
1126
+ for (const artifactPath of requiredArtifactPaths(definition)) {
1127
+ if (!byPath.has(artifactPath)) {
1128
+ issues.push(issue("error", "missing_section_artifact", `Missing required section artifact: ${artifactPath}`, definition.id));
1129
+ }
1130
+ }
1131
+ for (const path of [definition.paths?.shopify, definition.paths?.preview].filter(Boolean)) {
1132
+ const artifact = byPath.get(path);
1133
+ if (artifact && !artifact.content.includes(marker)) {
1134
+ issues.push(issue("error", "missing_section_provenance_marker", `${path} is missing ${marker}.`, definition.id));
1135
+ }
1136
+ }
1137
+ for (const artifact of artifacts.filter((item) => isGemFrameworkArtifactPath(item.path))) {
1138
+ const validation = validateGemFrameworkArtifact(artifact, {
1139
+ enforceKnownClasses: true,
1140
+ sectionClassPrefixes: [
1141
+ `gem-${slugify(definition.archetype)}`,
1142
+ ...definition.class_root ? [definition.class_root] : []
1143
+ ]
1144
+ });
1145
+ for (const frameworkIssue of validation.issues) {
1146
+ const severity = definition.status === "stable" && frameworkIssue.severity === "warn" ? "error" : frameworkIssue.severity;
1147
+ issues.push(issue(
1148
+ severity,
1149
+ `gem_framework_${frameworkIssue.code}`,
1150
+ `${artifact.path}: ${frameworkIssue.message}`,
1151
+ definition.id,
1152
+ {
1153
+ artifact_path: frameworkIssue.artifact_path ?? artifact.path,
1154
+ ...frameworkIssue.class_name ? { framework_class_name: frameworkIssue.class_name } : {},
1155
+ ...frameworkIssue.property ? { framework_property: frameworkIssue.property } : {},
1156
+ ...frameworkIssue.value ? { framework_value: frameworkIssue.value } : {}
1157
+ }
1158
+ ));
1159
+ }
1160
+ }
1161
+ return {
1162
+ valid: !issues.some((item) => item.severity === "error"),
1163
+ issues
1164
+ };
1165
+ }
1166
+ export {
1167
+ CLAIM_KEY,
1168
+ CLAIM_PLAN_KEY,
1169
+ CLAIM_TEXT,
1170
+ CLAIM_TEXT_VI,
1171
+ INVENTED_PERSON,
1172
+ INVENTED_PERSON_VI,
1173
+ LAYOUT_ATTRS,
1174
+ PRICE_K_VI,
1175
+ PROMISE,
1176
+ PROMISE_VI,
1177
+ SHARED_FRAMEWORK_PREFIXES,
1178
+ TESTIMONY_NAME,
1179
+ TESTIMONY_PROPERTY,
1180
+ claimIn,
1181
+ classesStyledBy,
1182
+ cssPairingOrphans,
1183
+ demoOutsidePreview,
1184
+ fabricatedProofInSchema,
1185
+ fabricatedProofInSource,
1186
+ inventedTestimonyIn,
1187
+ isFrameworkOwnedClass,
1188
+ isGemFrameworkArtifactPath,
1189
+ issue,
1190
+ layoutKnobCheck,
1191
+ requiredArtifactPaths,
1192
+ sectionClassesIn,
1193
+ sectionLibraryIdOf,
1194
+ slotContractDrift,
1195
+ slotSourceFromElementTree,
1196
+ slotsReadBy,
1197
+ stringLiteralsIn,
1198
+ validateSectionArtifacts
1199
+ };