@tokenoftrust/storefront-runner 2.0.41 → 2.0.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/apps/storefront/integrations/lifecycle-host-asset.mjs +20 -6
  2. package/apps/storefront/package.json +1 -0
  3. package/apps/storefront/platform-reconcile-tenants.json +3 -0
  4. package/apps/storefront/public/favicon.ico +0 -0
  5. package/apps/storefront/public/shared/commerce-chrome.css +147 -16
  6. package/apps/storefront/src/components/AsyncStylesheet.astro +26 -0
  7. package/apps/storefront/src/components/Breadcrumbs.astro +8 -1
  8. package/apps/storefront/src/components/Img.astro +6 -5
  9. package/apps/storefront/src/components/ProductCard.astro +6 -4
  10. package/apps/storefront/src/components/Seo.astro +2 -1
  11. package/apps/storefront/src/components/chrome/SiteFooter.astro +45 -11
  12. package/apps/storefront/src/components/chrome/SiteHeader.astro +83 -13
  13. package/apps/storefront/src/components/commerce/RatingStars.astro +1 -1
  14. package/apps/storefront/src/components/compliance/ComplianceNotice.astro +2 -2
  15. package/apps/storefront/src/components/content/PromoTiles.astro +107 -0
  16. package/apps/storefront/src/components/content/TrustStrip.astro +30 -14
  17. package/apps/storefront/src/components/home/Hero.astro +156 -4
  18. package/apps/storefront/src/components/islands/AgeGate.tsx +135 -26
  19. package/apps/storefront/src/components/islands/ImageGallery.tsx +6 -9
  20. package/apps/storefront/src/components/islands/IsolatedAgeGate.tsx +194 -46
  21. package/apps/storefront/src/components/islands/SearchTypeahead.tsx +10 -4
  22. package/apps/storefront/src/components/marketing/Faq.astro +9 -2
  23. package/apps/storefront/src/config/devTenantSeed.ts +1 -1
  24. package/apps/storefront/src/config/kvTenantResolver.ts +7 -1
  25. package/apps/storefront/src/config/publicationSpaceConfig.ts +12 -1
  26. package/apps/storefront/src/config/publicationSpaceRegistry.ts +5 -0
  27. package/apps/storefront/src/config/tenants.ts +22 -0
  28. package/apps/storefront/src/layouts/Layout.astro +69 -44
  29. package/apps/storefront/src/lib/activity/changeActorAttribution.ts +24 -8
  30. package/apps/storefront/src/lib/activity/recordActivity.ts +15 -0
  31. package/apps/storefront/src/lib/asyncCss.ts +23 -0
  32. package/apps/storefront/src/lib/auth/loginGate.ts +22 -11
  33. package/apps/storefront/src/lib/basePath.ts +70 -9
  34. package/apps/storefront/src/lib/chrome/managedCart.ts +50 -0
  35. package/apps/storefront/src/lib/chrome/model.ts +4 -0
  36. package/apps/storefront/src/lib/chrome/newsletterAgeAttestation.ts +9 -0
  37. package/apps/storefront/src/lib/content/trustStrip.ts +38 -3
  38. package/apps/storefront/src/lib/cspStaticHashes.ts +4 -1
  39. package/apps/storefront/src/lib/diag/capture.ts +161 -0
  40. package/apps/storefront/src/lib/diag/consoleLine.ts +76 -0
  41. package/apps/storefront/src/lib/diag/debugConsole.ts +118 -0
  42. package/apps/storefront/src/lib/diag/debugMode.ts +163 -0
  43. package/apps/storefront/src/lib/diag/debugWindow.ts +111 -0
  44. package/apps/storefront/src/lib/diag/reference.ts +148 -0
  45. package/apps/storefront/src/lib/diag/referenceStore.ts +113 -0
  46. package/apps/storefront/src/lib/diag/report.ts +109 -0
  47. package/apps/storefront/src/lib/diag/session.ts +93 -0
  48. package/apps/storefront/src/lib/featuredProductsPresentation.ts +5 -0
  49. package/apps/storefront/src/lib/heroSlideshow.ts +41 -0
  50. package/apps/storefront/src/lib/homeVisualParity.ts +107 -0
  51. package/apps/storefront/src/lib/imageAttrs.ts +9 -6
  52. package/apps/storefront/src/lib/integration/nonceStamp.ts +23 -1
  53. package/apps/storefront/src/lib/integration/renderIntegrationState.ts +36 -0
  54. package/apps/storefront/src/lib/integration/tenantAnalyticsDeclarations.ts +17 -12
  55. package/apps/storefront/src/lib/jsonScript.ts +13 -0
  56. package/apps/storefront/src/lib/pinnedRouteRewriteBoundary.ts +31 -0
  57. package/apps/storefront/src/lib/previewReload.ts +53 -9
  58. package/apps/storefront/src/lib/rawChrome.ts +148 -19
  59. package/apps/storefront/src/lib/rawMarketingHtml.ts +120 -7
  60. package/apps/storefront/src/lib/storyblok/content-model.ts +46 -3
  61. package/apps/storefront/src/lib/styleGuideThemes.ts +107 -0
  62. package/apps/storefront/src/lib/tenantRouting.ts +15 -4
  63. package/apps/storefront/src/lib/tot/syncState.ts +21 -0
  64. package/apps/storefront/src/lib/tracking/publishSafetyNet.ts +29 -3
  65. package/apps/storefront/src/middleware/controlPlane.ts +9 -5
  66. package/apps/storefront/src/middleware/index.ts +13 -1
  67. package/apps/storefront/src/middleware/renderChain.ts +54 -11
  68. package/apps/storefront/src/middleware/routeBranch.ts +58 -12
  69. package/apps/storefront/src/middleware/staticBundle.ts +37 -3
  70. package/apps/storefront/src/pages/[...slug].astro +6 -2
  71. package/apps/storefront/src/pages/api/request-access.ts +5 -4
  72. package/apps/storefront/src/pages/blog/[slug].astro +11 -1
  73. package/apps/storefront/src/pages/cart.ts +23 -0
  74. package/apps/storefront/src/pages/index.astro +64 -85
  75. package/apps/storefront/src/pages/products/[handle].astro +8 -6
  76. package/apps/storefront/src/pages/sitemap.xml.ts +8 -2
  77. package/apps/storefront/src/pages/style-guide/[tenant]/[theme].astro +14 -25
  78. package/apps/storefront/src/pages/style-guide/[tenant]/chrome/[theme].astro +10 -17
  79. package/apps/storefront/src/pages/style-guide/[tenant]/guide/[theme].astro +9 -16
  80. package/apps/storefront/src/pages/style-guide/[tenant]/index.astro +9 -15
  81. package/apps/storefront/src/pages/the-build/[slug].astro +7 -1
  82. package/apps/storefront/src/styles/fonts.css +7 -0
  83. package/apps/storefront/src/styles/global.css +397 -3
  84. package/package.json +4 -1
  85. package/packages/cli/src/visual-parity.mjs +321 -0
  86. package/packages/public-runtime/package.json +7 -0
  87. package/packages/public-runtime/schemas/publication-space/v1/manifest.schema.json +1 -1
  88. package/packages/public-runtime/schemas/publication-space/v1/normalized-mount.schema.json +1 -1
  89. package/packages/public-runtime/src/chrome.ts +44 -7
  90. package/packages/public-runtime/src/csp.ts +12 -1
  91. package/packages/public-runtime/src/extension-contract-values.d.mts +19 -0
  92. package/packages/public-runtime/src/extension-contract-values.mjs +62 -0
  93. package/packages/public-runtime/src/extension-contract.ts +20 -6
  94. package/packages/public-runtime/src/image-dependencies.ts +1438 -0
  95. package/packages/public-runtime/src/index.ts +8 -0
  96. package/packages/public-runtime/src/integration/consent-broker.ts +10 -7
  97. package/packages/public-runtime/src/integration/consent-config.ts +9 -0
  98. package/packages/public-runtime/src/integration/contact-sales-action-source.ts +56 -10
  99. package/packages/public-runtime/src/integration/governed-event-properties.ts +62 -0
  100. package/packages/public-runtime/src/integration/index.ts +1 -0
  101. package/packages/public-runtime/src/integration/lifecycle-boot.ts +30 -2
  102. package/packages/public-runtime/src/integration/lifecycle-host-asset.d.mts +2 -0
  103. package/packages/public-runtime/src/integration/lifecycle-host-asset.mjs +17 -3
  104. package/packages/public-runtime/src/integration/native-amplitude.ts +12 -3
  105. package/packages/public-runtime/src/integration/native-ga4-sandbox-asset.mjs +9 -0
  106. package/packages/public-runtime/src/integration/native-ga4-sandbox-entry.ts +27 -3
  107. package/packages/public-runtime/src/integration/native-ga4-sandbox.ts +13 -0
  108. package/packages/public-runtime/src/integration/native-ga4.ts +11 -0
  109. package/packages/public-runtime/src/integration/platform-adapters.ts +5 -1
  110. package/packages/public-runtime/src/integration/render-phases.ts +28 -2
  111. package/packages/public-runtime/src/publication-redirects.d.mts +45 -0
  112. package/packages/public-runtime/src/publication-redirects.mjs +379 -0
  113. package/packages/public-runtime/src/publication-space.ts +24 -3
  114. package/packages/public-runtime/src/static-bundle.ts +145 -26
  115. package/packages/public-runtime/src/tenant-embed-defaults.d.mts +3 -0
  116. package/packages/public-runtime/src/tenant-embed-defaults.mjs +40 -0
  117. package/packages/public-runtime/src/tenant.ts +59 -1
  118. package/pnpm-lock.runner.yaml +19 -0
  119. package/pnpm-workspace.yaml +4 -0
  120. package/scripts/dev/git-status.mjs +1 -1
  121. package/scripts/dev/publish.mjs +6 -0
  122. package/scripts/lib/tenant-asset-copy.mjs +4 -2
  123. package/scripts/tenant/materialize-tenant-content.mjs +1 -0
  124. package/scripts/tenant/validate.mjs +171 -8
  125. package/apps/storefront/src/lib/cfImage.ts +0 -55
  126. package/apps/storefront/src/workers/pruneRollupWorker.ts +0 -120
  127. package/tenants/home/public/fonts/inter-latin-400-normal.woff2 +0 -0
  128. package/tenants/home/public/fonts/inter-latin-500-normal.woff2 +0 -0
  129. package/tenants/home/public/fonts/inter-latin-600-normal.woff2 +0 -0
  130. package/tenants/home/public/fonts/inter-latin-ext-400-normal.woff2 +0 -0
  131. package/tenants/home/public/fonts/inter-latin-ext-500-normal.woff2 +0 -0
  132. package/tenants/home/public/fonts/inter-latin-ext-600-normal.woff2 +0 -0
  133. package/tenants/home/public/fonts/jetbrains-mono-latin-400-normal.woff2 +0 -0
  134. package/tenants/home/public/fonts/jetbrains-mono-latin-500-normal.woff2 +0 -0
  135. package/tenants/home/public/fonts/jetbrains-mono-latin-ext-400-normal.woff2 +0 -0
  136. package/tenants/home/public/fonts/jetbrains-mono-latin-ext-500-normal.woff2 +0 -0
  137. package/tenants/home/public/fonts/plus-jakarta-sans-latin-500-normal.woff2 +0 -0
  138. package/tenants/home/public/fonts/plus-jakarta-sans-latin-600-normal.woff2 +0 -0
  139. package/tenants/home/public/fonts/plus-jakarta-sans-latin-700-normal.woff2 +0 -0
  140. package/tenants/home/public/fonts/plus-jakarta-sans-latin-800-normal.woff2 +0 -0
  141. package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-500-normal.woff2 +0 -0
  142. package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-600-normal.woff2 +0 -0
  143. package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-700-normal.woff2 +0 -0
  144. package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-800-normal.woff2 +0 -0
  145. package/tenants/home/public/js/storefront.js +0 -129
  146. package/tenants/home/public/logo-wordmark.png +0 -0
  147. package/tenants/home/public/pages/storefront.css +0 -235
  148. package/tenants/home/theme.json +0 -6
