@tokenoftrust/storefront-runner 2.2.126 → 2.3.1

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 (109) hide show
  1. package/apps/storefront/package.json +3 -1
  2. package/apps/storefront/public/shared/commerce-chrome.css +57 -15
  3. package/apps/storefront/public/shared/commerce-marketing.css +7 -7
  4. package/apps/storefront/src/components/Badge.astro +1 -1
  5. package/apps/storefront/src/components/Breadcrumbs.astro +8 -3
  6. package/apps/storefront/src/components/Section.astro +4 -1
  7. package/apps/storefront/src/components/Seo.astro +7 -5
  8. package/apps/storefront/src/components/blog/BlogIndexView.astro +22 -13
  9. package/apps/storefront/src/components/blog/ShareRow.astro +43 -0
  10. package/apps/storefront/src/components/chrome/AnnouncementBar.astro +1 -1
  11. package/apps/storefront/src/components/chrome/NavDropdown.astro +7 -7
  12. package/apps/storefront/src/components/chrome/SiteFooter.astro +17 -0
  13. package/apps/storefront/src/components/chrome/SiteHeader.astro +5 -5
  14. package/apps/storefront/src/components/commerce/TagFilterCloud.astro +1 -1
  15. package/apps/storefront/src/components/compliance/NicotineWarning.astro +12 -22
  16. package/apps/storefront/src/components/compliance/PactActNotice.astro +9 -10
  17. package/apps/storefront/src/components/content/ProseSections.astro +2 -2
  18. package/apps/storefront/src/components/home/Hero.astro +34 -12
  19. package/apps/storefront/src/components/islands/IsolatedAgeGate.tsx +39 -5
  20. package/apps/storefront/src/components/islands/VariantSelector.tsx +3 -3
  21. package/apps/storefront/src/components/marketing/CardGrid.astro +1 -1
  22. package/apps/storefront/src/components/marketing/CodeSample.astro +1 -1
  23. package/apps/storefront/src/components/marketing/CtaBand.astro +1 -1
  24. package/apps/storefront/src/components/marketing/Faq.astro +1 -1
  25. package/apps/storefront/src/components/marketing/Integrations.astro +1 -1
  26. package/apps/storefront/src/components/marketing/MarketingCtas.astro +1 -1
  27. package/apps/storefront/src/components/marketing/MarketingHero.astro +1 -1
  28. package/apps/storefront/src/components/marketing/ProofStrip.astro +1 -1
  29. package/apps/storefront/src/components/marketing/SplitCompare.astro +1 -1
  30. package/apps/storefront/src/components/marketing/Steps.astro +1 -1
  31. package/apps/storefront/src/components/marketing/Testimonials.astro +2 -2
  32. package/apps/storefront/src/components/marketing/TrustBar.astro +1 -1
  33. package/apps/storefront/src/components/membership/TierComparisonTable.astro +1 -1
  34. package/apps/storefront/src/components/plp/ActiveFilters.astro +1 -1
  35. package/apps/storefront/src/components/plp/FacetSidebar.astro +4 -4
  36. package/apps/storefront/src/components/plp/Pagination.astro +1 -1
  37. package/apps/storefront/src/components/plp/SortSelect.astro +1 -1
  38. package/apps/storefront/src/components/style-guide/StyleGuideNav.astro +18 -3
  39. package/apps/storefront/src/components/the-build/TheBuildIndexView.astro +9 -5
  40. package/apps/storefront/src/config/devTenantSeed.ts +23 -34
  41. package/apps/storefront/src/config/resolver.ts +0 -38
  42. package/apps/storefront/src/layouts/Layout.astro +8 -1
  43. package/apps/storefront/src/lib/assets/preview-version-cookie.ts +52 -5
  44. package/apps/storefront/src/lib/blog/pagination.ts +20 -10
  45. package/apps/storefront/src/lib/blog/presentation.ts +94 -0
  46. package/apps/storefront/src/lib/blog/rss.ts +1 -1
  47. package/apps/storefront/src/lib/blog/types.ts +24 -7
  48. package/apps/storefront/src/lib/breadcrumbs.ts +25 -0
  49. package/apps/storefront/src/lib/checkoutCommerce.ts +26 -3
  50. package/apps/storefront/src/lib/chrome/model.ts +4 -0
  51. package/apps/storefront/src/lib/cloudflare-workers.d.ts +53 -0
  52. package/apps/storefront/src/lib/compliance/rawComplianceNotices.ts +38 -18
  53. package/apps/storefront/src/lib/dashboard/delegateGate.ts +28 -0
  54. package/apps/storefront/src/lib/homeVisualParity.ts +8 -5
  55. package/apps/storefront/src/lib/jsonld.ts +5 -5
  56. package/apps/storefront/src/lib/pinnedRouteRewriteBoundary.ts +20 -2
  57. package/apps/storefront/src/lib/rawChrome.ts +74 -12
  58. package/apps/storefront/src/lib/runtimeEnv.ts +1 -1
  59. package/apps/storefront/src/lib/seo/documentTitle.ts +19 -0
  60. package/apps/storefront/src/lib/shared/addToRelease.ts +32 -0
  61. package/apps/storefront/src/lib/storyblok/content-model.ts +3 -0
  62. package/apps/storefront/src/lib/styleGuideThemes.ts +20 -22
  63. package/apps/storefront/src/pages/[...slug].astro +13 -8
  64. package/apps/storefront/src/pages/blog/[slug].astro +56 -24
  65. package/apps/storefront/src/pages/blog/author/[author].astro +17 -9
  66. package/apps/storefront/src/pages/blog/category/[category].astro +18 -10
  67. package/apps/storefront/src/pages/blog/index.astro +2 -0
  68. package/apps/storefront/src/pages/blog/page/[n].astro +2 -0
  69. package/apps/storefront/src/pages/blog/tag/[tag].astro +18 -10
  70. package/apps/storefront/src/pages/collections/[handle].astro +5 -4
  71. package/apps/storefront/src/pages/collections/index.astro +4 -3
  72. package/apps/storefront/src/pages/index.astro +34 -27
  73. package/apps/storefront/src/pages/products/[handle].astro +5 -4
  74. package/apps/storefront/src/pages/saved.astro +4 -3
  75. package/apps/storefront/src/pages/search.astro +4 -3
  76. package/apps/storefront/src/pages/style-guide/[tenant]/[theme].astro +129 -118
  77. package/apps/storefront/src/pages/style-guide/[tenant]/chrome/[theme].astro +19 -15
  78. package/apps/storefront/src/pages/style-guide/[tenant]/guide/[theme].astro +23 -27
  79. package/apps/storefront/src/pages/style-guide/[tenant]/index.astro +7 -14
  80. package/apps/storefront/src/pages/style-guide/index.astro +6 -5
  81. package/apps/storefront/src/pages/tenants/[id]/[...path].ts +32 -7
  82. package/apps/storefront/src/pages/the-build/[slug].astro +44 -20
  83. package/apps/storefront/src/pages/the-build/index.astro +2 -0
  84. package/apps/storefront/src/pages/the-build/page/[n].astro +2 -0
  85. package/apps/storefront/src/styles/admin.css +47 -0
  86. package/apps/storefront/src/styles/global.css +104 -56
  87. package/apps/storefront/src/themes/derivedTokens.ts +34 -0
  88. package/apps/storefront/src/themes/schema.ts +6 -0
  89. package/apps/storefront/test/pipeline/harness.ts +164 -0
  90. package/apps/storefront/test/pipeline/intake-worker.mjs +14 -0
  91. package/apps/storefront/test/pipeline/pipeline-host.ts +77 -0
  92. package/apps/storefront/test/pipeline/synthetic-job.ts +156 -0
  93. package/apps/storefront/vitest.workers.config.cts +67 -0
  94. package/docs/widget-library-guide.md +1496 -0
  95. package/package.json +1 -1
  96. package/packages/cli/src/declared-config.mjs +400 -5
  97. package/packages/public-runtime/package.json +1 -0
  98. package/packages/public-runtime/src/checkout.ts +13 -0
  99. package/packages/public-runtime/src/chrome.ts +93 -2
  100. package/packages/public-runtime/src/compliance-coherence.d.mts +17 -0
  101. package/packages/public-runtime/src/compliance-coherence.mjs +84 -0
  102. package/packages/public-runtime/src/declared-tenant-config.d.mts +1 -1
  103. package/packages/public-runtime/src/declared-tenant-config.mjs +6 -2
  104. package/packages/public-runtime/src/extension-contract-values.d.mts +4 -1
  105. package/packages/public-runtime/src/extension-contract-values.mjs +14 -1
  106. package/packages/public-runtime/src/extension-contract.ts +2 -0
  107. package/packages/public-runtime/src/tenant.ts +48 -4
  108. package/pnpm-lock.runner.yaml +469 -0
  109. package/scripts/tenant/validate.mjs +27 -1