@@ -59,6 +59,19 @@
59
59
  --color-field-border: var(--color-field-border, color-mix(in srgb, var(--color-border) 52%, var(--color-text)));
60
60
  --color-star: var(--color-star, var(--color-accent));
61
61
 
62
+ /* Conversion-role aliases (style-concept.md §Semantic tokens). The palette
63
+ above names a COLOR; these name the JOB a color does on a conversion
64
+ surface, so a reskin can move the buy button without dragging chrome,
65
+ headings and ::selection with it (all of which read --color-primary).
66
+ Each defaults to the palette role it plays today, so nothing renders
67
+ differently until a tenant supplies one — a tenant brand-tunes by emitting
68
+ color.cta / color.cta-hover / color.cta-contrast / color.trust, which
69
+ themeToCssVars serializes like any other --color-<k>. */
70
+ --color-cta: var(--color-cta, var(--color-primary));
71
+ --color-cta-hover: var(--color-cta-hover, var(--color-primary-hover));
72
+ --color-cta-contrast: var(--color-cta-contrast, var(--color-primary-contrast));
73
+ --color-trust: var(--color-trust, var(--color-success));
74
+
62
75
  --font-display: var(--font-display, "Space Grotesk"), system-ui, sans-serif;
63
76
  --font-body: var(--font-body, "Source Serif 4"), Georgia, serif;
64
77
  --font-mono: var(--font-mono, "Space Mono"), ui-monospace, monospace;
@@ -85,6 +98,21 @@
85
98
  injected --tap-target (schema.ts, from shape.tapTarget) wins when present,
86
99
  else 44px. Components size touch controls with var(--tap-target). (F6.) */
87
100
  --tap-target: var(--tap-target, 44px);
101
+
102
+ /* Role aliases that are NOT colors and so cannot live in @theme (Tailwind
103
+ v4 owns the --text-* namespace for font sizes — a --text-muted there would
104
+ collide with the existing `text-muted` color utility). Declared here for
105
+ the same reason --tap-target is, and read by the conversion components
106
+ below so they survive a reskin:
107
+ --focus-ring the whole keyboard-focus outline, not just its color, so
108
+ a theme can change the ring's weight/style too
109
+ --surface-card raised card/panel fill (testimonials, proof cards)
110
+ --text-muted secondary copy (help text, attributions, fine print)
111
+ Each resolves through an optional --color-<k> override first, so a theme
112
+ brand-tunes it over the normal token channel without a new mechanism. */
113
+ --focus-ring: var(--border-thick, 2px) solid var(--color-focus-ring);
114
+ --surface-card: var(--color-surface-card, var(--color-surface));
115
+ --text-muted: var(--color-text-muted, var(--color-muted));
88
116
  }