@@ -0,0 +1,1496 @@
1
+ # Storefront widget library — user guide
2
+
3
+ **Status:** maintained reference, workstream `shared-ecommerce-widgets`. This is the
4
+ human- and agent-facing guide to the shared regulated-ecommerce widget library that every
5
+ storefront tenant inherits. It is the **source of truth**; the style-guide route
6
+ `/_style-guide/<tenant>/guide/<theme>/` renders this same file as HTML.
7
+
8
+ > **How to keep this accurate.** Every widget entry is enumerated from real code under
9
+ > `apps/storefront/src/components/**` and the shared libs in `apps/storefront/src/lib/`.
10
+ > When you add, rename, or reshape a component's props, update its entry here in the same change.
11
+ > Where a fact is load-bearing (a prop name, a token, a data attribute) it is quoted from the source.
12
+
13
+ ---
14
+
15
+ ## 1. Overview — how the library works
16
+
17
+ The library is a set of **platform-owned Astro/Preact components**. A tenant never forks a
18
+ component: it restyles the whole library by supplying **theme tokens** and drives regulated
19
+ surfaces with **data**. Three ideas underpin everything below.
20
+
21
+ ### 1.1 The token contract (the library's public API)
22
+
23
+ Components read only the **`themeToCssVars` `--color-*` vocabulary** (plus `--font-*`, `--fs-*`,
24
+ `--space-*`, `--radius-*`, `--shadow-*`, `--container-max`, `--section-rhythm`). This CSS
25
+ custom-property vocabulary is the library's public API — the full spec, reference values, and the
26
+ rule for aliasing a tenant's own brand vars onto it live in
27
+ [`docs/architecture/token-contract.md`](architecture/token-contract.md).
28
+
29
+ - **Core palette (every theme states these):** `--color-{bg, surface, text, muted, primary,
30
+ primary-contrast, accent, border, success, sale}`.
31
+ - **Semantic / state (bridge-defaulted — always resolvable):** `--color-{surface-2, warning,
32
+ error, info, accent-ink, primary-hover, accent-hover}`.
33
+ - **The rule for components:** read tokens only — `var(--color-…)` or the Tailwind utility that
34
+ maps to it (`bg-bg`, `text-primary`, `text-error`, `rounded-md`, `border-border`). Never
35
+ hard-code a hex; never read a tenant-private brand var (`var(--brand-red)`) — those are aliased
36
+ onto the contract.
37
+
38
+ Two equivalent token-reference styles appear across the library and both resolve to the same
39
+ contract: chrome + several atoms use **Tailwind token utilities** (`bg-primary`, `text-muted`,
40
+ `border-border`), while PLP components and most marketing blocks use **arbitrary
41
+ `[var(--color-*)]` utilities** or scoped `<style>` blocks with `var(--color-*)`.
42
+
43
+ ### 1.2 The shared-vs-tenant boundary
44
+
45
+ What is shared platform vs. what is per-tenant is drawn in
46
+ [`docs/architecture/shared-vs-tenant-assets.md`](architecture/shared-vs-tenant-assets.md):
47
+
48
+ - **Tier 1 — shared platform:** every component in this guide + `styles/global.css` + the style
49
+ guide. Driven by theme tokens.
50
+ - **Tier 3 — per-tenant:** theme tokens, content (`home`/pages/catalog), brand assets. Differences
51
+ between tenants must be **data + tokens, never re-authored component CSS**.
52
+
53
+ ### 1.3 Two delivery paths, one look
54
+
55
+ Many chrome/commerce widgets ship **both** as a platform component **and** as shared raw-chrome
56
+ CSS, so a tenant that renders hand-authored raw marketing HTML gets the same look without copying
57
+ CSS (decision `raw-chrome-single-stylesheet` / `materialization-compat`):
58
+
59
+ - **Platform component** — the `.astro`/`.tsx` in `components/**`, token-styled, used by
60
+ token-driven storefronts and the style guide.
61
+ - **Shared raw-chrome CSS** — one platform stylesheet, `public/shared/commerce-chrome.css`
62
+ (linked as `/shared/commerce-chrome.css`), whose class vocabulary (`.site-header`, `.announce`,
63
+ `.primary-nav`/`.mega`, `.site-footer`, `.compliance-strip`, `.age-gate`, …) mirrors the
64
+ component DOM. The server-side generator `lib/rawChrome.ts` emits that DOM from a tenant's typed
65
+ `chrome.json`, links exactly that one stylesheet, and emits **no inline chrome CSS**. See §11.
66
+
67
+ Where a component has a raw-chrome twin, its entry notes the matching selectors.
68
+
69
+ ### 1.4 Compliance widgets are platform-owned and data-driven (never faked in marketing)
70
+
71
+ Regulated-commerce notices (`components/compliance/**`) are **platform-owned and data-driven from
72
+ `tenant.compliance`** (decision `compliance-widgets-platform-owned`). They:
73
+
74
+ - render **only** from tenant config, and render **nothing** when their `tenant.compliance` field
75
+ is absent;
76
+ - **never fabricate legal copy** — the mandated wording (FDA nicotine warning, PACT Act, Prop 65
77
+ skeleton) is owned by the platform component; only the on/off flag and small parameters
78
+ (`minAge`, chemical names, state lists) come from config;
79
+ - for display-only cues (purchase limit, state eligibility, shipping restriction, excise tax), the
80
+ **enforcement/decision/calculation stays product-owned** — the widget only surfaces the message.
81
+
82
+ Marketing blocks (§7) must never hand-author a fake compliance notice; use the platform widget fed
83
+ from `tenant.compliance`.
84
+
85
+ The parallel discipline for **membership/subscription** figures (§5, §6): figures are
86
+ **illustrative by default** — unless a tier/figure asserts `real: true`, the widget shows a
87
+ first-class "illustrative figures" affordance (`IllustrativeLabel`) so an example number is never
88
+ shown as a live offer.
89
+
90
+ ### 1.5 How to read each entry
91
+
92
+ Every widget lists: **Purpose**, **Props** (exact names + TS types), **Dependencies** (components,
93
+ libs, and any tenant config/locals it reads), **Tokens consumed**, **Delivery** (platform component
94
+ + any raw-chrome twin), and a realistic **Usage** snippet. The live style guide
95
+ `/_style-guide/<tenant>/<theme>/` renders almost every widget below with sample data.
96
+
97
+ ### 1.6 Category index
98
+
99
+ - §2 Chrome — `components/chrome/`
100
+ - §3 Commerce — `components/commerce/`
101
+ - §4 Compliance — `components/compliance/`
102
+ - §5 Membership — `components/membership/`
103
+ - §6 Subscription — `components/subscription/`
104
+ - §7 Marketing / widget blocks — `components/marketing/`
105
+ - §8 PLP controls — `components/plp/`
106
+ - §9 Islands — `components/islands/`
107
+ - §10 Foundational atoms + home — `components/*` + `components/home/`
108
+ - §11 Shared raw-chrome library — `lib/rawChrome.ts`, `lib/chrome/`, `public/shared/commerce-chrome.css`
109
+
110
+ Shared chrome model (`lib/chrome/model.ts`), referenced throughout §2:
111
+
112
+ - `ChromeLink` — `{ label: string; href: string; external?: boolean; badge?: string }`
113
+ - `MegaColumn` — `{ title?: string; links: ChromeLink[] }`
114
+ - `ChromeNavItem` — `{ label: string; href: string; badge?: string; flag?: boolean; children?: ChromeLink[]; columns?: MegaColumn[] }` (plain link / single-column dropdown / multi-column mega)
115
+ - `SocialLink` — `{ label: string; href: string; iconPath?: string }`
116
+ - `ChromeLogo` — `{ src: string; alt: string; width?: number; height?: number }`
117
+ - `ChromeCta` — `{ label: string; href: string; variant?: "primary" | "secondary" | "ghost" }`
118
+ - `FooterColumn` — `{ title: string; links: ChromeLink[] }`
119
+ - `FooterNewsletter` — `{ title?: string; body?: string; action?: string; placeholder?: string; source?: string; ageAttestation?: boolean }`
120
+
121
+ ---
122
+
123
+ ## 2. Chrome — `components/chrome/`
124
+
125
+ The site frame: announcement, utility bar, header, primary/mega nav, footer. All six have a
126
+ raw-chrome twin in `public/shared/commerce-chrome.css`. Every href-emitting chrome component reads
127
+ `Astro.locals.basePath` via `withBase` (`@/lib/basePath`) for path-prefix tenant routing.
128
+
129
+ ### AnnouncementBar (`components/chrome/AnnouncementBar.astro`)
130
+
131
+ - **Purpose:** Top promo strip — `static` centered single line or `marquee` continuously-scrolling
132
+ track (duplicated for a seamless loop). Reduced-motion-safe (freezes to static copy).
133
+ - **Props:** `text: string`; `variant?: "static" | "marquee"`; `repeat?: number` (marquee phrase
134
+ repeats, default 6); `tone?: "primary" | "sale" | "accent"`; `label?: string` (region aria-label);
135
+ `class?: string`.
136
+ - **Dependencies:** none (self-contained; no imports, no locals).
137
+ - **Tokens consumed:** `--font-mono`; tone → `bg-primary text-primary-contrast` /
138
+ `bg-[var(--color-sale)]` / `bg-accent`.
139
+ - **Delivery:** platform component + raw-chrome twin `.announce` (solid), `.announce-marquee` /
140
+ `.announce-marquee__track` (marquee), `.announce-bar` / `.announce-bar__msg` /
141
+ `.announce-bar__close` (dismissible/rotating bar).
142
+ - **Usage:**
143
+ ```astro
144
+ <AnnouncementBar text="Free US shipping on orders over $100!" variant="marquee" tone="sale" />
145
+ ```
146
+
147
+ ### UtilityNav (`components/chrome/UtilityNav.astro`)
148
+
149
+ - **Purpose:** Thin secondary bar above the header — minor links, locale/currency indicator, social
150
+ icons. Hidden below `md` (the mobile menu carries the links).
151
+ - **Props:** `links?: ChromeLink[]`; `locale?: string`; `currency?: string`; `social?: SocialLink[]`;
152
+ `class?: string`.
153
+ - **Dependencies:** `type ChromeLink, SocialLink` from `@/lib/chrome/model`; `withBase`.
154
+ - **Tokens consumed:** `border-border`, `bg-surface`, `text-muted`, `hover:text-primary`,
155
+ `font-mono`.
156
+ - **Delivery:** platform component + raw-chrome twin `.utility-nav` / `.utility-nav__right` /
157
+ `.sel` / `.social` / `.member-cue`.
158
+ - **Usage:**
159
+ ```astro
160
+ <UtilityNav
161
+ links={[{ label: "News", href: "/news" }, { label: "Contact", href: "/contact" }]}
162
+ locale="English"
163
+ currency="United States (USD $)"
164
+ social={social}
165
+ />
166
+ ```
167
+
168
+ ### SiteHeader (`components/chrome/SiteHeader.astro`)
169
+
170
+ - **Purpose:** Canonical site header — logo/wordmark + search typeahead island + saved / marketing
171
+ CTA / auth / account / cart affordances, with `<MegaMenu>` below and a `<MobileNav>` drawer.
172
+ Cart is a **link only** (managed checkout; count is static). Pure component — the caller resolves
173
+ session/theme/feature state and passes props.
174
+ - **Props:** `logo?: ChromeLogo`; `wordmark?: string`; `nav?: ChromeNavItem[]`; `searchAction?:
175
+ string` (omit to hide search); `announcement?: string` (surfaced in the mobile drawer);
176
+ `accountHref?: string`; `authLink?: { label: string; href: string }`; `savedItemsHref?: string`;
177
+ `headerCtas?: ChromeCta[]`; `cartHref?: string`; `cartCount?: number`; `variant?: "light" |
178
+ "dark"`; `sticky?: boolean` (default `true`); `class?: string`.
179
+ - **Dependencies:** `type ChromeNavItem, ChromeLogo, ChromeCta` from `@/lib/chrome/model`;
180
+ `MegaMenu`; islands `SearchTypeahead` (`client:idle`) and `MobileNav` (`client:idle`); `withBase`.
181
+ - **Tokens consumed:** `border-border`, `bg-surface`/`bg-surface/95`, `text-text`, `bg-primary`,
182
+ `text-primary-contrast`, `hover:text-accent`, `bg-accent`, `bg-[var(--color-sale)]`.
183
+ - **Delivery:** platform component + raw-chrome twin `.site-header` / `.header-row`, `.brand`,
184
+ `.primary-nav-bar`.
185
+ - **Usage:**
186
+ ```astro
187
+ <SiteHeader
188
+ wordmark="Sample Store"
189
+ nav={nav}
190
+ searchAction="/search"
191
+ accountHref="/account"
192
+ cartHref="/cart"
193
+ cartCount={2}
194
+ />
195
+ ```
196
+
197
+ ### MegaMenu (`components/chrome/MegaMenu.astro`)
198
+
199
+ - **Purpose:** Primary desktop nav bar — a horizontal `<ul>` of items, each rendered by
200
+ `<NavDropdown>`. Hidden below `md`.
201
+ - **Props:** `items: ChromeNavItem[]`; `ariaLabel?: string` (default `"Primary"`); `class?: string`.
202
+ - **Dependencies:** `type ChromeNavItem` from `@/lib/chrome/model`; `NavDropdown`.
203
+ - **Tokens consumed:** none directly (layout utilities only; token styling delegated to
204
+ `NavDropdown`).
205
+ - **Delivery:** platform component + raw-chrome twin `.primary-nav` / `.primary-nav > ul` /
206
+ `.primary-nav .nav-item`.
207
+ - **Usage:**
208
+ ```astro
209
+ <MegaMenu items={nav} />
210
+ ```
211
+
212
+ ### NavDropdown (`components/chrome/NavDropdown.astro`)
213
+
214
+ - **Purpose:** One primary-nav item in two modes. `mode="bar"` (default) = desktop item with a
215
+ CSS-only hover/focus single-column dropdown (`children`) or multi-column mega panel (`columns`),
216
+ zero-JS and keyboard accessible. `mode="accordion"` = mobile stacked item using native
217
+ `<details>/<summary>`. Supports caret, `badge`, and highlighted `flag`.
218
+ - **Props:** `item: ChromeNavItem`; `mode?: "bar" | "accordion"` (default `"bar"`).
219
+ - **Dependencies:** `type ChromeNavItem, ChromeLink` from `@/lib/chrome/model`; `withBase`.
220
+ - **Tokens consumed:** `text-text`, `text-muted`, `text-accent`, `text-primary`, `border-border`,
221
+ `bg-surface`, `bg-primary`, `text-primary-contrast`, `hover:bg-bg`, `bg-[var(--color-sale)]`,
222
+ `shadow-md`, `rounded-md`, `rounded-sm`.
223
+ - **Delivery:** platform component + raw-chrome twin `.nav-item`, `.mega` / `.mega--wide` /
224
+ `.mega__cols` / `.mega__col`, `.caret`, `.new-badge`, `.is-flag`.
225
+ - **Usage:**
226
+ ```astro
227
+ <NavDropdown item={item} mode="accordion" />
228
+ ```
229
+
230
+ ### SiteFooter (`components/chrome/SiteFooter.astro`)
231
+
232
+ - **Purpose:** Brand block + link columns + newsletter POST form + payment-badge chip row +
233
+ trust/compliance line + legal bottom bar. Payment badges are text chips (CSP-clean, no external
234
+ images). No cart/checkout logic (commerce boundary).
235
+ - **Props:** `columns: FooterColumn[]`; `brand?: { logo?: ChromeLogo; wordmark?: string; contact?:
236
+ string[] }`; `newsletter?: FooterNewsletter`; `payments?: string[]`; `social?: SocialLink[]`;
237
+ `compliance?: string`; `legal?: string`; `bottomLinks?: ChromeLink[]`; `class?: string`.
238
+ - **Dependencies:** `type ChromeLink, FooterColumn, FooterNewsletter, SocialLink, ChromeLogo` from
239
+ `@/lib/chrome/model`; `withBase`.
240
+ - **Tokens consumed:** `border-border`, `bg-surface`, `text-text`, `text-muted`,
241
+ `hover:text-primary`, `bg-bg`, `bg-primary`, `text-primary-contrast`, `font-display`, `font-mono`.
242
+ - **Delivery:** platform component + raw-chrome twin `.site-footer`, `.footer-grid`,
243
+ `.footer-brand`, `.footer-col`, `.footer-news` (+ `__row`), `.footer-attest`, `.trust-marks`,
244
+ `.footer-warning`, `.footer-bottom`.
245
+ - **Usage:**
246
+ ```astro
247
+ <SiteFooter
248
+ brand={{ wordmark: "Sample Store", contact: ["hello@example.com"] }}
249
+ columns={footerCols}
250
+ newsletter={{ title: "Join the community", placeholder: "Email" }}
251
+ payments={["Visa", "Mastercard", "Amex", "PayPal"]}
252
+ social={social}
253
+ compliance="Must be 21+ to purchase. Products intended for adults of legal age only."
254
+ legal="© Sample Store. All rights reserved."
255
+ />
256
+ ```
257
+
258
+ ---
259
+
260
+ ## 3. Commerce — `components/commerce/`
261
+
262
+ Product cards, merchandising rows, price/rating atoms, blog cards, and checkout wiring.
263
+ `ArticleCard`, `BrandGrid`, and `TagFilterCloud` each carry a scoped `<style>` that mirrors a
264
+ class-only block in `public/shared/commerce-chrome.css` (`.article-card`, `.brand-grid`/`.brand-tile`,
265
+ `.tag-cloud`/`.tag-chip`) — keep the two visually equal.
266
+
267
+ ### PriceDisplay (`components/commerce/PriceDisplay.astro`)
268
+
269
+ - **Purpose:** Price atom — struck compare-at when on sale, "From $X" prefix for variant ranges,
270
+ optional sale-percent badge. Accepts a `product` (derives everything) or raw primitives.
271
+ - **Props:** `product?: CatalogProduct`; `amount?: number`; `compareAt?: number | null`; `from?:
272
+ boolean`; `currency?: string`; `locale?: string` (default `"en-US"`); `showBadge?: boolean`
273
+ (default `false`); `size?: "sm" | "md" | "lg"` (default `"md"`); `class?: string`.
274
+ - **Dependencies:** `CatalogProduct` from `@tot/public-runtime`; `formatPrice, discountPercent`
275
+ (`@/lib/format`); `fromPrice, isOnSale` (`@/lib/tot/query`); `Badge`.
276
+ - **Tokens consumed:** `text-muted`, `text-sale`, `font-display`; sale badge carries `--color-sale`.
277
+ - **Delivery:** platform component.
278
+ - **Usage:**
279
+ ```astro
280
+ <PriceDisplay amount={64} compareAt={96} currency="USD" showBadge />
281
+ <PriceDisplay amount={42} from currency="USD" />
282
+ ```
283
+
284
+ ### RatingStars (`components/commerce/RatingStars.astro`)
285
+
286
+ - **Purpose:** 0–5 star rating with fractional fill (single clipped overlay) + accessible label;
287
+ optional numeric review count. Presentation only.
288
+ - **Props:** `rating: number` (0–5); `count?: number`; `max?: number` (default `5`); `size?: "sm" |
289
+ "md"` (default `"sm"`); `showCount?: boolean` (default `true`); `class?: string`.
290
+ - **Dependencies:** none.
291
+ - **Tokens consumed:** `text-[var(--color-border)]` (empty track), `text-[var(--color-accent)]`
292
+ (filled), `text-muted` (count).
293
+ - **Delivery:** platform component.
294
+ - **Usage:**
295
+ ```astro
296
+ <RatingStars rating={4.5} count={128} />
297
+ ```
298
+
299
+ ### ProductCarousel (`components/commerce/ProductCarousel.astro`)
300
+
301
+ - **Purpose:** Horizontal-scroll row of `<ProductCard>`s bound to a `SectionSource`, with optional
302
+ CSS-only tabs (each tab its own source; radio + adjacent-sibling reveal, zero JS). Presets
303
+ (BestSellers / NewArrivals / OnSale) are this + a `SECTION_PRESETS.*` source.
304
+ - **Props:** `source?: SectionSource`; `products?: CatalogProduct[]` (wins over source); `tabs?:
305
+ Tab[]` (wins over both, where `Tab = { label: string; source?: SectionSource; products?:
306
+ CatalogProduct[] }`); `limit?: number` (default `DEFAULT_SECTION_LIMIT`); `title?: string`;
307
+ `eyebrow?: string`; `columns?: 2 | 3 | 4 | 5` (default `4`); `variant?: "default" | "compact"`
308
+ (default `"default"`); `tone?: "bg" | "surface"` (default `"bg"`); `class?: string`.
309
+ - **Dependencies:** `CatalogProduct` from `@tot/public-runtime`; `Section`, `ProductCard`;
310
+ `resolveSectionSource, DEFAULT_SECTION_LIMIT, SectionSource` (`@/lib/sections`). Reads
311
+ `Astro.locals.tot`.
312
+ - **Tokens consumed:** `--color-border`, `--color-muted`, `--color-text`, `--color-primary`,
313
+ `--color-primary-contrast`, `--color-accent`, `--radius-full`, `--font-mono`.
314
+ - **Delivery:** platform component.
315
+ - **Usage:**
316
+ ```astro
317
+ <ProductCarousel title="Best Sellers" eyebrow="Shop" products={products} tone="surface" />
318
+ <ProductCarousel title="Featured" tabs={[{ label: "New", products: newest }, { label: "On Sale", products: onSale }]} />
319
+ ```
320
+
321
+ ### ProductGrid (`components/commerce/ProductGrid.astro`)
322
+
323
+ - **Purpose:** Non-scrolling paginated-look grid of `<ProductCard>`s bound to a `SectionSource`.
324
+ Thin sibling of `ProductCarousel`; `columns` tunes density; hides itself when empty.
325
+ - **Props:** `source?: SectionSource`; `products?: CatalogProduct[]` (wins over source); `limit?:
326
+ number` (default `DEFAULT_SECTION_LIMIT`); `title?: string`; `eyebrow?: string`; `columns?: 2 | 3
327
+ | 4 | 5` (default `4`); `tone?: "bg" | "surface"` (default `"bg"`); `hideWhenEmpty?: boolean`
328
+ (default `true`); `class?: string`.
329
+ - **Dependencies:** `CatalogProduct` from `@tot/public-runtime`; `Section`, `ProductCard`,
330
+ `EmptyState`; `@/lib/sections`. Reads `Astro.locals.tot`.
331
+ - **Tokens consumed:** none directly (token surface from child `ProductCard` / `Section` /
332
+ `EmptyState`).
333
+ - **Delivery:** platform component.
334
+ - **Usage:**
335
+ ```astro
336
+ <ProductGrid title="All Products" products={products} columns={3} />
337
+ ```
338
+
339
+ ### BrandGrid (`components/commerce/BrandGrid.astro`)
340
+
341
+ - **Purpose:** "Shop by Brands" logo wall — grid of tiles linking to brand collections; each tile is
342
+ a rehosted logo image OR a text-wordmark fallback. Optional "View all brands" CTA.
343
+ - **Props:** `brands: Brand[]` where `export interface Brand { label: string; href: string; logoSrc?:
344
+ string; logoRehosted?: boolean; logoWidth?: number; logoHeight?: number }`; `title?: string`;
345
+ `eyebrow?: string`; `tone?: "bg" | "surface"` (default `"bg"`); `viewAllHref?: string`;
346
+ `viewAllLabel?: string` (default `"View all brands"`); `class?: string`.
347
+ - **Dependencies:** `Section`, `Img`; `withBase`.
348
+ - **Tokens consumed:** `--color-surface`, `--color-border`, `--color-text`, `--color-primary`,
349
+ `--radius-md`, `--radius-full`, `--shadow-md`, `--font-display`.
350
+ - **Delivery:** platform component + raw-chrome twin `.brand-grid` / `.brand-tile`.
351
+ - **Usage:**
352
+ ```astro
353
+ <BrandGrid title="Shop by Brands" eyebrow="Find your fav" brands={brands} viewAllHref="/brands" tone="surface" />
354
+ ```
355
+
356
+ ### ArticleCard (`components/commerce/ArticleCard.astro`)
357
+
358
+ - **Purpose:** One blog/article card — featured image + category tag chip + title + excerpt + meta
359
+ (author · date · read-time). Presentational; omits any absent field's row, never fabricates
360
+ authors/dates/excerpts.
361
+ - **Props:** `article: Article` where `export interface Article { title: string; href: string;
362
+ excerpt?: string; image?: ArticleImage; tag?: string; tagHref?: string; author?: string; date?:
363
+ string; dateTime?: string; readMinutes?: number }` and `ArticleImage = { src: string; alt?: string;
364
+ rehosted?: boolean; width?: number; height?: number }`; `as?: "h2" | "h3" | "h4"` (default `"h3"`);
365
+ `class?: string`.
366
+ - **Dependencies:** `Img`; `withBase`.
367
+ - **Tokens consumed:** `--color-surface`, `--color-border`, `--color-surface-2`, `--color-primary`,
368
+ `--color-text`, `--color-muted`, `--radius-lg`, `--shadow-md`, `--font-display`.
369
+ - **Delivery:** platform component + raw-chrome twin `.article-card`.
370
+ - **Usage:**
371
+ ```astro
372
+ <ArticleCard article={article} />
373
+ ```
374
+
375
+ ### TagFilterCloud (`components/commerce/TagFilterCloud.astro`)
376
+
377
+ - **Purpose:** Cloud of tag/category chips for a blog/article index, each a plain link to its
378
+ filtered view (URL-addressable, zero-JS). Active tag filled + `aria-current`; optional leading
379
+ "All" chip clears the filter.
380
+ - **Props:** `tags: TagItem[]` where `export interface TagItem { label: string; href: string; count?:
381
+ number; active?: boolean }`; `title?: string`; `allHref?: string`; `allLabel?: string` (default
382
+ `"All"`); `allActive?: boolean` (default `false`); `ariaLabel?: string` (default `"Filter by
383
+ tag"`); `class?: string`.
384
+ - **Dependencies:** `withBase`.
385
+ - **Tokens consumed:** `--color-muted`, `--color-border`, `--color-surface`, `--color-text`,
386
+ `--color-primary`, `--color-primary-contrast`, `--radius-full`, `--font-display`.
387
+ - **Delivery:** platform component + raw-chrome twin `.tag-cloud` / `.tag-chip`.
388
+ - **Usage:**
389
+ ```astro
390
+ <TagFilterCloud title="Browse by topic" tags={tags} allHref="/blog" allActive={false} />
391
+ ```
392
+
393
+ ### CheckoutLoader (`components/commerce/CheckoutLoader.astro`)
394
+
395
+ - **Purpose:** Injects the managed checkout loader near end of `<body>` to enable the cart drawer +
396
+ hosted checkout. Rendered only for commerce tenants; marketing tenants ship nothing.
397
+ - **Props:** none. Reads `Astro.locals.commerce` (`commerce.loaderSrc`).
398
+ - **Dependencies:** none.
399
+ - **Tokens consumed:** none.
400
+ - **Delivery:** platform component.
401
+ - **Usage:**
402
+ ```astro
403
+ <CheckoutLoader />
404
+ ```
405
+
406
+ ### CartDrawerStyles (`components/commerce/CartDrawerStyles.astro`)
407
+
408
+ - **Purpose:** Emits a nonce'd `<style>` mapping the in-page cart drawer (`#fc` light DOM)
409
+ selectors to the tenant's theme tokens so the cart is brand-matched. Baseline layer plus optional
410
+ tenant `cartDrawerCss` override appended verbatim.
411
+ Commerce tenants only.
412
+ - **Props:** none. Reads `Astro.locals.commerce` (`commerce.cartDrawerCss`) and `Astro.locals.cspNonce`.
413
+ - **Dependencies:** none.
414
+ - **Tokens consumed (scoped under `#fc`):** `--font-body`, `--font-display`, `--color-text`,
415
+ `--color-primary`, `--color-primary-contrast`, `--radius-md`.
416
+ - **Delivery:** platform component.
417
+ - **Usage:**
418
+ ```astro
419
+ <CartDrawerStyles />
420
+ ```
421
+
422
+ ---
423
+
424
+ ## 4. Compliance — `components/compliance/`
425
+
426
+ Platform-owned, data-driven from `tenant.compliance` (decision `compliance-widgets-platform-owned`).
427
+ Every widget renders **nothing** when its config field is absent, **never fabricates legal copy**,
428
+ and emits `role="note"` + a `data-compliance="…"` hook. All share the presentational
429
+ `ComplianceNotice` base except `NicotineWarning` (self-contained). The `tenant.compliance` config
430
+ types live in `packages/public-runtime/src/tenant.ts` (`ComplianceConfig`): `nicotineWarning`,
431
+ `pactAct`, `adultSignature` are `boolean`; `shippingRestriction` is `string`; `minAge` is `number`;
432
+ `prop65`/`purchaseLimit`/`stateEligibility`/`exciseTax` are the config objects below.
433
+
434
+ ### ComplianceNotice (`components/compliance/ComplianceNotice.astro`)
435
+
436
+ - **Purpose:** Shared presentational shell for the compliance widgets. Owns only the visual
437
+ treatment; the fixed legal text is supplied by each sibling via `<slot />`. Adds no copy of its own.
438
+ - **Props:** `variant?: "bar" | "block" | "inline" | "footer"` (default `"block"`); `tone?: "warning"
439
+ | "info" | "neutral"` (default `"neutral"`); `hook: string` (required — the `data-compliance`
440
+ value); `label: string` (required — accessible label); `symbol?: string` (optional leading glyph,
441
+ e.g. `⚠`).
442
+ - **`data-compliance`:** emits the caller's `hook`.
443
+ - **Dependencies:** none (leaf base). Reads no tenant field directly — siblings pass data in.
444
+ - **Tokens consumed:** `--color-border`, `--color-surface-2`, `--color-bg`, `--color-text`,
445
+ `--color-muted`, `--color-warning`, `--color-info`, `--radius-md`.
446
+ - **Delivery:** platform base + raw-chrome counterparts `.compliance-strip` (bar/block),
447
+ `.topbar-warning` (top FDA band), `.footer-warning` (footer band).
448
+ - **Usage:** consumed by the siblings below (not used directly).
449
+
450
+ ### NicotineWarning (`components/compliance/NicotineWarning.astro`)
451
+
452
+ - **Purpose:** FDA-mandated nicotine warning (21 CFR 1143.3). Wording fixed by law, verbatim.
453
+ Self-contained (does not use `ComplianceNotice`).
454
+ - **Props:** `variant?: "bar" | "block"` (default `"bar"`).
455
+ - **`data-compliance`:** `"nicotine-warning"`. Gated by `tenant.compliance.nicotineWarning`.
456
+ - **Dependencies:** none.
457
+ - **Tokens consumed:** `--color-text` (inverted bg/border), `--color-bg` (text on bar),
458
+ `--radius-md`.
459
+ - **Delivery:** platform component + raw-chrome twin `.topbar-warning`.
460
+ - **Usage:**
461
+ ```astro
462
+ <NicotineWarning variant="bar" />
463
+ <NicotineWarning variant="block" />
464
+ ```
465
+
466
+ ### PactActNotice (`components/compliance/PactActNotice.astro`)
467
+
468
+ - **Purpose:** PACT Act compliance notice (ENDS/vapor). Federal-requirement wording fixed here; only
469
+ on/off + age come from config.
470
+ - **Props:** `enabled?: boolean` (default `false`; from `tenant.compliance.pactAct`); `minAge?:
471
+ number` (default `21`; from `tenant.compliance.minAge`); `variant?: "bar" | "block" | "inline" |
472
+ "footer"` (default `"block"`). Passes `tone="info"`.
473
+ - **`data-compliance`:** `"pact-act"`.
474
+ - **Dependencies:** `ComplianceNotice`. Reads `tenant.compliance.pactAct` + `.minAge`.
475
+ - **Tokens consumed:** info tone → `--color-info` (+ base tokens).
476
+ - **Delivery:** platform component + raw-chrome `.compliance-strip` / `.footer-warning`.
477
+ - **Usage:**
478
+ ```astro
479
+ <PactActNotice enabled minAge={21} variant="bar" />
480
+ <PactActNotice enabled minAge={21} variant="footer" />
481
+ ```
482
+
483
+ ### AdultSignatureNotice (`components/compliance/AdultSignatureNotice.astro`)
484
+
485
+ - **Purpose:** Adult-signature-on-delivery notice. Fixed wording; age reflects `minAge`.
486
+ - **Props:** `enabled?: boolean` (default `false`; from `tenant.compliance.adultSignature`); `minAge?:
487
+ number` (default `21`); `variant?: "bar" | "block" | "inline" | "footer"` (default `"inline"`).
488
+ Passes `tone="neutral"`.
489
+ - **`data-compliance`:** `"adult-signature"`.
490
+ - **Dependencies:** `ComplianceNotice`. Reads `tenant.compliance.adultSignature` + `.minAge`.
491
+ - **Tokens consumed:** neutral tone → `--color-text` / `--color-muted`.
492
+ - **Delivery:** platform component + raw-chrome `.compliance-strip`.
493
+ - **Usage:**
494
+ ```astro
495
+ <AdultSignatureNotice enabled minAge={21} variant="inline" />
496
+ ```
497
+
498
+ ### Prop65Warning (`components/compliance/Prop65Warning.astro`)
499
+
500
+ - **Purpose:** California Prop 65 safe-harbor warning. Owns the mandated skeleton, the `⚠` symbol,
501
+ and the official warnings URL. Only harm endpoint(s) + chemical name(s) come from config. Short
502
+ form (`config={true}`) vs long form (when `chemicals` supplied).
503
+ - **Props:** `config?: Prop65Config | boolean` (from `tenant.compliance.prop65`; `true` normalizes to
504
+ `{ harm: "cancer-and-reproductive" }`), where `Prop65Config = { harm: "cancer" | "reproductive" |
505
+ "cancer-and-reproductive"; chemicals?: string[]; url?: string }`; `variant?: "bar" | "block" |
506
+ "inline" | "footer"` (default `"block"`). Passes `tone="warning"`, `symbol="⚠"`.
507
+ - **`data-compliance`:** `"prop65"`.
508
+ - **Dependencies:** `ComplianceNotice`, `type Prop65Config` from `@tot/public-runtime`. Reads
509
+ `tenant.compliance.prop65`.
510
+ - **Tokens consumed:** warning tone → `--color-warning`; underlined `<a>`.
511
+ - **Delivery:** platform component + raw-chrome `.compliance-strip`.
512
+ - **Usage:**
513
+ ```astro
514
+ <Prop65Warning config={true} variant="block" />
515
+ <Prop65Warning config={{ harm: "reproductive", chemicals: ["nicotine"] }} variant="block" />
516
+ ```
517
+
518
+ ### PurchaseLimitNotice (`components/compliance/PurchaseLimitNotice.astro`)
519
+
520
+ - **Purpose:** Purchase-quantity-limit **display** (state/PACT order caps). Display surface only —
521
+ enforcement stays product/commerce-owned.
522
+ - **Props:** `config?: PurchaseLimitConfig` (from `tenant.compliance.purchaseLimit`), where
523
+ `PurchaseLimitConfig = { text: string; reason?: string }`; `variant?: "bar" | "block" | "inline" |
524
+ "footer"` (default `"inline"`). Passes `tone="neutral"`.
525
+ - **`data-compliance`:** `"purchase-limit"`.
526
+ - **Dependencies:** `ComplianceNotice`, `type PurchaseLimitConfig` from `@tot/public-runtime`.
527
+ - **Tokens consumed:** neutral tone via base.
528
+ - **Delivery:** platform component + raw-chrome `.compliance-strip`.
529
+ - **Usage:**
530
+ ```astro
531
+ <PurchaseLimitNotice config={{ text: "2 units per order", reason: "per state law" }} variant="inline" />
532
+ ```
533
+
534
+ ### StateEligibilityNotice (`components/compliance/StateEligibilityNotice.astro`)
535
+
536
+ - **Purpose:** State/jurisdiction sale-eligibility **display**. Shows tenant-configured messaging;
537
+ does **not** decide eligibility for a cart/address. Precedence: explicit `note` wins, else composes
538
+ from `restrictedStates` or `eligibleStates`.
539
+ - **Props:** `config?: StateEligibilityConfig` (from `tenant.compliance.stateEligibility`), where
540
+ `StateEligibilityConfig = { restrictedStates?: string[]; eligibleStates?: string[]; note?: string }`;
541
+ `variant?: "bar" | "block" | "inline" | "footer"` (default `"block"`). Passes `tone="warning"`,
542
+ `symbol="⚠"`.
543
+ - **`data-compliance`:** `"state-eligibility"`.
544
+ - **Dependencies:** `ComplianceNotice`, `type StateEligibilityConfig` from `@tot/public-runtime`.
545
+ - **Tokens consumed:** warning tone → `--color-warning`.
546
+ - **Delivery:** platform component + raw-chrome `.compliance-strip`.
547
+ - **Usage:**
548
+ ```astro
549
+ <StateEligibilityNotice config={{ restrictedStates: ["UT", "AR", "VT"] }} variant="block" />
550
+ ```
551
+
552
+ ### ShippingRestrictionNotice (`components/compliance/ShippingRestrictionNotice.astro`)
553
+
554
+ - **Purpose:** Shipping-restriction **display** (PACT no-ship states, adult signature, carrier
555
+ limits). Renders the tenant's configured text; the ship/no-ship decision stays product-owned. This
556
+ is the platform component behind the footer shipping band the app Layout previously inlined.
557
+ - **Props:** `text?: string` (from `tenant.compliance.shippingRestriction`, display text only);
558
+ `variant?: "bar" | "block" | "inline" | "footer"` (default `"footer"`). Passes `tone="neutral"`.
559
+ - **`data-compliance`:** `"shipping-restriction"`.
560
+ - **Dependencies:** `ComplianceNotice`. Reads `tenant.compliance.shippingRestriction` (`string`).
561
+ - **Tokens consumed:** neutral tone; footer variant uses `--color-bg`, `--color-border`,
562
+ `--color-muted`.
563
+ - **Delivery:** platform component + raw-chrome twin `.footer-warning`.
564
+ - **Usage:**
565
+ ```astro
566
+ <ShippingRestrictionNotice text="Adult signature (21+) required on delivery. Not shipped via USPS." variant="footer" />
567
+ ```
568
+
569
+ ### ExciseTaxNotice (`components/compliance/ExciseTaxNotice.astro`)
570
+
571
+ - **Purpose:** Excise-tax **display** cue. Surfaces that excise taxes apply (+ optional
572
+ per-jurisdiction breakdown); **never computes tax** — calculation is product/commerce-owned.
573
+ - **Props:** `config?: ExciseTaxConfig` (from `tenant.compliance.exciseTax`), where `ExciseTaxConfig =
574
+ { note: string; jurisdictions?: Array<{ label: string; detail: string }> }`; `variant?: "bar" |
575
+ "block" | "inline" | "footer"` (default `"inline"`). Passes `tone="info"`. Renders only if
576
+ `config.note` is non-empty.
577
+ - **`data-compliance`:** `"excise-tax"`.
578
+ - **Dependencies:** `ComplianceNotice`, `type ExciseTaxConfig` from `@tot/public-runtime`.
579
+ - **Tokens consumed:** info tone → `--color-info`; jurisdiction spans use `--color-muted`.
580
+ - **Delivery:** platform component + raw-chrome `.compliance-strip`.
581
+ - **Usage:**
582
+ ```astro
583
+ <ExciseTaxNotice config={{ note: "Applicable state vapor excise taxes are calculated at checkout." }} variant="inline" />
584
+ <ExciseTaxNotice config={{ note: "State excise taxes apply:", jurisdictions: [{ label: "CA", detail: "$0.05/ml" }, { label: "NY", detail: "20% wholesale" }] }} variant="block" />
585
+ ```
586
+
587
+ ---
588
+
589
+ ## 5. Membership — `components/membership/`
590
+
591
+ Presentational + **education-only** (no pricing/checkout/subscription mechanics; CTAs are plain
592
+ handoff links). Figures are **illustrative by default**: a tier's figure is an example unless the
593
+ tier asserts `real: true` (`isIllustrative(tier)` returns `tier.real !== true`). Model in
594
+ `lib/membership/model.ts`. All emit `data-widget="…"`.
595
+
596
+ Model types (`lib/membership/model.ts`):
597
+
598
+ - `TierFigure` — `{ value: string; caption?: string }` (`value` is display text like `"$0"`,
599
+ `"$19/mo"`, `"Save 15%"` — never a computed number).
600
+ - `TierFeature` — `{ label: string; included?: boolean }` (`true`→check, `false`→dash+strikethrough,
601
+ omitted→plain bullet).
602
+ - `TierCta` — `{ label: string; href: string }` (education/handoff link only).
603
+ - `Tier` — `{ name: string; tagline?: string; figure?: TierFigure; features?: TierFeature[]; cta?:
604
+ TierCta; featured?: boolean; badge?: string; real?: boolean }`.
605
+ - `ComparisonRow` — `{ label: string; cells: Array<boolean | string | null> }` (aligned to `tiers`
606
+ by index: boolean→check/dash, string→text, null→em dash).
607
+ - `isIllustrative(tier: Pick<Tier, "real">): boolean` → `tier.real !== true`.
608
+
609
+ ### IllustrativeLabel (`components/membership/IllustrativeLabel.astro`)
610
+
611
+ - **Purpose:** The first-class "these figures are examples, not an offer" affordance shared by
612
+ `TierCards`, `TierComparisonTable`, and `SavingsExplainer`.
613
+ - **Props:** `as?: "badge" | "note"` (default `"badge"`); `text?: string` (override copy).
614
+ - **`data-illustrative`:** `"badge"` or `"note"` (note: `data-illustrative`, not `data-widget`).
615
+ - **Dependencies:** none (leaf; driven by the parent widget's illustrative state).
616
+ - **Tokens consumed:** `--color-muted` (note), `--color-info` (badge border + text), `--radius-full`.
617
+ - **Delivery:** platform component (no raw-chrome twin).
618
+ - **Usage:** rendered internally by the tier/savings widgets; `<IllustrativeLabel as="badge" />`.
619
+
620
+ ### TierCards (`components/membership/TierCards.astro`)
621
+
622
+ - **Purpose:** Membership/pricing tier comparison card row (e.g. Guest / Member / Autopilot). Shows
623
+ tiers, headline figures, feature bullets; each CTA is a plain handoff link. When any shown tier is
624
+ illustrative, the header carries an `IllustrativeLabel` badge, each illustrative figure is prefixed
625
+ "e.g." + `data-illustrative="figure"`, and a footnote spells it out.
626
+ - **Props:** `tiers: Tier[]` (required); `eyebrow?: string`; `heading?: string`; `subhead?: string`.
627
+ - **`data-widget`:** `"tier-cards"` (per-tier `data-tier={tier.name}`).
628
+ - **Dependencies:** `type Tier` + `isIllustrative` (`@/lib/membership/model`); `IllustrativeLabel`.
629
+ No tenant-config field (tenant supplies `tiers` on a membership page).
630
+ - **Tokens consumed:** `--color-accent-ink`, `--color-text`, `--color-muted`, `--color-surface`,
631
+ `--color-border`, `--color-primary`, `--color-primary-contrast`, `--color-success`, `--radius-lg`,
632
+ `--radius-full`, `--shadow-md`.
633
+ - **Delivery:** platform component (no raw-chrome twin).
634
+ - **Usage:**
635
+ ```astro
636
+ <TierCards eyebrow="Membership" heading="Choose how you shop" subhead="Compare guest checkout with our membership tiers." tiers={tiers} />
637
+ ```
638
+
639
+ ### TierComparisonTable (`components/membership/TierComparisonTable.astro`)
640
+
641
+ - **Purpose:** Feature matrix companion to `TierCards` — tiers across the top, features down the side,
642
+ one cell per (feature × tier). Same illustrative-figure affordance. Horizontally scrollable.
643
+ - **Props:** `tiers: Tier[]` (required); `rows: ComparisonRow[]` (required, cells align to `tiers` by
644
+ index); `eyebrow?: string`; `heading?: string`.
645
+ - **`data-widget`:** `"tier-comparison-table"`.
646
+ - **Dependencies:** `type Tier, ComparisonRow` + `isIllustrative` (`@/lib/membership/model`);
647
+ `IllustrativeLabel`.
648
+ - **Tokens consumed:** `--color-accent-ink`, `--color-text`, `--color-muted`, `--color-surface-2`,
649
+ `--color-border`, `--color-success`, `--radius-md`.
650
+ - **Delivery:** platform component (no raw-chrome twin).
651
+ - **Usage:**
652
+ ```astro
653
+ <TierComparisonTable eyebrow="Compare" heading="What each tier includes" tiers={tiers} rows={comparisonRows} />
654
+ ```
655
+
656
+ ---
657
+
658
+ ## 6. Subscription — `components/subscription/`
659
+
660
+ Presentational + **education-only**: explain recurring/Autopilot ordering and hand off to the
661
+ tenant's product-owned subscribe/manage flow; **no scheduling/billing/cancellation mechanics**. The
662
+ savings figure is illustrative by default (same discipline as §5). Model in
663
+ `lib/subscription/model.ts`. All emit `data-widget="…"`.
664
+
665
+ Model types (`lib/subscription/model.ts`):
666
+
667
+ - `SubscriptionStep` — `{ title: string; description: string }`.
668
+ - `SubscriptionFigure` — `{ value: string; caption?: string; real?: boolean }` (`value` is display
669
+ text like `"Save 15%"` — never computed).
670
+ - `SubscriptionManageEntry` — `{ label: string; description?: string; href: string }` (handoff URL).
671
+ - `isIllustrative(figure: Pick<SubscriptionFigure, "real">): boolean` → `figure.real !== true`.
672
+
673
+ ### HowItWorksSteps (`components/subscription/HowItWorksSteps.astro`)
674
+
675
+ - **Purpose:** Autopilot/subscription "how it works" explainer (numbered steps). Ships no
676
+ scheduling/billing mechanics.
677
+ - **Props:** `steps: SubscriptionStep[]` (required); `eyebrow?: string`; `heading?: string`;
678
+ `subhead?: string`.
679
+ - **`data-widget`:** `"subscription-how-it-works"`.
680
+ - **Dependencies:** `type SubscriptionStep` (`@/lib/subscription/model`).
681
+ - **Tokens consumed:** `--color-accent-ink`, `--color-text`, `--color-muted`, `--color-border`,
682
+ `--color-surface`, `--color-primary`, `--color-primary-contrast`, `--radius-lg`, `--radius-full`.
683
+ - **Delivery:** platform component (no raw-chrome twin).
684
+ - **Usage:**
685
+ ```astro
686
+ <HowItWorksSteps eyebrow="Autopilot" heading="How Autopilot works" subhead="Set it up once — we handle the rest." steps={steps} />
687
+ ```
688
+
689
+ ### SavingsExplainer (`components/subscription/SavingsExplainer.astro`)
690
+
691
+ - **Purpose:** Autopilot/subscription savings headline (e.g. "Save 15% — vs. one-time purchase") +
692
+ supporting bullets + plain handoff CTA. Figure illustrative by default → "e.g." affordance +
693
+ `IllustrativeLabel` when illustrative.
694
+ - **Props:** `figure: SubscriptionFigure` (required); `eyebrow?: string`; `heading?: string`;
695
+ `bullets?: string[]`; `cta?: { label: string; href: string }` (education/handoff, never a checkout
696
+ mechanic).
697
+ - **`data-widget`:** `"subscription-savings-explainer"`.
698
+ - **Dependencies:** `type SubscriptionFigure` + `isIllustrative` (`@/lib/subscription/model`);
699
+ `IllustrativeLabel` (cross-imports the membership affordance).
700
+ - **Tokens consumed:** `--color-accent-ink`, `--color-text`, `--color-muted`, `--color-border`,
701
+ `--color-surface`, `--color-success`, `--color-primary`, `--color-primary-contrast`, `--radius-lg`,
702
+ `--radius-full`.
703
+ - **Delivery:** platform component (no raw-chrome twin).
704
+ - **Usage:**
705
+ ```astro
706
+ <SavingsExplainer eyebrow="Autopilot savings" heading="Save on every order" figure={{ value: "Save 15%", caption: "vs. one-time purchase" }} bullets={["Free shipping on every Autopilot order", "Pause or cancel anytime"]} cta={{ label: "How Autopilot works", href: "/autopilot" }} />
707
+ ```
708
+
709
+ ### ManageSubscriptionEntry (`components/subscription/ManageSubscriptionEntry.astro`)
710
+
711
+ - **Purpose:** Set of manage-subscription entry points (e.g. "Update delivery date", "Pause or
712
+ cancel"), each a plain link into the tenant's product-owned manage flow. Renders no controls of its
713
+ own.
714
+ - **Props:** `entries: SubscriptionManageEntry[]` (required); `eyebrow?: string`; `heading?: string`;
715
+ `subhead?: string`.
716
+ - **`data-widget`:** `"subscription-manage-entry"`.
717
+ - **Dependencies:** `type SubscriptionManageEntry` (`@/lib/subscription/model`).
718
+ - **Tokens consumed:** `--color-accent-ink`, `--color-text`, `--color-muted`, `--color-border`,
719
+ `--color-surface`, `--color-primary`, `--radius-lg`.
720
+ - **Delivery:** platform component (no raw-chrome twin).
721
+ - **Usage:**
722
+ ```astro
723
+ <ManageSubscriptionEntry eyebrow="Manage" heading="Manage your Autopilot" entries={entries} />
724
+ ```
725
+
726
+ ---
727
+
728
+ ## 7. Marketing / widget blocks — `components/marketing/`
729
+
730
+ The block library a `siteType: "marketing"` tenant composes pages from — token-driven, distinct from
731
+ the ported `mkt.css` composition layer. Every block takes a discriminated-union `block` view-model
732
+ from `lib/storyblok/content-model.ts` (the `MarketingBlock` union) plus, where it emits links,
733
+ `basePath: string`. `MarketingCta = { label: string; href: string; variant?: "primary" |
734
+ "secondary" | "ghost" }`.
735
+
736
+ ### MarketingBlocks (`components/marketing/MarketingBlocks.astro`)
737
+
738
+ - **Purpose:** The marketing page renderer — dispatches a `MarketingBlock[]` discriminated union to
739
+ its block components by `block.component`. Tenant-agnostic; reused by homepage + future marketing
740
+ pages.
741
+ - **Props:** `blocks: MarketingBlock[]`; `basePath: string`.
742
+ - **Dispatch table** (`block.component` → component): `"marketing_hero"` → `MarketingHero`;
743
+ `"trust_bar"` → `TrustBar`; `"split_compare"` → `SplitCompare`; `"steps"` → `Steps`; `"card_grid"`
744
+ → `CardGrid`; `"integrations"` → `Integrations`; `"proof_strip"` → `ProofStrip`; `"testimonials"`
745
+ → `Testimonials`; `"faq"` → `Faq`; `"cta_band"` → `CtaBand`; default → `null`.
746
+ - **Dependencies:** all 10 block components; `MarketingBlock` type.
747
+ - **Tokens consumed:** none directly (delegates to children).
748
+ - **Delivery:** platform component.
749
+ - **Usage:**
750
+ ```astro
751
+ <MarketingBlocks blocks={blocks} basePath={basePath} />
752
+ ```
753
+
754
+ ### MarketingHero (`components/marketing/MarketingHero.astro`)
755
+
756
+ - **Purpose:** Signature top-of-page hero for marketing tenants — eyebrow + oversized headline +
757
+ subhead/body + CTAs + trust chips; optional right-hand "product moment" panel of mono log lines.
758
+ - **Props:** `block: MarketingHeroBlock` (`{ component: "marketing_hero"; eyebrow?: string; headline:
759
+ string; subhead?: string; body?: string; trust?: string[]; ctas?: MarketingCta[]; panel?: { title?:
760
+ string; lines: string[] } }`); `basePath: string`.
761
+ - **Dependencies:** `MarketingCtas`.
762
+ - **Tokens consumed:** `--color-bg`, `--color-text`, `--color-muted`, `--color-accent`,
763
+ `--color-border`, `--color-surface`, `--radius-lg`, `--shadow-lg`; helper classes `container-prose`,
764
+ `display-xl`, `eyebrow eyebrow--tick`.
765
+ - **Delivery:** platform component.
766
+ - **Usage:**
767
+ ```astro
768
+ <MarketingHero block={block} basePath={basePath} />
769
+ ```
770
+
771
+ ### MarketingCtas (`components/marketing/MarketingCtas.astro`)
772
+
773
+ - **Purpose:** Row of CTA buttons for marketing blocks. External `http[s]` hrefs open a new tab with
774
+ `rel="noopener noreferrer"`; root-absolute same-origin hrefs get the base-path prefix. Three variant
775
+ styles.
776
+ - **Props:** `ctas?: MarketingCta[]` (default `[]`); `basePath: string`; `class?: string`.
777
+ - **Dependencies:** `withBase`.
778
+ - **Tokens consumed:** primary → `bg-accent` + `--color-primary-contrast` + `--shadow-sm`; secondary
779
+ → `--color-border` + `--color-surface` + `--color-text` + hover `--color-primary`; ghost →
780
+ `--color-primary`; `rounded-full`.
781
+ - **Delivery:** platform component (used internally by `MarketingHero` / `CtaBand`).
782
+ - **Usage:**
783
+ ```astro
784
+ <MarketingCtas ctas={block.ctas} basePath={basePath} class="mt-8" />
785
+ ```
786
+
787
+ ### TrustBar (`components/marketing/TrustBar.astro`)
788
+
789
+ - **Purpose:** Quiet horizontal strip of proof tokens (countries, standards, ratings). Plain text,
790
+ mono, dot-separated.
791
+ - **Props:** `block: TrustBarBlock` (`{ component: "trust_bar"; items: string[] }`).
792
+ - **Dependencies:** `TrustBarBlock` type.
793
+ - **Tokens consumed:** `--color-border`, `--color-surface`, `--color-muted`.
794
+ - **Delivery:** platform component.
795
+ - **Usage:**
796
+ ```astro
797
+ <TrustBar block={{ component: "trust_bar", items: ["200+ countries", "SOC 2 Type II"] }} />
798
+ ```
799
+
800
+ ### SplitCompare (`components/marketing/SplitCompare.astro`)
801
+
802
+ - **Purpose:** Before/after two-column comparison — `before` reads as the problem (muted, struck
803
+ items), `after` as the solution (accent border, ✓).
804
+ - **Props:** `block: SplitCompareBlock` (`{ component: "split_compare"; eyebrow?: string; title:
805
+ string; subhead?: string; before: { title: string; items: string[] }; after: { title: string;
806
+ items: string[] } }`).
807
+ - **Dependencies:** `Section`.
808
+ - **Tokens consumed:** `--color-muted`, `--color-border`, `--color-surface`, `--color-primary`,
809
+ `--color-text`, `--color-accent`, `--radius-lg`, `--shadow-md`.
810
+ - **Delivery:** platform component.
811
+ - **Usage:**
812
+ ```astro
813
+ <SplitCompare block={block} />
814
+ ```
815
+
816
+ ### Steps (`components/marketing/Steps.astro`)
817
+
818
+ - **Purpose:** Numbered/emoji journey (e.g. a checkout verification flow) — a grid of step cards, each
819
+ with a circular number/icon badge + label + optional detail.
820
+ - **Props:** `block: StepsBlock` (`{ component: "steps"; eyebrow?: string; title: string; subhead?:
821
+ string; steps: { icon?: string; label: string; detail?: string }[] }`).
822
+ - **Dependencies:** `Section`.
823
+ - **Tokens consumed:** `--color-muted`, `--color-border`, `--color-bg`, `--color-primary`,
824
+ `--color-primary-contrast`, `--color-text`, `--radius-lg`.
825
+ - **Delivery:** platform component.
826
+ - **Usage:**
827
+ ```astro
828
+ <Steps block={block} />
829
+ ```
830
+
831
+ ### CardGrid (`components/marketing/CardGrid.astro`)
832
+
833
+ - **Purpose:** Responsive grid of capability/industry/feature cards (one component, three variants
834
+ tuning density/columns). Cards optionally link internally (base-path) or externally.
835
+ - **Props:** `block: CardGridBlock` (`{ component: "card_grid"; anchor?: string; eyebrow?: string;
836
+ title: string; subhead?: string; variant?: "capability" | "industry" | "feature"; cards: { icon?:
837
+ string; title: string; body?: string; meta?: string; href?: string }[]; footnote?: string }`);
838
+ `basePath: string`.
839
+ - **Dependencies:** `Section`; `withBase`.
840
+ - **Tokens consumed:** `--color-muted`, `--color-border`, `--color-surface`, `--color-text`,
841
+ `--radius-lg`, `--shadow-md` (hover).
842
+ - **Delivery:** platform component.
843
+ - **Usage:**
844
+ ```astro
845
+ <CardGrid block={block} basePath={basePath} />
846
+ ```
847
+
848
+ ### Integrations (`components/marketing/Integrations.astro`)
849
+
850
+ - **Purpose:** "Fits the store you already run" section — a strip of platform name/link chips,
851
+ optional implementation points (✓ list), and an optional safe code sample.
852
+ - **Props:** `block: IntegrationsBlock` (`{ component: "integrations"; eyebrow?: string; title:
853
+ string; subhead?: string; platforms: { label: string; href?: string }[]; points?: string[]; code?:
854
+ CodeSample }`); `basePath: string`.
855
+ - **Dependencies:** `Section`; `CodeSample`; `withBase`.
856
+ - **Tokens consumed:** `--color-muted`, `--color-border`, `--color-bg`, `--color-text`,
857
+ `--color-primary` (hover), `--color-accent` (✓).
858
+ - **Delivery:** platform component.
859
+ - **Usage:**
860
+ ```astro
861
+ <Integrations block={block} basePath={basePath} />
862
+ ```
863
+
864
+ ### CodeSample (`components/marketing/CodeSample.astro`)
865
+
866
+ - **Purpose:** Display-only fenced code block; code rendered as **escaped** text inside `<pre><code>`
867
+ (never `set:html`). Never embed secrets. Caption + language label.
868
+ - **Props:** `sample: CodeSample` where `export interface CodeSample { language?: string; code: string;
869
+ caption?: string }`.
870
+ - **Dependencies:** `CodeSample` type.
871
+ - **Tokens consumed:** `--radius-lg`, `--color-border`, `--color-text` (dark code surface); code text
872
+ colors are deliberately not tokenized.
873
+ - **Delivery:** platform component (usually nested inside `Integrations`).
874
+ - **Usage:**
875
+ ```astro
876
+ <CodeSample sample={{ language: "js", caption: "Client-side embed", code: "await verify({ minAge: 21 });" }} />
877
+ ```
878
+
879
+ ### ProofStrip (`components/marketing/ProofStrip.astro`)
880
+
881
+ - **Purpose:** Privacy/trust proof band painted in the primary color (a single strong moment) — bold
882
+ statement + supporting copy + grid of proof bullets.
883
+ - **Props:** `block: ProofStripBlock` (`{ component: "proof_strip"; eyebrow?: string; title: string;
884
+ body?: string; items?: string[] }`).
885
+ - **Dependencies:** `ProofStripBlock` type.
886
+ - **Tokens consumed:** `--color-primary`, `--color-primary-contrast`, `--radius-md`; chips use
887
+ `color-mix(... --color-primary-contrast ...)`; helper classes `container-prose`, `display-lg`.
888
+ - **Delivery:** platform component.
889
+ - **Usage:**
890
+ ```astro
891
+ <ProofStrip block={block} />
892
+ ```
893
+
894
+ ### Testimonials (`components/marketing/Testimonials.astro`)
895
+
896
+ - **Purpose:** Outcome/quote cards with optional stat lead-in + attribution, plus an optional row of
897
+ rating tokens.
898
+ - **Props:** `block: TestimonialsBlock` (`{ component: "testimonials"; eyebrow?: string; title?:
899
+ string; quotes: { stat?: string; quote: string; attribution?: string }[]; ratings?: string[] }`).
900
+ - **Dependencies:** `Section`.
901
+ - **Tokens consumed:** `--color-border`, `--color-surface`, `--color-accent` (stat), `--color-text`,
902
+ `--color-muted`, `--radius-lg`.
903
+ - **Delivery:** platform component.
904
+ - **Usage:**
905
+ ```astro
906
+ <Testimonials block={block} />
907
+ ```
908
+
909
+ ### Faq (`components/marketing/Faq.astro`)
910
+
911
+ - **Purpose:** No-JS accordion of Q/A pairs using native `<details>`/`<summary>`; emits FAQPage
912
+ JSON-LD for rich results. Answers plain text (auto-escaped).
913
+ - **Props:** `block: FaqBlock` (`{ component: "faq"; eyebrow?: string; title: string; lede?: string;
914
+ items: { q: string; a: string }[] }`).
915
+ - **Dependencies:** `Section`.
916
+ - **Tokens consumed:** `divide-[var(--color-border)]`, `--color-text`, `--color-accent` (the `+`
917
+ toggle), `--color-muted`.
918
+ - **Delivery:** platform component.
919
+ - **Usage:**
920
+ ```astro
921
+ <Faq block={block} />
922
+ ```
923
+
924
+ ### CtaBand (`components/marketing/CtaBand.astro`)
925
+
926
+ - **Purpose:** Final conversion band on the surface tone — headline + optional subhead + centered CTA
927
+ row.
928
+ - **Props:** `block: CtaBandBlock` (`{ component: "cta_band"; title: string; subhead?: string; ctas?:
929
+ MarketingCta[] }`); `basePath: string`.
930
+ - **Dependencies:** `MarketingCtas`.
931
+ - **Tokens consumed:** `--color-surface`, `--color-muted`; helper classes `container-prose`,
932
+ `display-lg`; CTA tokens via `MarketingCtas`.
933
+ - **Delivery:** platform component.
934
+ - **Usage:**
935
+ ```astro
936
+ <CtaBand block={block} basePath={basePath} />
937
+ ```
938
+
939
+ ---
940
+
941
+ ## 8. PLP controls — `components/plp/`
942
+
943
+ URL-addressable faceted-search chrome for a collection page — every control is a real link or GET
944
+ form carrying the query, so filtering needs no JS and is back-button safe. All take `query:
945
+ ListProductsQuery` + `basePath: string` (from `@tot/public-runtime`) and share `@/lib/url`
946
+ (`toggleFacet`, `buildQueryString`). None have a raw-chrome twin (PLP-body, not site chrome).
947
+
948
+ ### FacetSidebar (`components/plp/FacetSidebar.astro`)
949
+
950
+ - **Purpose:** Faceted filtering UI — each facet value is a real link toggling a querystring param;
951
+ price range is a GET form preserving other params via hidden inputs. Facets ordered: options, then
952
+ tags, availability last.
953
+ - **Props:** `facets: Facet[]` (from `@tot/public-runtime`); `query: ListProductsQuery`; `basePath:
954
+ string`.
955
+ - **Dependencies:** `type Facet, ListProductsQuery` from `@tot/public-runtime`; `toggleFacet,
956
+ buildQueryString` (`@/lib/url`).
957
+ - **Tokens consumed:** `--color-text`, `--color-muted`, `--color-accent-ink` (hover), `--radius-sm`,
958
+ `--color-primary`, `--color-primary-contrast`, `--color-border`, `--color-bg`; `.eyebrow` class.
959
+ - **Delivery:** platform component.
960
+ - **Usage:**
961
+ ```astro
962
+ <FacetSidebar facets={facets} query={query} basePath="/collections/bestsellers" />
963
+ ```
964
+
965
+ ### ActiveFilters (`components/plp/ActiveFilters.astro`)
966
+
967
+ - **Purpose:** Removable filter chips above the grid; each chip is a link that drops exactly one
968
+ filter, plus a "Clear all" link.
969
+ - **Props:** `query: ListProductsQuery`; `basePath: string`.
970
+ - **Dependencies:** `type ListProductsQuery` from `@tot/public-runtime`; `toggleFacet,
971
+ buildQueryString` (`@/lib/url`).
972
+ - **Tokens consumed:** `--color-border`, `--color-surface`, `--color-accent-ink` (hover),
973
+ `--color-muted`, `--color-primary-contrast` (group-hover fill).
974
+ - **Delivery:** platform component.
975
+ - **Usage:**
976
+ ```astro
977
+ <ActiveFilters query={query} basePath="/collections/bestsellers" />
978
+ ```
979
+
980
+ ### SortSelect (`components/plp/SortSelect.astro`)
981
+
982
+ - **Purpose:** Sort control — a plain GET `<form>` `<select>` that auto-submits on change (nonce'd
983
+ `is:inline` script; `<noscript>` Apply fallback). Other query params ride along as hidden inputs.
984
+ Fixed options: `featured`, `newest`, `price-asc`, `price-desc`, `title-asc`.
985
+ - **Props:** `query: ListProductsQuery`; `basePath: string`.
986
+ - **Dependencies:** `type ListProductsQuery` from `@tot/public-runtime`. Reads `Astro.locals.cspNonce`.
987
+ - **Tokens consumed:** `--color-muted`, `--radius-sm`, `--color-border`, `--color-surface`;
988
+ `font-display`.
989
+ - **Delivery:** platform component.
990
+ - **Usage:**
991
+ ```astro
992
+ <SortSelect query={query} basePath="/collections/bestsellers" />
993
+ ```
994
+
995
+ ### Pagination (`components/plp/Pagination.astro`)
996
+
997
+ - **Purpose:** SEO-friendly numbered pagination (compact 5-page window with first/last + ellipses) +
998
+ a "Load more" next-page link. All plain anchors carrying the full query — crawlable, back-button
999
+ safe, no-JS correct.
1000
+ - **Props:** `query: ListProductsQuery`; `basePath: string`; `page: number`; `pageSize: number`;
1001
+ `total: number`.
1002
+ - **Dependencies:** `type ListProductsQuery` from `@tot/public-runtime`; `buildQueryString`
1003
+ (`@/lib/url`).
1004
+ - **Tokens consumed:** `--color-border`, `--color-surface`, `--color-accent` (hover),
1005
+ `--color-accent-ink`, `--radius-sm`, `--color-muted`, `--color-primary`, `--color-primary-contrast`.
1006
+ - **Delivery:** platform component.
1007
+ - **Usage:**
1008
+ ```astro
1009
+ <Pagination query={query} basePath="/collections/bestsellers" page={2} pageSize={12} total={58} />
1010
+ ```
1011
+
1012
+ ---
1013
+
1014
+ ## 9. Islands — `components/islands/`
1015
+
1016
+ Client-hydrated Preact `.tsx`. Every island is tenant-agnostic and reads only fixtures / `localStorage`
1017
+ / its own props — **no live catalog, cart, or ToT calls** (`SearchTypeahead` fetches the tenant's own
1018
+ `/search` endpoint). Cross-island wiring uses `window` CustomEvents; `localStorage` keys in play:
1019
+ `"tot:age-affirmed"`, `"tot:recently-viewed"`, `"tot:wishlist"`.
1020
+
1021
+ ### AgeGate (`components/islands/AgeGate.tsx`)
1022
+
1023
+ - **Purpose:** Soft `minAge`+ affirmation overlay, shown once per device for regulated tenants.
1024
+ Front-door affirmation only — real ToT identity/age verification is a later phase (the "Yes" path is
1025
+ where that handoff will live).
1026
+ - **Props:** `{ minAge: number; brand: string }`. Directive: `client:load`.
1027
+ - **State:** reads/writes `localStorage` `"tot:age-affirmed"` (`"yes"`); local state machine; locks
1028
+ page scroll while asking. No network.
1029
+ - **Dependencies:** `preact/hooks`.
1030
+ - **Tokens consumed:** `--color-text` (backdrop mix), `--radius-lg`, `--color-surface`, `--shadow-lg`,
1031
+ `--radius-md`, `--color-primary`, `--color-primary-contrast`, `--color-border`, `--color-muted`;
1032
+ classes `eyebrow eyebrow--tick`, `display-md`, `link-accent`.
1033
+ - **Delivery:** platform island + raw-chrome twin `.age-gate` / `.age-gate__panel` / `__mark` /
1034
+ `__actions` / `__deny` / `__fine` / `.is-open` / `.is-denied` (same `localStorage` key + closed
1035
+ shadow-DOM tamper pattern, produced server-side by `lib/rawChrome.ts`).
1036
+ - **Usage:**
1037
+ ```astro
1038
+ <AgeGate client:load minAge={21} brand={tenant.brandName} />
1039
+ ```
1040
+
1041
+ ### IsolatedAgeGate (`components/islands/IsolatedAgeGate.tsx`)
1042
+
1043
+ - **Purpose:** Tamper-resistant wrapper that mounts `<AgeGate>` inside a **closed shadow root** so no
1044
+ tenant CSS/JS can select, restyle, or hide it. A light-DOM host `<div data-tot-compliance>` is
1045
+ re-asserted by a `MutationObserver` (restored if detached / `display:none` / `hidden`). "Customize
1046
+ anything except what keeps you legal," as a technical guarantee — this is the production age-gate
1047
+ mount for component pages.
1048
+ - **Props:** `{ minAge: number; brand: string }` (passed through). Returns `null`; mounts via
1049
+ `useEffect`. Directive: `client:load`.
1050
+ - **State:** no own state; clones `link[rel=stylesheet]` into the shadow root (`:root` tokens inherit
1051
+ across the boundary); inherits AgeGate's localStorage behavior. No network.
1052
+ - **Dependencies:** `preact` (`render`), `preact/hooks`; `./AgeGate`.
1053
+ - **Tokens consumed:** none of its own (delegates to AgeGate; relies on `:root` token inheritance).
1054
+ - **Delivery:** platform island (server/raw-chrome equivalent is the closed-shadow gate in
1055
+ `CHROME_BEHAVIOR_JS`).
1056
+ - **Usage:**
1057
+ ```astro
1058
+ <IsolatedAgeGate client:load minAge={21} brand={tenant.brandName} />
1059
+ ```
1060
+
1061
+ ### VariantSelector (`components/islands/VariantSelector.tsx`)
1062
+
1063
+ - **Purpose:** PDP variant picker (the centerpiece interaction) — one radiogroup per product option;
1064
+ resolves the matching variant, disables unbuyable combinations, updates a polite live region
1065
+ (price/compare-at/SKU/stock), and drives cross-island events. Add-to-cart is a real checkout form
1066
+ when a signed payload is wired, else a disabled placeholder.
1067
+ - **Props:** `{ variants: CatalogVariant[]; options: OptionDef[]; currency: string; locale?: string;
1068
+ checkoutCart?: { action: string; purchaseOptions: ProductPurchaseOption[]; byVariantId: Record<string, Record<string, VariantCartPayload>> } }` where local
1069
+ `OptionDef = { name: string; position: number; values: string[] }`; `CatalogVariant` /
1070
+ `VariantCartPayload` from `@tot/public-runtime`. Directive: `client:load`.
1071
+ - **State:** props only. Default selection = first available variant. Dispatches `window` CustomEvents
1072
+ `variant:image { imageId }` (consumed by `ImageGallery`) and `variant:change { variantId, available
1073
+ }`. No storage; no catalog/ToT fetch.
1074
+ - **Dependencies:** `preact/hooks`; `formatPrice` (`@/lib/format`); types from `@tot/public-runtime`.
1075
+ - **Tokens consumed:** `text-muted`, `text-sale`, `text-accent`, `text-primary`, `text-text`,
1076
+ `border-border`, `bg-surface`, `border-primary`/`bg-primary`/`text-primary-contrast`,
1077
+ `outline-primary`, `bg-border/40`.
1078
+ - **Delivery:** platform island (PDP).
1079
+ - **Usage:**
1080
+ ```astro
1081
+ <VariantSelector client:load variants={product.variants} options={product.options} currency={product.price_range.currency} locale="en-US" />
1082
+ ```
1083
+
1084
+ ### ImageGallery (`components/islands/ImageGallery.tsx`)
1085
+
1086
+ - **Purpose:** PDP image gallery — main image + thumbnail strip; thumbnails swap the main image, main
1087
+ image has click-to-toggle zoom (reduced-motion safe). First image is the LCP candidate until first
1088
+ interaction.
1089
+ - **Props:** `{ images: CatalogImage[]; title: string; productHandle: string; activeImageId?: string }`
1090
+ (`CatalogImage` from `@tot/public-runtime`). Realistic directive: `client:load`.
1091
+ - **State:** props only. Listens on `window` for `variant:image { imageId }` (dispatched by
1092
+ `VariantSelector`) and switches to the matching image. Dead fixture URLs fall back to a deterministic
1093
+ placeholder. No network.
1094
+ - **Dependencies:** `preact/hooks`; `placeholderDataUri, isUsableImageUrl` (`@/lib/placeholder`);
1095
+ type from `@tot/public-runtime`.
1096
+ - **Tokens consumed:** `--color-border` (placeholder bg); `outline-primary` (focus), `border-primary`
1097
+ / `border-border` (thumbnails).
1098
+ - **Delivery:** platform island (PDP).
1099
+ - **Usage:**
1100
+ ```astro
1101
+ <ImageGallery client:load images={product.images} title={product.title} productHandle={product.handle} />
1102
+ ```
1103
+
1104
+ ### QuickView (`components/islands/QuickView.tsx`)
1105
+
1106
+ - **Purpose:** One global native `<dialog>` any product card opens via a `[data-quickview]` button
1107
+ (event-delegated, so cards stay zero-JS). Shows a peek — image, price, colors, short description —
1108
+ and routes to the full PDP. No cart action (Phase 1 browse-only).
1109
+ - **Props:** `{ locale: string }`. Directive: `client:idle`. The per-card payload is parsed from the
1110
+ button's `data-quickview` JSON attribute.
1111
+ - **State:** props + DOM-delegated clicks. Reads product data from the clicked button's attribute, not
1112
+ storage/network.
1113
+ - **Dependencies:** `preact/hooks`; `formatPrice` (`@/lib/format`), `@/lib/placeholder`, `withClientBase`
1114
+ (`@/lib/basePath`).
1115
+ - **Tokens consumed:** `--radius-lg`, `--color-surface`, `--color-text` (+ backdrop mix),
1116
+ `--color-border`, `--color-muted`, `--color-sale`, `--color-bg`, `--radius-md`, `--color-primary`,
1117
+ `--color-primary-contrast`.
1118
+ - **Delivery:** platform island (global, mounted once per page).
1119
+ - **Usage:**
1120
+ ```astro
1121
+ <QuickView client:idle locale="en-US" />
1122
+ ```
1123
+
1124
+ ### SearchTypeahead (`components/islands/SearchTypeahead.tsx`)
1125
+
1126
+ - **Purpose:** Compact header search with debounced typeahead dropdown. Fetches `GET
1127
+ {action}?q=<q>&format=json` and renders up to 6 results as an ARIA combobox/listbox; submitting
1128
+ navigates to the full results page.
1129
+ - **Props:** `{ action?: string }` (default `"/search"`). Directive: `client:idle`. `TypeaheadDoc`
1130
+ from `@/lib/search`.
1131
+ - **State:** local state only; debounced `fetch` (150 ms, min 2 chars, `AbortController`) against the
1132
+ tenant's own `/search` endpoint (base-path prefixed) — a same-app fetch, not a catalog/cart/ToT API.
1133
+ Failures degrade to "no dropdown". No storage.
1134
+ - **Dependencies:** `preact/hooks`; `type TypeaheadDoc` (`@/lib/search`), `@/lib/format`,
1135
+ `@/lib/placeholder`, `withClientBase` (`@/lib/basePath`).
1136
+ - **Tokens consumed:** `border-border`, `bg-surface`, `border-primary`/`focus-within:border-primary`,
1137
+ `text-muted`, `text-text`, `placeholder:text-muted`, `shadow-lg`, `bg-border/50`; thumb uses
1138
+ `--color-border`.
1139
+ - **Delivery:** platform island (header).
1140
+ - **Usage:**
1141
+ ```astro
1142
+ <SearchTypeahead client:idle />
1143
+ ```
1144
+
1145
+ ### MobileNav (`components/islands/MobileNav.tsx`)
1146
+
1147
+ - **Purpose:** Hamburger button + slide-in drawer for small screens (`md:hidden`). WCAG 2.2 AA:
1148
+ `aria-expanded`/`aria-controls`, focus trap, Escape + backdrop close, body-scroll lock, focus
1149
+ restore, reduced-motion guard.
1150
+ - **Props:** `{ nav: NavItem[]; announcement?: string }` where `NavItem = { label: string; href: string;
1151
+ children?: { label: string; href: string }[] }`. Directive: `client:idle`.
1152
+ - **State:** props only; local `open` state; toggles body scroll. No storage/network.
1153
+ - **Dependencies:** `preact/hooks`.
1154
+ - **Tokens consumed:** `text-text`, `bg-surface`, `outline-primary`, `bg-text/40` (backdrop),
1155
+ `border-border`, `text-muted`, `bg-primary`, `text-primary-contrast`, `text-primary`, `bg-border/40`,
1156
+ `shadow-xl`.
1157
+ - **Delivery:** platform island (mobile chrome; raw-chrome analog is the hamburger/`#primaryNav` toggle
1158
+ in `CHROME_BEHAVIOR_JS` + the off-canvas `.primary-nav`/`.hamburger` rules).
1159
+ - **Usage:**
1160
+ ```astro
1161
+ <MobileNav client:idle nav={nav} announcement="Free US shipping on orders over $100" />
1162
+ ```
1163
+
1164
+ ### RecentlyViewed (`components/islands/RecentlyViewed.tsx`)
1165
+
1166
+ - **Purpose:** "Recently viewed" discovery rail. On a PDP it records the current product to
1167
+ localStorage and renders the rest as a quiet strip. Renders nothing until it has something to show
1168
+ (no layout shift / empty-state noise).
1169
+ - **Props:** `{ current?: RVItem; locale: string; title?: string; max?: number }` (`title` default
1170
+ `"Recently viewed"`, `max` default `5`) where `export interface RVItem { handle: string; title:
1171
+ string; price: number; currency: string; vendor?: string; img?: string; rehosted?: boolean; seed:
1172
+ string }`. Directive: `client:idle`.
1173
+ - **State:** reads/writes `localStorage` `"tot:recently-viewed"` (JSON array, capped at 12); excludes
1174
+ `current`. No network.
1175
+ - **Dependencies:** `preact/hooks`; `formatPrice` (`@/lib/format`), `@/lib/placeholder`,
1176
+ `withClientBase` (`@/lib/basePath`).
1177
+ - **Tokens consumed:** `--color-border`, `--color-muted`, `--color-primary`; class `eyebrow
1178
+ eyebrow--tick`, `container-prose`.
1179
+ - **Delivery:** platform island.
1180
+ - **Usage:**
1181
+ ```astro
1182
+ <RecentlyViewed client:idle locale="en-US" />
1183
+ <!-- On a PDP, pass current so it's recorded + excluded: -->
1184
+ <RecentlyViewed client:idle current={rvItem} locale="en-US" />
1185
+ ```
1186
+
1187
+ ### SavedList (`components/islands/SavedList.tsx`)
1188
+
1189
+ - **Purpose:** Renders the wishlist from localStorage on the `/saved` page. Client-only; each item
1190
+ links to its PDP and can be removed inline; empty state invites browsing. Phase 2 swaps the store for
1191
+ an account-backed list without touching the component.
1192
+ - **Props:** `{ locale: string }`. Directive: `client:only="preact"` (no SSR — the list only exists
1193
+ client-side). `SavedItem` from `@/lib/wishlist`.
1194
+ - **State:** reads `localStorage` `"tot:wishlist"` via `readWishlist()`; listens for `WISHLIST_EVENT`
1195
+ (`"tot:wishlist-changed"`) to refresh; removes via `toggleSaved()`. Renders `null` pre-hydration.
1196
+ - **Dependencies:** `preact/hooks`; `readWishlist, toggleSaved, WISHLIST_EVENT, type SavedItem`
1197
+ (`@/lib/wishlist`), `@/lib/format`, `@/lib/placeholder`, `@/lib/basePath`.
1198
+ - **Tokens consumed:** `--radius-lg`, `--color-border`, `--color-surface`, `--color-muted`,
1199
+ `--radius-md`, `--color-primary`, `--color-primary-contrast`, `--color-sale` (heart).
1200
+ - **Delivery:** platform island (`/saved` page).
1201
+ - **Usage:**
1202
+ ```astro
1203
+ <SavedList client:only="preact" locale="en-US" />
1204
+ ```
1205
+
1206
+ ---
1207
+
1208
+ ## 10. Foundational atoms + home — `components/*`, `components/home/`
1209
+
1210
+ Shared primitives composed by the higher-level widgets, plus the homepage hero.
1211
+
1212
+ ### Badge (`components/Badge.astro`)
1213
+
1214
+ - **Purpose:** Tiny mono-uppercase pill used over product imagery and inline (sale %, New, low-stock,
1215
+ sold-out, compliance-verified).
1216
+ - **Props:** `variant: "sale" | "new" | "low-stock" | "soldout" | "compliant"`; `label?: string` (or
1217
+ default slot); `class?: string`.
1218
+ - **Dependencies:** none.
1219
+ - **Tokens consumed:** `bg-sale`, `bg-primary text-primary-contrast`, `bg-accent`, `bg-surface
1220
+ text-muted border-border`, `bg-[var(--color-success)]`; `font-mono`, `rounded-full`.
1221
+ - **Delivery:** platform component.
1222
+ - **Usage:**
1223
+ ```astro
1224
+ <Badge variant="compliant" label="Compliant" />
1225
+ ```
1226
+
1227
+ ### Breadcrumbs (`components/Breadcrumbs.astro`)
1228
+
1229
+ - **Purpose:** Semantic breadcrumb trail; last crumb is the current page (`aria-current`, not a link).
1230
+ - **Props:** `crumbs: { label: string; href: string }[]`.
1231
+ - **Dependencies:** `withBase`.
1232
+ - **Tokens consumed:** `text-muted`, `text-border`, `text-text`, `hover:text-primary`.
1233
+ - **Delivery:** platform component (`.crumbs` exists in the marketing `mkt.css` layer, not
1234
+ commerce-chrome.css).
1235
+ - **Usage:**
1236
+ ```astro
1237
+ <Breadcrumbs crumbs={[{ label: "Home", href: "/" }, { label: "Bestsellers", href: "/collections/bestsellers" }]} />
1238
+ ```
1239
+
1240
+ ### ProductCard (`components/ProductCard.astro`)
1241
+
1242
+ - **Purpose:** PLP/grid card using the stretched-link pattern (title is the link; an `::after` overlay
1243
+ makes the whole card clickable so wishlist/quick-view controls sit above it). Image hover-zoom +
1244
+ optional second image; status badges; color swatches; price with struck compare-at; optional rating.
1245
+ - **Props:** `product: CatalogProduct`; `position?: number`; `list?: string`; `priority?: boolean`;
1246
+ `rating?: number`; `reviewCount?: number`.
1247
+ - **Dependencies:** `CatalogProduct` from `@tot/public-runtime`; `discountPercent, truncate`
1248
+ (`@/lib/format`); `isOnSale, isNew, isAvailable, fromPrice` (`@/lib/tot/query`); `colorOptionValues,
1249
+ resolveSwatch, isLightFill` (`@/lib/colors`); `Img`, `Badge`, `PriceDisplay`, `RatingStars`;
1250
+ `withBase`. Reads `Astro.locals.tenant` (`.locale`) + `Astro.locals.basePath`. The compliance badge
1251
+ is tag-driven (`compliant`/`lab-tested`/`age-verified`) — tenant-agnostic, no per-merchant branch.
1252
+ - **Tokens consumed:** `bg-border`, `ring-[var(--color-border)]`, `shadow-[var(--shadow-md)]`,
1253
+ `rounded-md`, `rounded-[var(--radius-sm)]`, `text-[var(--color-text)]`, `hover:text-[var(--color-sale)]`,
1254
+ `text-muted`, `group-hover:text-primary`, `font-display`, `font-mono`.
1255
+ - **Delivery:** platform component.
1256
+ - **Usage:**
1257
+ ```astro
1258
+ <ProductCard product={p} rating={4.5} reviewCount={128} />
1259
+ ```
1260
+
1261
+ ### CollectionCard (`components/CollectionCard.astro`)
1262
+
1263
+ - **Purpose:** Editorial collection tile with a dark gradient scrim so the overlaid title (+ optional
1264
+ piece count) reads on any image. `data-reveal` scroll-in.
1265
+ - **Props:** `collection: CatalogCollection` (from `@tot/public-runtime`); `productCount?: number`.
1266
+ - **Dependencies:** `CatalogCollection` from `@tot/public-runtime`; `Img`; `withBase`.
1267
+ - **Tokens consumed:** `bg-border`, `rounded-md`, `font-display`, `font-mono` (overlay uses fixed
1268
+ `text-white` / `from-black/75` scrim).
1269
+ - **Delivery:** platform component.
1270
+ - **Usage:**
1271
+ ```astro
1272
+ <CollectionCard collection={collection} productCount={24} />
1273
+ ```
1274
+
1275
+ ### Section (`components/Section.astro`)
1276
+
1277
+ - **Purpose:** Section wrapper with consistent vertical rhythm + optional eyebrow/title header +
1278
+ alternating band background.
1279
+ - **Props:** `title?: string`; `eyebrow?: string`; `tone?: "bg" | "surface"`; `class?: string`; `id?:
1280
+ string`. Slots: default + named `header-aside`.
1281
+ - **Dependencies:** none.
1282
+ - **Tokens consumed:** `bg-[var(--color-surface)]` / `bg-[var(--color-bg)]`; classes `container-prose`,
1283
+ `eyebrow eyebrow--tick`, `display-lg`.
1284
+ - **Delivery:** platform component.
1285
+ - **Usage:**
1286
+ ```astro
1287
+ <Section eyebrow="Shop" title="Best Sellers" tone="surface"> … </Section>
1288
+ ```
1289
+
1290
+ ### EmptyState (`components/EmptyState.astro`)
1291
+
1292
+ - **Purpose:** Intentional empty result (search/PLP). Copy passed in by the parent; optional CTA.
1293
+ - **Props:** `title: string`; `body?: string`; `ctaLabel?: string`; `ctaHref?: string`.
1294
+ - **Dependencies:** `withBase`.
1295
+ - **Tokens consumed:** `text-muted`, `bg-primary`, `text-primary-contrast`, `font-display`,
1296
+ `rounded-full`; class `display-md`.
1297
+ - **Delivery:** platform component.
1298
+ - **Usage:**
1299
+ ```astro
1300
+ <EmptyState title="No results" body="Try broadening your filters." ctaLabel="Browse all" ctaHref="/collections/all" />
1301
+ ```
1302
+
1303
+ ### Img (`components/Img.astro`)
1304
+
1305
+ - **Purpose:** CLS-free, theme-agnostic image with offline placeholder fallback — dead fixture URLs
1306
+ render a deterministic gradient SVG keyed by `seed`; real rehosted / Shopify `?width=` URLs get
1307
+ responsive srcset/sizes. Explicit width/height always set.
1308
+ - **Props:** `src?: string`; `alt: string`; `width: number`; `height: number`; `seed: string`; `label?:
1309
+ string`; `rehosted?: boolean`; `priority?: boolean` (LCP: eager+high); `sizes?: string`; `class?:
1310
+ string`; `loading?: "eager" | "lazy"`.
1311
+ - **Dependencies:** `resolveImage` (`@/lib/imageAttrs`).
1312
+ - **Tokens consumed:** `--color-border` (placeholder backdrop).
1313
+ - **Delivery:** platform component.
1314
+ - **Usage:**
1315
+ ```astro
1316
+ <Img src={image?.url} rehosted={image?.rehosted} seed={image?.id ?? handle} alt={title} width={800} height={1000} sizes="(min-width:1024px) 25vw, 100vw" priority />
1317
+ ```
1318
+
1319
+ ### WidgetFrame (`components/WidgetFrame.astro`)
1320
+
1321
+ - **Purpose:** Renders a tenant-registered widget (`isolation: "sandbox"`) in a sandboxed iframe —
1322
+ `allow-scripts` without `allow-same-origin` puts it in an opaque origin (can't read page
1323
+ cookies/DOM/storage); bundle hash-pinned in CSP.
1324
+ - **Props:** `src: string` (same-origin bundle URL); `title: string` (required a11y); `strategy?:
1325
+ ScriptStrategy` (from `@tot/public-runtime`, default `"on-idle"`); `height?: string` (default
1326
+ `"auto"`).
1327
+ - **Dependencies:** `widgetFrameAttrs, type ScriptStrategy` from `@tot/public-runtime`.
1328
+ - **Tokens consumed:** none (`.tot-widget-frame` + inline sizing; no theme tokens).
1329
+ - **Delivery:** platform component.
1330
+ - **Usage:**
1331
+ ```astro
1332
+ <WidgetFrame src="/widgets/tenant-abc/reviews.html" title="Customer reviews" height="480px" />
1333
+ ```
1334
+
1335
+ ### Seo (`components/Seo.astro`)
1336
+
1337
+ - **Purpose:** Per-page `<title>`/meta, Open Graph, Twitter card, canonical, JSON-LD. Tenant-aware —
1338
+ OG image + site name from the resolved tenant. Head-only (renders no styled markup).
1339
+ - **Props:** `title: string`; `description?: string`; `path: string` (path-only canonical, joined to
1340
+ the tenant canonical base); `ogType?: "website" | "product" | "article"`; `ogImage?: string`;
1341
+ `noindex?: boolean`; `jsonLd?: JsonLd | JsonLd[]` (`JsonLd = { [key: string]: unknown }`).
1342
+ - **Dependencies:** none. Reads `Astro.locals.tenant` (`.displayName`, `.theme_tokens?.brand?.ogImage`)
1343
+ + `Astro.locals.canonicalBase`.
1344
+ - **Tokens consumed:** none.
1345
+ - **Delivery:** platform component.
1346
+ - **Usage:**
1347
+ ```astro
1348
+ <Seo title="Merino Zip Hoodie" description="Soft, warm, everyday." path="/products/merino-zip-hoodie" ogType="product" jsonLd={productSchema} />
1349
+ ```
1350
+
1351
+ ### Hero (`components/home/Hero.astro`)
1352
+
1353
+ - **Purpose:** The signature homepage moment ("hero is a thesis"). Treatments: `"bold"` (default —
1354
+ full-bleed primary background, contrast text), `"light"` (page background, brand color as
1355
+ headline/CTA accent), `"banner"` (merchant artwork edge-to-edge); non-empty `slides` render a
1356
+ slideshow. Feature image is the merchant's own hero slide (`block.image`) when adopted, else the
1357
+ hero collection's first product. `block.notice` renders a `.hero__notice` status pill in every
1358
+ treatment.
1359
+ - **Props:** `block: HeroBlock` (from `@/lib/storyblok/content-model`: `{ component: "hero"; notice?:
1360
+ string; eyebrow?: string; headline: string; headlineAccent?: string; subhead?: string; ctaLabel?: string; ctaHref?:
1361
+ string; featureCollection?: string; image?: string; imageAlt?: string }`); `featureProduct?:
1362
+ CatalogProduct | null`.
1363
+ - **Dependencies:** `Img`; `withBase`; `HeroBlock` + `CatalogProduct` (`@tot/public-runtime`).
1364
+ - **Note:** the bold/light variant is read from
1365
+ `Astro.locals.tenant?.theme_tokens?.brand?.heroVariant` — **not a prop**. Also reads
1366
+ `Astro.locals.basePath`.
1367
+ - **Tokens consumed:** `--color-bg`, `--color-text`, `--color-primary`, `--color-primary-contrast`,
1368
+ `--color-muted`, `--color-accent`, `--radius-lg`, `--shadow-lg`, `--radius-md`, `--color-surface`,
1369
+ `--color-border`, `--color-hero-eyebrow`, `--heading-hero`; classes `display-hero`,
1370
+ `hero__eyebrow eyebrow eyebrow--tick`, `hero__notice`, `container-prose`.
1371
+ - **Delivery:** platform component (SSR, homepage).
1372
+ - **Usage:**
1373
+ ```astro
1374
+ ---
1375
+ // tenant sets Astro.locals.tenant.theme_tokens.brand.heroVariant = "bold" | "light" | "banner"
1376
+ ---
1377
+ <Hero block={heroBlock} featureProduct={featureProduct} />
1378
+ ```
1379
+
1380
+ ---
1381
+
1382
+ ## 11. Shared raw-chrome library — `lib/rawChrome.ts`, `lib/chrome/`, `public/shared/commerce-chrome.css`
1383
+
1384
+ The raw-chrome path exists so a tenant that ships **hand-authored raw marketing HTML** still gets the
1385
+ platform chrome look without copying CSS. It is the second delivery path referenced throughout §2–§4.
1386
+
1387
+ ### `lib/rawChrome.ts` — the canonical raw-chrome generator
1388
+
1389
+ Replaces the three divergent hand-authored `content/<tenant>/chrome.html` documents with **one
1390
+ deterministic server-side generator** driven entirely by a tenant's `chrome.json` (typed
1391
+ `TenantChrome`) — **content only, no styling**.
1392
+
1393
+ - **Single-stylesheet model** (`raw-chrome-single-stylesheet`): the wrapper `<link>`s exactly one
1394
+ platform stylesheet, `SHARED_CHROME_STYLESHEET = "/shared/commerce-chrome.css"`, and emits **no
1395
+ inline chrome CSS**. Every color/space/type comes from theme tokens the tenant's own linked
1396
+ `*-theme.css` sets on `:root`. The markup mirrors, class-for-class, the DOM the F2 stylesheet styles.
1397
+ - **Splice-marker output** (`materialization-compat`): returns a full `<!DOCTYPE html>` document
1398
+ carrying `<!--PAGE_HEAD-->` / `<!--PAGE_BODY-->` markers (`CHROME_HEAD_MARKER` / `CHROME_BODY_MARKER`)
1399
+ so body-only page fragments get spliced in and the result is nonce-stamped + base-path-prefixed
1400
+ downstream. `rawMarketingHtml.prefixTenantLinks` exempts `/shared/` so the shared link survives
1401
+ path-prefix routing.
1402
+ - **Security:** all authored values are HTML-escaped (`esc`/`escSingle`) — defense-in-depth for
1403
+ first-party config.
1404
+
1405
+ Exports:
1406
+
1407
+ - `renderRawChrome(chrome: TenantChrome): string` — the entry point. Builds `<head>` (theme-color,
1408
+ favicon, tenant `themeStylesheets`, then the shared stylesheet), a skip link, then conditional
1409
+ sections in order: `warningBar` → `announcement` → `utilityNav` → header; the body marker; then
1410
+ `complianceStrip` → footer; then the age-gate JSON `<script>` + the inlined behavior script.
1411
+ - `CHROME_BEHAVIOR_JS: string` — the ONE shared, nonce-safe progressive-enhancement script (never
1412
+ copy-pasted per tenant). Three guarded blocks: (1) mobile nav drawer toggle (`#hamburger`/
1413
+ `#primaryNav`), (2) announcement-bar dismiss (remembered in `localStorage`, default key
1414
+ `tot_announce_dismissed`) + message rotation, (3) the 21+ age gate built inside a **closed shadow
1415
+ root** with a `data-tot-compliance` light-DOM host + `MutationObserver` re-assert — the same
1416
+ tamper-resistance pattern as `IsolatedAgeGate.tsx`, keyed on `tot:age-affirmed`.
1417
+ - `SHARED_CHROME_STYLESHEET: string` = `"/shared/commerce-chrome.css"`.
1418
+
1419
+ ### `lib/chrome/model.ts` — typed chrome config
1420
+
1421
+ Re-exports `@tot/public-runtime`'s `ChromeConfig` (`packages/public-runtime/src/chrome.ts`) — the
1422
+ ONE schema for a tenant's site chrome, content-only, skinned by tokens. `header` and `footer` are
1423
+ independent, separately-evolvable sub-shapes; every nav item, header CTA and footer link carries a
1424
+ stable `id` (a governed-action-registry substitute, independent of the visible `label`). In addition
1425
+ to `ChromeHeader`/`ChromeFooter`, it exports `ChromeBrand`, `ChromeSearch`, `ChromeUtilityNav`,
1426
+ `ChromeAnnouncement` (`{ variant: "marquee" | "bar" | "solid"; messages: string[]; label?;
1427
+ dismissible?; rotateMs?; storageKey? }`), `ChromeAgeGate` (`{ minAge: number; storageKey?; title?;
1428
+ lede?; yesLabel?; noLabel?; denyText?; fineText?; mark? }`), `ChromeFooterBrand`, `ChromeTrustMark`,
1429
+ `ChromeHead`, `ChromeCta`, `ChromeNavItem`, and `ChromeConfig` itself.
1430
+
1431
+ ### `lib/chrome/parseConfig.ts` — the validation gate
1432
+
1433
+ Re-exports `@tot/public-runtime`'s `validateChromeConfig`/`parseChromeConfig`. `parseChromeConfig(raw:
1434
+ unknown): ChromeConfig | null` returns a config only when `raw.header` and `raw.footer` both validate
1435
+ (header: a valid `variant`, `brand.label` string, `nav` array; footer: a valid `variant`, `columns`
1436
+ array — see {@link validateChromeConfig} for the full per-field rules). Any other shape (missing,
1437
+ malformed, or half-authored) returns `null`, so the caller keeps serving the tenant's hand-authored
1438
+ `chrome.html`. `validateChromeConfig(raw)` returns the actionable, path-prefixed error list instead of
1439
+ collapsing to `null` — this is what `@tot/private-controlplane`'s write-side `ArtifactValidator` for
1440
+ `tenants/<id>/content/chrome.json` calls, so a malformed `chrome.json` is rejected with real diagnostics
1441
+ at candidate-submission time.
1442
+
1443
+ ### `public/shared/commerce-chrome.css` — the one shared stylesheet (F2)
1444
+
1445
+ ONE platform-shipped, token-driven sheet that generalizes the three per-tenant `mkt.css` files into a
1446
+ single themed layer. **Contract-token-only** — reads only `--color-*` / `--font-*` / `--radius-*` /
1447
+ `--shadow-*` / `--container-max`, no tenant-private brand vars or hard-coded hex; on-dark surfaces
1448
+ (footer, age-gate overlay, category cards) derive from `--color-text` + `--color-primary-contrast` via
1449
+ `color-mix`. Its header documents the old-var → contract-token mapping (`--navy`/`--brand-red` →
1450
+ `--color-primary`, `--sale` → `--color-sale`, `--brand-blue` → `--color-accent`). Main selector groups:
1451
+
1452
+ - **Layout:** `.container`, `.section` / `.section--tight` / `.section--surface`.
1453
+ - **Buttons:** `.btn` + `.btn-primary` / `.btn-secondary` / `.btn-line` / `.btn-ghost` / `.btn-outline`.
1454
+ - **Announcement:** `.announce`, `.announce-marquee` / `__track` (`@keyframes chrome-marquee`,
1455
+ reduced-motion stops it), `.announce-bar` / `__msg` / `__close`.
1456
+ - **Utility / top bar:** `.topbar-warning` (FDA strip), `.utility-nav` / `__right` / `.sel` / `.social`,
1457
+ `.member-cue`.
1458
+ - **Header / nav:** `.site-header` (sticky) / `.header-row`, `.brand` (+ `.b-prime`/`.b-vapor`/`.b-club`),
1459
+ `.header-search`, `.header-actions` / `.cart` / `.count`, `.hamburger`.
1460
+ - **Primary nav + mega:** `.primary-nav` / `> ul`, `.is-flag`, `.nav-item`, `.caret`, `.new-badge`,
1461
+ `.primary-nav-bar`, `.mega` / `.mega--wide` / `.mega__cols` / `.mega__col`.
1462
+ - **Section headers:** `.sec-head` / `--between` / `.eyebrow` / `.link-more`.
1463
+ - **Tiles / grids:** `.tile-grid` / `.tile`, `.brand-grid` / `.brand-tile` / `__text`, `.article-grid`
1464
+ / `.article-card` (+ `__media`/`__body`/`__tag`/`__title`/`__excerpt`/`__meta`), `.tag-cloud` /
1465
+ `.tag-chip`.
1466
+ - **Category cards:** `.cat-grid` / `.cat-card` (gradient primary→primary-hover).
1467
+ - **Compliance:** `.compliance-strip` (source-copied legal band).
1468
+ - **Age gate:** `.age-gate` / `.is-open` / `__panel` / `__mark` (+ `.is-red`) / `__actions` / `__deny` /
1469
+ `.is-denied` / `__fine` / `__lede`.
1470
+ - **Footer:** `.site-footer` / `.footer-grid` / `.footer-brand` / `.footer-col` / `.footer-news`
1471
+ (+ `__row`, `.footer-attest`) / `.pay-badges` / `.trust-marks` / `.footer-warning` / `.footer-bottom`.
1472
+ - **Reveal-on-scroll:** `.reveal` / `.reveal.in`.
1473
+ - **Responsive:** breakpoints at 1000 / 960 / 860 (off-canvas `.primary-nav` drawer, `.hamburger`,
1474
+ mega→inline accordion) / 560 px.
1475
+
1476
+ ---
1477
+
1478
+ ## Appendix — cross-cutting reference
1479
+
1480
+ **Cross-island `window` CustomEvents:** `variant:image` (VariantSelector → ImageGallery),
1481
+ `variant:change` (VariantSelector → any listener), `tot:wishlist-changed` / `WISHLIST_EVENT`
1482
+ (`lib/wishlist` → SavedList + header count), and delegated `[data-quickview]` buttons → QuickView.
1483
+
1484
+ **`localStorage` keys:** `"tot:age-affirmed"` (age gate), `"tot:recently-viewed"` (RecentlyViewed),
1485
+ `"tot:wishlist"` (SavedList/wishlist), `tot_announce_dismissed` (raw-chrome announce dismissal).
1486
+
1487
+ **`data-compliance` hooks (9):** `nicotine-warning`, `pact-act`, `adult-signature`, `prop65`,
1488
+ `purchase-limit`, `state-eligibility`, `shipping-restriction`, `excise-tax`, plus `ComplianceNotice`
1489
+ re-emitting the caller's `hook`.
1490
+
1491
+ **`data-widget` hooks (5):** `tier-cards`, `tier-comparison-table`, `subscription-how-it-works`,
1492
+ `subscription-savings-explainer`, `subscription-manage-entry`. Illustrative surfaces are marked
1493
+ `data-illustrative="badge" | "note" | "figure"`.
1494
+
1495
+ **Related architecture docs:** [`token-contract.md`](architecture/token-contract.md),
1496
+ [`shared-vs-tenant-assets.md`](architecture/shared-vs-tenant-assets.md).