89
117
 
90
118
  html {
@@ -124,7 +152,9 @@
124
152
  dedicated --color-focus-ring token (defaults to the accent spark) so a theme
125
153
  can brand-tune the ring without touching the accent used elsewhere. */
126
154
  :focus-visible {
127
- outline: var(--border-thick, 2px) solid var(--color-focus-ring);
155
+ /* --focus-ring is the whole outline; its own default is built from
156
+ --color-focus-ring, so the dedicated ring color still drives it. */
157
+ outline: var(--focus-ring, var(--border-thick, 2px) solid var(--color-focus-ring));
128
158
  outline-offset: 2px;
129
159
  border-radius: 2px;
130
160
  }
@@ -136,6 +166,36 @@
136
166
 
137
167
  img { max-width: 100%; height: auto; }
138
168
 
169
+ /* Disabled + busy controls (style-concept.md §Full interactive states).
170
+ --color-disabled / --color-disabled-contrast were declared for exactly this
171
+ and nothing read them, so an unstyled `<button disabled>` was pixel-identical
172
+ to a live one. This is a @layer base default: any component rule or Tailwind
173
+ utility (utilities layer) still wins, so controls that already carry their
174
+ own disabled treatment are untouched.
175
+ Checkboxes, radios, ranges, colors and file inputs are excluded — a
176
+ background-color on those replaces the native widget rather than tinting it. */
177
+ :where(button, [role="button"], a, input, select, textarea):disabled,
178
+ :where(button, [role="button"], a, input, select, textarea)[aria-disabled="true"] {
179
+ cursor: not-allowed;
180
+ }
181
+
182
+ :is(
183
+ button,
184
+ [role="button"],
185
+ select,
186
+ textarea,
187
+ input:not([type="checkbox"], [type="radio"], [type="range"], [type="color"], [type="file"])
188
+ ):disabled {
189
+ background-color: var(--color-disabled);
190
+ color: var(--color-disabled-contrast);
191
+ border-color: var(--color-disabled);
192
+ box-shadow: none;
193
+ }
194
+
195
+ /* Loading/busy: the control holds its place and stops accepting input. */
196
+ [aria-busy="true"] { cursor: progress; }
197
+ :is(button, [role="button"], form, fieldset)[aria-busy="true"] { pointer-events: none; }
198
+
139
199
  /* Honor reduced motion (brief §6/§8). */
140
200
  @media (prefers-reduced-motion: reduce) {
141
201
  *, *::before, *::after {
@@ -147,6 +207,22 @@
147
207
  }
148
208
  }
149
209
 
210
+ @layer components {
211
+ .skip-link {
212
+ position: fixed;
213
+ inset-block-start: 0;
214
+ inset-inline-start: 0;
215
+ z-index: 2147483646;
216
+ transform: translateY(-150%);
217
+ border-radius: var(--radius-md);
218
+ background: var(--color-primary);
219
+ color: var(--color-primary-contrast);
220
+ padding: .75rem 1rem;
221
+ }
222
+
223
+ .skip-link:focus-visible { transform: translateY(0); }
224
+ }
225
+
150
226
  /* ---- shared component primitives ----------------------------------------- */
151
227
  @layer components {
152
228
  .container-prose {
@@ -158,7 +234,7 @@
158
234
 
159
235
  .eyebrow {
160
236
  font-family: var(--font-mono);
161
- font-size: 0.72rem;
237
+ font-size: 0.875rem;
162
238
  letter-spacing: 0.16em;
163
239
  text-transform: uppercase;
164
240
  color: var(--color-muted);
@@ -249,7 +325,10 @@
249
325
  .link-accent {
250
326
  color: var(--color-text);
251
327
  background-image: linear-gradient(var(--color-accent-ink), var(--color-accent-ink));
252
- background-position: 0 100%;
328
+ /* The underline sits against the CONTENT box, so `.link-accent--cta`'s
329
+ padding cannot push it away from the text. */
330
+ background-origin: content-box;
331
+ background-position: 0 calc(100% + 2px);
253
332
  background-repeat: no-repeat;
254
333
  background-size: 100% 1.5px;
255
334
  transition: color 0.2s ease, background-size 0.25s cubic-bezier(0.22, 1, 0.36, 1);
@@ -258,6 +337,17 @@
258
337
  .link-accent:focus-visible {
259
338
  color: var(--color-accent-ink);
260
339
  }
340
+ /* Standalone CTA form — a link on its own line, so it carries the
341
+ --tap-target minimum. Vertical padding on an INLINE box grows the hit area
342
+ and the measured rect without adding a pixel to the line box, so nothing
343
+ around it moves; keep these links inline, because an inline-block would lay
344
+ that padding out. 0.875rem clears 44px over the 18px content area of a
345
+ 14px/1.25 line (measured in Chrome at 390px). Deliberately NOT on the base
346
+ class: `.link-accent` also marks up links inside a sentence, where the
347
+ padding would reach into the hit area of the line above. */
348
+ .link-accent--cta {
349
+ padding-block: 0.875rem;
350
+ }
261
351
  @media (prefers-reduced-motion: no-preference) {
262
352
  .link-accent { background-size: 0% 1.5px; }
263
353
  .link-accent:hover,
@@ -310,3 +400,307 @@
310
400
  to { opacity: 1; transform: none; }
311
401
  }
312
402
  }
403
+
404
+ /* ---- conversion patterns -------------------------------------------------- */
405
+ /*
406
+ * The CTA hierarchy, trust/social-proof and accessible-form families the
407
+ * ecommerce quality bar asks of every theme
408
+ * (playbook/site-migration/style-concept.md §"Ecommerce quality bar"), built
409
+ * once at platform level so every tenant inherits them rather than re-deriving
410
+ * them per migration.
411
+ *
412
+ * Every value reads a contract token, so a reskin moves the whole set and a
413
+ * tenant never hand-writes a hex here.
414
+ *
415
+ * The class names are deliberately NOT `.btn` / `.field`: admin.css and several
416
+ * page-scoped <style> blocks already own those, and a bare `.btn` here would
417
+ * reach into them.
418
+ */
419
+ @layer components {
420
+ /* --- CTA hierarchy -------------------------------------------------------
421
+ Three weights so a page can carry exactly ONE primary ask and route every
422
+ other action down a tier:
423
+ .btn-primary the conversion action (buy, start, book) — one per view
424
+ .btn-secondary the supporting action (compare, learn more)
425
+ .btn-tertiary the low-commitment out (quiet/text; nav, footer, cards)
426
+ Placement rules for hero / nav / pricing / footer / mobile-sticky live in
427
+ the playbook; the treatments live here. */
428
+ .btn-primary,
429
+ .btn-secondary,
430
+ .btn-tertiary {
431
+ display: inline-flex;
432
+ align-items: center;
433
+ justify-content: center;
434
+ gap: 0.5em;
435
+ /* WCAG 2.2 §2.5.8 — the same minimum every other touch control uses. */
436
+ min-height: var(--tap-target);
437
+ padding: 0.75rem 1.5rem;
438
+ border: var(--border-thin, 1px) solid transparent;
439
+ border-radius: var(--radius-full);
440
+ font-family: var(--font-display);
441
+ font-size: 0.875rem;
442
+ font-weight: 500;
443
+ line-height: 1.2;
444
+ letter-spacing: 0.08em;
445
+ text-transform: uppercase;
446
+ text-align: center;
447
+ cursor: pointer;
448
+ transition:
449
+ background-color 0.2s ease,
450
+ border-color 0.2s ease,
451
+ color 0.2s ease;
452
+ }
453
+
454
+ .btn-primary {
455
+ background-color: var(--color-cta);
456
+ color: var(--color-cta-contrast);
457
+ }
458
+ .btn-primary:hover { background-color: var(--color-cta-hover); }
459
+ .btn-primary:active {
460
+ background-color: color-mix(in srgb, var(--color-cta-hover) 90%, #000);
461
+ }
462
+
463
+ .btn-secondary {
464
+ background-color: transparent;
465
+ border-color: var(--color-cta);
466
+ color: var(--color-cta);
467
+ }
468
+ .btn-secondary:hover {
469
+ background-color: color-mix(in srgb, var(--color-cta) 10%, transparent);
470
+ border-color: var(--color-cta-hover);
471
+ color: var(--color-cta-hover);
472
+ }
473
+ .btn-secondary:active {
474
+ background-color: color-mix(in srgb, var(--color-cta) 18%, transparent);
475
+ }
476
+
477
+ .btn-tertiary {
478
+ background-color: transparent;
479
+ padding-inline: 0.5rem;
480
+ color: var(--color-text);
481
+ text-decoration: underline;
482
+ text-decoration-color: transparent;
483
+ text-underline-offset: 4px;
484
+ transition: color 0.2s ease, text-decoration-color 0.2s ease;
485
+ }
486
+ .btn-tertiary:hover {
487
+ color: var(--color-accent-ink);
488
+ text-decoration-color: currentColor;
489
+ }
490
+ .btn-tertiary:active { color: var(--color-cta-hover); }
491
+
492
+ /* Busy CTA: holds its box (no reflow mid-submit), reads as inactive, and the
493
+ base layer has already removed pointer events. */
494
+ .btn-primary[aria-busy="true"],
495
+ .btn-secondary[aria-busy="true"],
496
+ .btn-tertiary[aria-busy="true"] {
497
+ opacity: 0.7;
498
+ }
499
+
500
+ /* Full-width CTA — the mobile/sticky and in-card form. */
501
+ .btn-block { display: flex; width: 100%; }
502
+
503
+ /* --- Trust & social proof ------------------------------------------------
504
+ Boundary-safe SHELLS only. They style a claim; they never assert one. Copy
505
+ stays placeholder or `needs-review` until sourced (style-concept.md §Trust
506
+ and social proof components). */
507
+
508
+ /* Horizontal strip of short proof points under a hero or above a footer. */
509
+ .proof-strip {
510
+ display: flex;
511
+ flex-wrap: wrap;
512
+ align-items: center;
513
+ justify-content: center;
514
+ gap: 1rem 2rem;
515
+ padding-block: 1rem;
516
+ color: var(--text-muted);
517
+ font-family: var(--font-mono);
518
+ font-size: 0.8125rem;
519
+ letter-spacing: 0.08em;
520
+ text-transform: uppercase;
521
+ }
522
+
523
+ /* Review / rating display. The stars are decorative; the accessible value
524
+ belongs in the .rating__value text, never in color alone. */
525
+ .rating {
526
+ display: inline-flex;
527
+ align-items: center;
528
+ gap: 0.5em;
529
+ }
530
+ .rating__stars {
531
+ color: var(--color-star);
532
+ letter-spacing: 0.08em;
533
+ line-height: 1;
534
+ }
535
+ .rating__value {
536
+ color: var(--text-muted);
537
+ font-family: var(--font-mono);
538
+ font-size: 0.8125rem;
539
+ }
540
+
541
+ /* Customer testimonial / pull quote. */
542
+ .testimonial {
543
+ display: flex;
544
+ flex-direction: column;
545
+ gap: 0.75rem;
546
+ padding: 1.5rem;
547
+ border: var(--border-thin, 1px) solid var(--color-border);
548
+ border-radius: var(--radius-lg);
549
+ background: var(--surface-card);
550
+ box-shadow: var(--shadow-sm);
551
+ }
552
+ .testimonial__quote {
553
+ margin: 0;
554
+ font-size: var(--fs-md, 1.125rem);
555
+ line-height: 1.5;
556
+ text-wrap: pretty;
557
+ }
558
+ .testimonial__attribution {
559
+ display: flex;
560
+ align-items: center;
561
+ gap: 0.6rem;
562
+ color: var(--text-muted);
563
+ font-family: var(--font-mono);
564
+ font-size: 0.8125rem;
565
+ }
566
+ .testimonial__avatar {
567
+ flex: none;
568
+ width: 2.25rem;
569
+ height: 2.25rem;
570
+ border-radius: var(--radius-full);
571
+ object-fit: cover;
572
+ }
573
+
574
+ /* Security / compliance / certification badge. Reads --color-trust so the
575
+ reassurance palette moves independently of the CTA palette. */
576
+ .trust-badge {
577
+ display: inline-flex;
578
+ align-items: center;
579
+ gap: 0.45em;
580
+ padding: 0.35rem 0.7rem;
581
+ border: var(--border-thin, 1px) solid
582
+ color-mix(in srgb, var(--color-trust) 42%, transparent);
583
+ border-radius: var(--radius-full);
584
+ background: color-mix(in srgb, var(--color-trust) 10%, transparent);
585
+ color: var(--color-trust);
586
+ font-family: var(--font-mono);
587
+ font-size: 0.6875rem;
588
+ letter-spacing: 0.12em;
589
+ text-transform: uppercase;
590
+ white-space: nowrap;
591
+ }
592
+ .trust-badge__icon {
593
+ flex: none;
594
+ width: 0.95em;
595
+ height: 0.95em;
596
+ }
597
+
598
+ /* Guarantee / returns / refund / warranty callout — the risk-reversal line
599
+ that sits next to the buy button. */
600
+ .reassurance {
601
+ display: flex;
602
+ align-items: flex-start;
603
+ gap: 0.6rem;
604
+ padding: 0.75rem 1rem;
605
+ border-inline-start: var(--border-thick, 2px) solid var(--color-trust);
606
+ border-radius: var(--radius-sm);
607
+ background: color-mix(in srgb, var(--color-trust) 7%, transparent);
608
+ font-size: 0.875rem;
609
+ line-height: 1.45;
610
+ }
611
+ .reassurance__icon {
612
+ flex: none;
613
+ width: 1.1em;
614
+ height: 1.1em;
615
+ margin-top: 0.15em;
616
+ color: var(--color-trust);
617
+ }
618
+ .reassurance__note {
619
+ display: block;
620
+ color: var(--text-muted);
621
+ font-size: 0.8125rem;
622
+ }
623
+
624
+ /* "As seen in" / customer-logo strip. Logos are desaturated at rest so they
625
+ read as texture, not as endorsements competing with the CTA. */
626
+ .logo-strip {
627
+ display: flex;
628
+ flex-wrap: wrap;
629
+ align-items: center;
630
+ justify-content: center;
631
+ gap: 1.25rem 2.5rem;
632
+ padding-block: 1.25rem;
633
+ }
634
+ .logo-strip__item {
635
+ max-height: 2rem;
636
+ width: auto;
637
+ filter: grayscale(1);
638
+ opacity: 0.65;
639
+ transition: filter 0.2s ease, opacity 0.2s ease;
640
+ }
641
+ .logo-strip__item:hover {
642
+ filter: none;
643
+ opacity: 1;
644
+ }
645
+
646
+ /* --- Accessible forms ----------------------------------------------------
647
+ Lead-gen / contact / newsletter fields with every state the contract names:
648
+ label, help text, required + optional markers, validation error, success,
649
+ disabled and loading (the last two come from the base layer). Errors are
650
+ announced through aria-describedby + aria-invalid, never color alone. */
651
+ .form-field {
652
+ display: grid;
653
+ gap: 0.375rem;
654
+ }
655
+ .form-field__label {
656
+ font-family: var(--font-mono);
657
+ font-size: 0.75rem;
658
+ letter-spacing: 0.1em;
659
+ text-transform: uppercase;
660
+ color: var(--color-text);
661
+ }
662
+ /* Required/optional markers. The word is the signal; the color is decoration,
663
+ so the marker text must stay in the DOM for screen readers. */
664
+ .form-field__required { color: var(--color-error); }
665
+ .form-field__optional { color: var(--text-muted); font-weight: 400; }
666
+
667
+ .form-control {
668
+ width: 100%;
669
+ min-height: var(--tap-target);
670
+ padding: 0.625rem 0.85rem;
671
+ border: var(--border-thin, 1px) solid var(--color-field-border);
672
+ border-radius: var(--radius-md);
673
+ background: var(--color-surface);
674
+ color: var(--color-text);
675
+ font-family: inherit;
676
+ font-size: 1rem; /* >=16px: iOS zooms the page on focus below this. */
677
+ line-height: 1.4;
678
+ transition: border-color 0.15s ease;
679
+ }
680
+ .form-control::placeholder { color: var(--text-muted); }
681
+ .form-control:hover { border-color: var(--color-text); }
682
+
683
+ .form-control[aria-invalid="true"] { border-color: var(--color-error); }
684
+ .form-control[data-state="success"] { border-color: var(--color-success); }
685
+
686
+ /* Help text — the persistent hint, wired with aria-describedby. */
687
+ .form-field__help {
688
+ color: var(--text-muted);
689
+ font-size: 0.8125rem;
690
+ line-height: 1.4;
691
+ }
692
+ /* Validation messages. Both carry a glyph so the state survives a
693
+ monochrome/color-blind read. */
694
+ .form-field__error,
695
+ .form-field__success {
696
+ display: flex;
697
+ align-items: flex-start;
698
+ gap: 0.4em;
699
+ font-size: 0.8125rem;
700
+ line-height: 1.4;
701
+ }
702
+ .form-field__error { color: var(--color-error); }
703
+ .form-field__error::before { content: "\26A0"; }
704
+ .form-field__success { color: var(--color-success); }
705
+ .form-field__success::before { content: "\2713"; }
706
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenoftrust/storefront-runner",
3
- "version": "2.0.41",
3
+ "version": "2.0.43",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "description": "World-shareable storefront runner: multi-tenant renderer on Astro/Cloudflare. No control plane.",
6
6
  "packageManager": "pnpm@11.9.0",
@@ -17,6 +17,9 @@
17
17
  "typecheck": "pnpm -r typecheck",
18
18
  "depcruise": "pnpm --filter @tot/storefront-runner depcruise"
19
19
  },
20
+ "dependencies": {
21
+ "sharp": "^0.34.5"
22
+ },
20
23
  "devDependencies": {
21
24
  "typescript": "^5.6.3"
22
25
  },