@tokenoftrust/storefront-runner 1.3.2

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 (310) hide show
  1. package/LICENSE +58 -0
  2. package/README.md +42 -0
  3. package/apps/storefront/astro.config.mjs +66 -0
  4. package/apps/storefront/dev-plugins/dev-loop-state.mjs +147 -0
  5. package/apps/storefront/dev-plugins/tenant-hot-reload.mjs +238 -0
  6. package/apps/storefront/env.d.ts +162 -0
  7. package/apps/storefront/migrations/0000_baseline.sql +54 -0
  8. package/apps/storefront/migrations/0001_products_fts.sql +9 -0
  9. package/apps/storefront/migrations/README.md +54 -0
  10. package/apps/storefront/migrations/meta/0000_snapshot.json +347 -0
  11. package/apps/storefront/migrations/meta/0001_snapshot.json +347 -0
  12. package/apps/storefront/migrations/meta/_journal.json +20 -0
  13. package/apps/storefront/package.json +42 -0
  14. package/apps/storefront/public/brand/northwind-logo-dark.svg +4 -0
  15. package/apps/storefront/public/brand/northwind-logo-light.svg +4 -0
  16. package/apps/storefront/public/brand/northwind-og.svg +13 -0
  17. package/apps/storefront/public/favicon.svg +5 -0
  18. package/apps/storefront/public/js/dashboard-team.js +147 -0
  19. package/apps/storefront/public/js/dashboard-vendor-search.js +115 -0
  20. package/apps/storefront/public/shared/commerce-chrome.css +369 -0
  21. package/apps/storefront/src/components/Badge.astro +34 -0
  22. package/apps/storefront/src/components/Breadcrumbs.astro +45 -0
  23. package/apps/storefront/src/components/CollectionCard.astro +56 -0
  24. package/apps/storefront/src/components/EmptyState.astro +55 -0
  25. package/apps/storefront/src/components/Img.astro +68 -0
  26. package/apps/storefront/src/components/ProductCard.astro +228 -0
  27. package/apps/storefront/src/components/Section.astro +39 -0
  28. package/apps/storefront/src/components/Seo.astro +70 -0
  29. package/apps/storefront/src/components/WidgetFrame.astro +34 -0
  30. package/apps/storefront/src/components/admin/AdminAIWorkflowsTab.astro +531 -0
  31. package/apps/storefront/src/components/admin/AdminCustomersTab.astro +597 -0
  32. package/apps/storefront/src/components/admin/AdminFoxyCartSettingsPanel.astro +227 -0
  33. package/apps/storefront/src/components/admin/AdminFulfillmentTab.astro +465 -0
  34. package/apps/storefront/src/components/admin/AdminProductsTab.astro +901 -0
  35. package/apps/storefront/src/components/admin/AdminReportingTab.astro +435 -0
  36. package/apps/storefront/src/components/admin/AdminSettingsTab.astro +782 -0
  37. package/apps/storefront/src/components/admin/orders/OrdersWorkspace.astro +1223 -0
  38. package/apps/storefront/src/components/auth/AuthBadge.astro +44 -0
  39. package/apps/storefront/src/components/chrome/AnnouncementBar.astro +108 -0
  40. package/apps/storefront/src/components/chrome/MegaMenu.astro +26 -0
  41. package/apps/storefront/src/components/chrome/NavDropdown.astro +201 -0
  42. package/apps/storefront/src/components/chrome/SiteFooter.astro +201 -0
  43. package/apps/storefront/src/components/chrome/SiteHeader.astro +234 -0
  44. package/apps/storefront/src/components/chrome/UtilityNav.astro +79 -0
  45. package/apps/storefront/src/components/commerce/ArticleCard.astro +242 -0
  46. package/apps/storefront/src/components/commerce/BrandGrid.astro +175 -0
  47. package/apps/storefront/src/components/commerce/FoxyLoader.astro +23 -0
  48. package/apps/storefront/src/components/commerce/FoxySidecartStyles.astro +61 -0
  49. package/apps/storefront/src/components/commerce/PriceDisplay.astro +91 -0
  50. package/apps/storefront/src/components/commerce/ProductCarousel.astro +197 -0
  51. package/apps/storefront/src/components/commerce/ProductGrid.astro +75 -0
  52. package/apps/storefront/src/components/commerce/RatingStars.astro +58 -0
  53. package/apps/storefront/src/components/commerce/ReviewList.astro +54 -0
  54. package/apps/storefront/src/components/commerce/ReviewSummary.astro +44 -0
  55. package/apps/storefront/src/components/commerce/TagFilterCloud.astro +149 -0
  56. package/apps/storefront/src/components/compliance/AdultSignatureNotice.astro +37 -0
  57. package/apps/storefront/src/components/compliance/ComplianceNotice.astro +99 -0
  58. package/apps/storefront/src/components/compliance/ExciseTaxNotice.astro +45 -0
  59. package/apps/storefront/src/components/compliance/NicotineWarning.astro +33 -0
  60. package/apps/storefront/src/components/compliance/PactActNotice.astro +35 -0
  61. package/apps/storefront/src/components/compliance/Prop65Warning.astro +80 -0
  62. package/apps/storefront/src/components/compliance/PurchaseLimitNotice.astro +38 -0
  63. package/apps/storefront/src/components/compliance/ShippingRestrictionNotice.astro +35 -0
  64. package/apps/storefront/src/components/compliance/StateEligibilityNotice.astro +53 -0
  65. package/apps/storefront/src/components/home/Hero.astro +164 -0
  66. package/apps/storefront/src/components/islands/AgeGate.tsx +109 -0
  67. package/apps/storefront/src/components/islands/ImageGallery.tsx +237 -0
  68. package/apps/storefront/src/components/islands/IsolatedAgeGate.tsx +73 -0
  69. package/apps/storefront/src/components/islands/MobileNav.tsx +239 -0
  70. package/apps/storefront/src/components/islands/QuickView.tsx +138 -0
  71. package/apps/storefront/src/components/islands/RecentlyViewed.tsx +122 -0
  72. package/apps/storefront/src/components/islands/SavedList.tsx +99 -0
  73. package/apps/storefront/src/components/islands/SearchTypeahead.tsx +248 -0
  74. package/apps/storefront/src/components/islands/VariantSelector.tsx +375 -0
  75. package/apps/storefront/src/components/marketing/CardGrid.astro +72 -0
  76. package/apps/storefront/src/components/marketing/CodeSample.astro +20 -0
  77. package/apps/storefront/src/components/marketing/CtaBand.astro +24 -0
  78. package/apps/storefront/src/components/marketing/Faq.astro +37 -0
  79. package/apps/storefront/src/components/marketing/Integrations.astro +63 -0
  80. package/apps/storefront/src/components/marketing/MarketingBlocks.astro +53 -0
  81. package/apps/storefront/src/components/marketing/MarketingCtas.astro +48 -0
  82. package/apps/storefront/src/components/marketing/MarketingHero.astro +72 -0
  83. package/apps/storefront/src/components/marketing/ProofStrip.astro +37 -0
  84. package/apps/storefront/src/components/marketing/SplitCompare.astro +53 -0
  85. package/apps/storefront/src/components/marketing/Steps.astro +36 -0
  86. package/apps/storefront/src/components/marketing/Testimonials.astro +38 -0
  87. package/apps/storefront/src/components/marketing/TrustBar.astro +22 -0
  88. package/apps/storefront/src/components/membership/IllustrativeLabel.astro +56 -0
  89. package/apps/storefront/src/components/membership/TierCards.astro +164 -0
  90. package/apps/storefront/src/components/membership/TierComparisonTable.astro +148 -0
  91. package/apps/storefront/src/components/plp/ActiveFilters.astro +79 -0
  92. package/apps/storefront/src/components/plp/FacetSidebar.astro +199 -0
  93. package/apps/storefront/src/components/plp/Pagination.astro +99 -0
  94. package/apps/storefront/src/components/plp/SortSelect.astro +64 -0
  95. package/apps/storefront/src/components/style-guide/StyleGuideNav.astro +95 -0
  96. package/apps/storefront/src/components/subscription/HowItWorksSteps.astro +67 -0
  97. package/apps/storefront/src/components/subscription/ManageSubscriptionEntry.astro +67 -0
  98. package/apps/storefront/src/components/subscription/SavingsExplainer.astro +101 -0
  99. package/apps/storefront/src/config/adminTargetTenant.ts +28 -0
  100. package/apps/storefront/src/config/devTenantSeed.ts +78 -0
  101. package/apps/storefront/src/config/devTenants.ts +16 -0
  102. package/apps/storefront/src/config/kvTenantResolver.ts +87 -0
  103. package/apps/storefront/src/config/resolver.ts +102 -0
  104. package/apps/storefront/src/config/tenants.ts +87 -0
  105. package/apps/storefront/src/layouts/Layout.astro +368 -0
  106. package/apps/storefront/src/lib/admin/README.md +122 -0
  107. package/apps/storefront/src/lib/admin/ai/approvalMode.ts +21 -0
  108. package/apps/storefront/src/lib/admin/ai/askEnvelope.ts +48 -0
  109. package/apps/storefront/src/lib/admin/ai/audit.ts +14 -0
  110. package/apps/storefront/src/lib/admin/ai/index.ts +6 -0
  111. package/apps/storefront/src/lib/admin/ai/operationalRuns.ts +155 -0
  112. package/apps/storefront/src/lib/admin/ai/recommendation.ts +20 -0
  113. package/apps/storefront/src/lib/admin/checkoutStyleInput.ts +172 -0
  114. package/apps/storefront/src/lib/admin/checkoutStyleTarget.ts +65 -0
  115. package/apps/storefront/src/lib/admin/envelope.ts +10 -0
  116. package/apps/storefront/src/lib/admin/evidence/evidenceReference.ts +32 -0
  117. package/apps/storefront/src/lib/admin/memoizeByEnvelope.ts +27 -0
  118. package/apps/storefront/src/lib/admin/orderClient.ts +146 -0
  119. package/apps/storefront/src/lib/admin/seed.ts +54 -0
  120. package/apps/storefront/src/lib/admin/services/mock/aiWorkflows.ts +59 -0
  121. package/apps/storefront/src/lib/admin/services/mock/customers.ts +86 -0
  122. package/apps/storefront/src/lib/admin/services/mock/evidence.ts +99 -0
  123. package/apps/storefront/src/lib/admin/services/mock/foxyCartSettings.ts +78 -0
  124. package/apps/storefront/src/lib/admin/services/mock/fulfillment.ts +62 -0
  125. package/apps/storefront/src/lib/admin/services/mock/index.ts +12 -0
  126. package/apps/storefront/src/lib/admin/services/mock/orders.ts +184 -0
  127. package/apps/storefront/src/lib/admin/services/mock/products.ts +105 -0
  128. package/apps/storefront/src/lib/admin/services/mock/reporting.ts +51 -0
  129. package/apps/storefront/src/lib/admin/services/mock/settings.ts +70 -0
  130. package/apps/storefront/src/lib/admin/services/mock/tenants.ts +29 -0
  131. package/apps/storefront/src/lib/admin/services/orders.ts +300 -0
  132. package/apps/storefront/src/lib/admin/viewmodels/aiWorkflows.ts +67 -0
  133. package/apps/storefront/src/lib/admin/viewmodels/customers.ts +56 -0
  134. package/apps/storefront/src/lib/admin/viewmodels/fulfillment.ts +49 -0
  135. package/apps/storefront/src/lib/admin/viewmodels/orders.ts +92 -0
  136. package/apps/storefront/src/lib/admin/viewmodels/products.ts +74 -0
  137. package/apps/storefront/src/lib/admin/viewmodels/reporting.ts +34 -0
  138. package/apps/storefront/src/lib/admin/viewmodels/settings.ts +64 -0
  139. package/apps/storefront/src/lib/adminTenantEnvelope.ts +90 -0
  140. package/apps/storefront/src/lib/analytics/index.ts +47 -0
  141. package/apps/storefront/src/lib/auth/brokerAssertion.ts +136 -0
  142. package/apps/storefront/src/lib/auth/devAuth.ts +90 -0
  143. package/apps/storefront/src/lib/auth/gatePage.ts +81 -0
  144. package/apps/storefront/src/lib/auth/identityToken.ts +341 -0
  145. package/apps/storefront/src/lib/auth/loginGate.ts +119 -0
  146. package/apps/storefront/src/lib/auth/route.ts +146 -0
  147. package/apps/storefront/src/lib/auth/session.ts +203 -0
  148. package/apps/storefront/src/lib/auth/totAccessClient.ts +503 -0
  149. package/apps/storefront/src/lib/basePath.ts +41 -0
  150. package/apps/storefront/src/lib/cfImage.ts +55 -0
  151. package/apps/storefront/src/lib/chrome/model.ts +244 -0
  152. package/apps/storefront/src/lib/chrome/parseConfig.ts +34 -0
  153. package/apps/storefront/src/lib/colors.ts +97 -0
  154. package/apps/storefront/src/lib/commerce/foxyCartSettings.ts +253 -0
  155. package/apps/storefront/src/lib/content-edit/applyDiff.ts +91 -0
  156. package/apps/storefront/src/lib/content-edit/client.ts +230 -0
  157. package/apps/storefront/src/lib/content-edit/index.ts +14 -0
  158. package/apps/storefront/src/lib/content-edit/route.ts +25 -0
  159. package/apps/storefront/src/lib/content-edit/types.ts +19 -0
  160. package/apps/storefront/src/lib/cspStaticHashes.ts +24 -0
  161. package/apps/storefront/src/lib/d1/catalog.ts +366 -0
  162. package/apps/storefront/src/lib/dashboard/staffAdmission.ts +43 -0
  163. package/apps/storefront/src/lib/dashboard/staffVendorAccess.ts +449 -0
  164. package/apps/storefront/src/lib/dashboard/tenantSelection.ts +136 -0
  165. package/apps/storefront/src/lib/dashboard/vendorQuery.ts +61 -0
  166. package/apps/storefront/src/lib/demoGate.ts +113 -0
  167. package/apps/storefront/src/lib/dev/activityIngestToken.ts +78 -0
  168. package/apps/storefront/src/lib/dev/activityStore.ts +72 -0
  169. package/apps/storefront/src/lib/dev/ai-widget-client.js +308 -0
  170. package/apps/storefront/src/lib/dev/apiBase.ts +14 -0
  171. package/apps/storefront/src/lib/dev/cliSignInCode.ts +155 -0
  172. package/apps/storefront/src/lib/dev/devDraft.ts +133 -0
  173. package/apps/storefront/src/lib/dev/hostedCockpit.ts +169 -0
  174. package/apps/storefront/src/lib/dev/livePreview.ts +77 -0
  175. package/apps/storefront/src/lib/dev/runtimeStore.ts +120 -0
  176. package/apps/storefront/src/lib/dev/storeStatus.ts +142 -0
  177. package/apps/storefront/src/lib/diag/redact.ts +20 -0
  178. package/apps/storefront/src/lib/edgeCache.ts +128 -0
  179. package/apps/storefront/src/lib/email/magicLinkInviteEmail.ts +62 -0
  180. package/apps/storefront/src/lib/env.ts +60 -0
  181. package/apps/storefront/src/lib/fixtures/sampleProducts.ts +130 -0
  182. package/apps/storefront/src/lib/format.ts +38 -0
  183. package/apps/storefront/src/lib/foxyCommerce.ts +49 -0
  184. package/apps/storefront/src/lib/imageAttrs.ts +55 -0
  185. package/apps/storefront/src/lib/jsonld.ts +166 -0
  186. package/apps/storefront/src/lib/maintenance.ts +71 -0
  187. package/apps/storefront/src/lib/membership/model.ts +97 -0
  188. package/apps/storefront/src/lib/memoryKv.ts +32 -0
  189. package/apps/storefront/src/lib/messaging/messagesClient.ts +143 -0
  190. package/apps/storefront/src/lib/placeholder.ts +99 -0
  191. package/apps/storefront/src/lib/previewReload.ts +125 -0
  192. package/apps/storefront/src/lib/priceBuckets.ts +33 -0
  193. package/apps/storefront/src/lib/rawChrome.ts +505 -0
  194. package/apps/storefront/src/lib/rawMarketingHtml.ts +136 -0
  195. package/apps/storefront/src/lib/search/index.ts +74 -0
  196. package/apps/storefront/src/lib/sections.ts +129 -0
  197. package/apps/storefront/src/lib/securityHeaders.ts +31 -0
  198. package/apps/storefront/src/lib/storyblok/content-model.ts +289 -0
  199. package/apps/storefront/src/lib/storyblok/provider.ts +607 -0
  200. package/apps/storefront/src/lib/subscription/model.ts +68 -0
  201. package/apps/storefront/src/lib/tenantRouting.ts +91 -0
  202. package/apps/storefront/src/lib/tot/ToTClient.ts +177 -0
  203. package/apps/storefront/src/lib/tot/d1Client.ts +62 -0
  204. package/apps/storefront/src/lib/tot/query.ts +249 -0
  205. package/apps/storefront/src/lib/tot/totClientInterface.ts +30 -0
  206. package/apps/storefront/src/lib/url.ts +142 -0
  207. package/apps/storefront/src/lib/wishlist.ts +63 -0
  208. package/apps/storefront/src/middleware/index.ts +455 -0
  209. package/apps/storefront/src/pages/404.astro +61 -0
  210. package/apps/storefront/src/pages/[...slug].astro +90 -0
  211. package/apps/storefront/src/pages/admin.astro +1675 -0
  212. package/apps/storefront/src/pages/api/admin/orders/[id].ts +53 -0
  213. package/apps/storefront/src/pages/api/admin/orders.ts +71 -0
  214. package/apps/storefront/src/pages/api/auth/logout.ts +21 -0
  215. package/apps/storefront/src/pages/api/auth/magic-exchange.ts +107 -0
  216. package/apps/storefront/src/pages/api/auth/send.ts +85 -0
  217. package/apps/storefront/src/pages/api/auth/verify.ts +135 -0
  218. package/apps/storefront/src/pages/api/cache-purge.ts +44 -0
  219. package/apps/storefront/src/pages/api/checkout-style.css.ts +43 -0
  220. package/apps/storefront/src/pages/api/cli/policy.ts +70 -0
  221. package/apps/storefront/src/pages/api/content-edit/ai-edit.ts +164 -0
  222. package/apps/storefront/src/pages/api/csp-report.ts +25 -0
  223. package/apps/storefront/src/pages/api/dashboard/enter-vendor.ts +124 -0
  224. package/apps/storefront/src/pages/api/dashboard/vendor-search.ts +120 -0
  225. package/apps/storefront/src/pages/api/dev/activity.ts +124 -0
  226. package/apps/storefront/src/pages/api/dev/cli-signin-code.ts +73 -0
  227. package/apps/storefront/src/pages/api/dev/diff.ts +68 -0
  228. package/apps/storefront/src/pages/api/dev/file.ts +57 -0
  229. package/apps/storefront/src/pages/api/dev/locate.ts +125 -0
  230. package/apps/storefront/src/pages/api/dev/publish.ts +128 -0
  231. package/apps/storefront/src/pages/api/dev/tree.ts +70 -0
  232. package/apps/storefront/src/pages/api/dev/write.ts +116 -0
  233. package/apps/storefront/src/pages/api/newsletter.ts +104 -0
  234. package/apps/storefront/src/pages/api/request-access.ts +172 -0
  235. package/apps/storefront/src/pages/auth/login.astro +149 -0
  236. package/apps/storefront/src/pages/auth/magic.astro +105 -0
  237. package/apps/storefront/src/pages/capabilities.astro +292 -0
  238. package/apps/storefront/src/pages/collections/[handle].astro +177 -0
  239. package/apps/storefront/src/pages/collections/index.astro +59 -0
  240. package/apps/storefront/src/pages/dashboard/[appDomain]/index.astro +100 -0
  241. package/apps/storefront/src/pages/dashboard/[appDomain]/team.astro +154 -0
  242. package/apps/storefront/src/pages/dashboard/index.astro +160 -0
  243. package/apps/storefront/src/pages/index.astro +290 -0
  244. package/apps/storefront/src/pages/llms.txt.ts +92 -0
  245. package/apps/storefront/src/pages/products/[handle].astro +402 -0
  246. package/apps/storefront/src/pages/robots.txt.ts +24 -0
  247. package/apps/storefront/src/pages/saved.astro +35 -0
  248. package/apps/storefront/src/pages/search.astro +150 -0
  249. package/apps/storefront/src/pages/sitemap.xml.ts +78 -0
  250. package/apps/storefront/src/pages/style-guide/[tenant]/[theme].astro +1280 -0
  251. package/apps/storefront/src/pages/style-guide/[tenant]/chrome/[theme].astro +229 -0
  252. package/apps/storefront/src/pages/style-guide/[tenant]/guide/[theme].astro +144 -0
  253. package/apps/storefront/src/pages/style-guide/[tenant]/index.astro +116 -0
  254. package/apps/storefront/src/pages/style-guide/index.astro +98 -0
  255. package/apps/storefront/src/pages/tenants/[id]/[...path].ts +57 -0
  256. package/apps/storefront/src/styles/global.css +292 -0
  257. package/apps/storefront/src/themes/reference.ts +68 -0
  258. package/apps/storefront/src/themes/schema.ts +111 -0
  259. package/apps/storefront/src/themes/tenants/index.ts +51 -0
  260. package/apps/storefront/tsconfig.json +16 -0
  261. package/apps/storefront/vitest.config.ts +22 -0
  262. package/package.json +26 -0
  263. package/packages/public-runtime/LICENSE +58 -0
  264. package/packages/public-runtime/package.json +22 -0
  265. package/packages/public-runtime/src/binary-path.ts +68 -0
  266. package/packages/public-runtime/src/capabilities.ts +89 -0
  267. package/packages/public-runtime/src/catalog-api.ts +81 -0
  268. package/packages/public-runtime/src/catalog-d1.ts +192 -0
  269. package/packages/public-runtime/src/checkout-style-publish.ts +200 -0
  270. package/packages/public-runtime/src/checkout-style.ts +375 -0
  271. package/packages/public-runtime/src/csp.ts +109 -0
  272. package/packages/public-runtime/src/customization-preview.ts +197 -0
  273. package/packages/public-runtime/src/customization-reconcile.ts +63 -0
  274. package/packages/public-runtime/src/customization-runtime.ts +157 -0
  275. package/packages/public-runtime/src/customization-scripts.ts +64 -0
  276. package/packages/public-runtime/src/customization-versioning.ts +95 -0
  277. package/packages/public-runtime/src/foxy-orders.ts +136 -0
  278. package/packages/public-runtime/src/foxy.ts +196 -0
  279. package/packages/public-runtime/src/hot-standby-readiness.ts +140 -0
  280. package/packages/public-runtime/src/index.ts +34 -0
  281. package/packages/public-runtime/src/product.ts +159 -0
  282. package/packages/public-runtime/src/search.ts +45 -0
  283. package/packages/public-runtime/src/tenant-assets.ts +444 -0
  284. package/packages/public-runtime/src/tenant.ts +384 -0
  285. package/packages/public-runtime/tsconfig.json +8 -0
  286. package/packages/public-runtime/vitest.config.ts +8 -0
  287. package/pnpm-workspace.yaml +7 -0
  288. package/scripts/build/copy-tenant-assets.mjs +69 -0
  289. package/scripts/dev/ai-edit.mjs +183 -0
  290. package/scripts/dev/checkout-watch.mjs +116 -0
  291. package/scripts/dev/file-browser.mjs +159 -0
  292. package/scripts/dev/file-writer.mjs +141 -0
  293. package/scripts/dev/git-status.mjs +171 -0
  294. package/scripts/dev/locate-handler.mjs +163 -0
  295. package/scripts/dev/locate.mjs +112 -0
  296. package/scripts/dev/port-check.mjs +48 -0
  297. package/scripts/dev/safe-path.mjs +109 -0
  298. package/scripts/dev/shot.mjs +46 -0
  299. package/scripts/dev/tenant-source-root.mjs +76 -0
  300. package/scripts/dev/unified-diff.mjs +81 -0
  301. package/scripts/tot-dev.mjs +376 -0
  302. package/tenants/README.md +14 -0
  303. package/tenants/home/content/chrome.json +7 -0
  304. package/tenants/home/content/home.html +58 -0
  305. package/tenants/home/content/home.json +11 -0
  306. package/tenants/home/public/js/storefront.js +210 -0
  307. package/tenants/home/public/logo-wordmark.png +0 -0
  308. package/tenants/home/public/pages/storefront.css +212 -0
  309. package/tenants/home/theme.json +6 -0
  310. package/tsconfig.base.json +20 -0
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Client behavior for the ToT-STAFF vendor search + enter on /dashboard (#8425).
3
+ *
4
+ * Served as a STATIC same-origin file (public/) and referenced via
5
+ * <script src="/js/dashboard-vendor-search.js">, so the strict production CSP
6
+ * allows it via `script-src 'self'` — no nonce, no per-build hash. (An inline
7
+ * <script> is blocked: Astro's `is:inline` drops the templated nonce, and a
8
+ * processed inline script needs its content-hash pinned in cspStaticHashes on
9
+ * every change. An external 'self' file avoids both — mirrors dashboard-team.js.)
10
+ *
11
+ * Two session-authed, staff-gated endpoints (no server-injected values, so this
12
+ * file is fully static):
13
+ * GET /api/dashboard/vendor-search?q=<prefix> → { vendors: [{appDomain, displayName}] }
14
+ * POST /api/dashboard/enter-vendor {vendorAppDomain} → { ok, redirect } (writes audit)
15
+ */
16
+ (function () {
17
+ const $ = (id) => document.getElementById(id);
18
+ const input = $("vendorSearch");
19
+ if (!input) return; // non-staff page — nothing to wire.
20
+ const results = $("vendorResults");
21
+ const msg = $("vendorSearchMsg");
22
+
23
+ let seq = 0; // guards against out-of-order responses (last query wins).
24
+ let timer = null;
25
+
26
+ function showMsg(text, isErr) {
27
+ msg.textContent = text || "";
28
+ msg.className = isErr ? "err" : "";
29
+ }
30
+
31
+ function clearResults() {
32
+ results.replaceChildren();
33
+ }
34
+
35
+ function renderResults(vendors) {
36
+ clearResults();
37
+ if (!vendors || !vendors.length) {
38
+ showMsg("No matching vendors.");
39
+ return;
40
+ }
41
+ showMsg("");
42
+ for (const v of vendors) {
43
+ const li = document.createElement("li");
44
+ const btn = document.createElement("button");
45
+ btn.type = "button";
46
+ btn.dataset.appdomain = v.appDomain;
47
+ const dn = document.createElement("span");
48
+ dn.className = "dn";
49
+ dn.textContent = v.displayName || v.appDomain;
50
+ const ad = document.createElement("span");
51
+ ad.className = "ad";
52
+ ad.textContent = v.appDomain;
53
+ btn.appendChild(dn);
54
+ btn.appendChild(ad);
55
+ btn.addEventListener("click", () => enter(v.appDomain, btn));
56
+ li.appendChild(btn);
57
+ results.appendChild(li);
58
+ }
59
+ }
60
+
61
+ async function search(q) {
62
+ const mine = ++seq;
63
+ try {
64
+ const res = await fetch("/api/dashboard/vendor-search?q=" + encodeURIComponent(q), {
65
+ headers: { accept: "application/json" },
66
+ });
67
+ if (mine !== seq) return; // a newer query superseded this one.
68
+ const body = await res.json().catch(() => ({}));
69
+ if (res.ok) {
70
+ renderResults(body.vendors);
71
+ } else {
72
+ clearResults();
73
+ showMsg(body.error || "Search failed (HTTP " + res.status + ").", true);
74
+ }
75
+ } catch (e) {
76
+ if (mine !== seq) return;
77
+ clearResults();
78
+ showMsg("Search failed: " + (e && e.message ? e.message : e), true);
79
+ }
80
+ }
81
+
82
+ async function enter(appDomain, btn) {
83
+ if (btn) btn.disabled = true;
84
+ showMsg("Entering " + appDomain + "…");
85
+ try {
86
+ const res = await fetch("/api/dashboard/enter-vendor", {
87
+ method: "POST",
88
+ headers: { "content-type": "application/json", accept: "application/json" },
89
+ body: JSON.stringify({ vendorAppDomain: appDomain }),
90
+ });
91
+ const body = await res.json().catch(() => ({}));
92
+ if (res.ok && body.redirect) {
93
+ window.location.assign(body.redirect);
94
+ return;
95
+ }
96
+ showMsg(body.error || "Couldn't enter that vendor (HTTP " + res.status + ").", true);
97
+ } catch (e) {
98
+ showMsg("Couldn't enter: " + (e && e.message ? e.message : e), true);
99
+ } finally {
100
+ if (btn) btn.disabled = false;
101
+ }
102
+ }
103
+
104
+ input.addEventListener("input", () => {
105
+ const q = input.value.trim();
106
+ if (timer) clearTimeout(timer);
107
+ if (!q) {
108
+ seq++; // cancel any in-flight render.
109
+ clearResults();
110
+ showMsg("");
111
+ return;
112
+ }
113
+ timer = setTimeout(() => search(q), 200); // debounce keystrokes.
114
+ });
115
+ })();
@@ -0,0 +1,369 @@
1
+ /* ============================================================================
2
+ Shared commerce-chrome stylesheet — ONE platform-shipped, token-driven layer
3
+ (workstream shared-ecommerce-widgets, unit F2 `shared-chrome-css`).
4
+
5
+ Generalizes the divergent per-tenant `public/pages/mkt.css` files (some tenants
6
+ shared a byte-identical base sheet; others carried a richer variant with .mega /
7
+ .member-cue / .announce-bar / .age-gate / .trust-marks) into a SINGLE stylesheet
8
+ every storefront links and themes via
9
+ tokens only. Per decisions raw-chrome-single-stylesheet + materialization-compat.
10
+
11
+ CONTRACT-TOKEN-ONLY (docs/architecture/token-contract.md, unit F1). Every value
12
+ here reads the shared `--color-*` / `--font-*` / `--radius-*` / `--shadow-*` /
13
+ `--container-max` vocabulary and NO tenant-private brand var (never `--navy`,
14
+ `--brand-red`, unprefixed `--sale`/`--ink`/`--surface`), and no hard-coded brand
15
+ hex. On-dark surfaces (footer, age-gate overlay, cat cards) derive from
16
+ `--color-text` (the deep ink surface) + `--color-primary-contrast` via
17
+ `color-mix`, so a tenant reskins the whole chrome by supplying tokens/aliases.
18
+
19
+ Token mapping from the old per-tenant vars (applied by the integration units
20
+ that alias each tenant's brand vocabulary onto the contract):
21
+ --navy / --brand-red -> --color-primary (nav active, cart, CTA fill)
22
+ --navy-dark -> --color-primary-hover
23
+ --sale -> --color-sale (flag / NEW / marquee emphasis)
24
+ --surface / --surface-2 / --border / --muted -> --color-{surface,surface-2,border,muted}
25
+ --ink / --ink-2 -> --color-text
26
+ --brand-blue -> --color-accent (member cue, promo/trust spark)
27
+ --container -> --container-max
28
+ footer / age-gate ink surfaces -> --color-text (+ --color-primary-contrast)
29
+
30
+ DELIVERY: platform static asset, served at /shared/commerce-chrome.css (root,
31
+ global — like /brand, /js). It is a LINKED sheet, not inline: commerce routes
32
+ (/products, /collections) carry linked <link> sheets but strip inline <style>,
33
+ which is exactly the PVC breakage this closes. The canonical chrome partial (F3)
34
+ links it; per-tenant chrome retires its copied mkt.css at integration (I1/I2/I3).
35
+ rawMarketingHtml.prefixTenantLinks exempts /shared/ so the link survives
36
+ path-prefix tenant routing (as it already does for /tenants/).
37
+ ========================================================================== */
38
+
39
+ /* --- Layout --------------------------------------------------------------- */
40
+ /* Clip (not hidden) so a closed off-canvas mobile drawer never creates a
41
+ horizontal scrollbar, while position:sticky on the header still works. */
42
+ body { overflow-x: clip; }
43
+ .container { width: 100%; max-width: var(--container-max); margin-inline: auto; padding-inline: clamp(16px, 4vw, 40px); }
44
+ .section { padding-block: clamp(2.5rem, 6vw, 4.5rem); }
45
+ .section--tight { padding-block: clamp(1.75rem, 4vw, 3rem); }
46
+ .section--surface { background: var(--color-surface); }
47
+
48
+ /* --- Buttons -------------------------------------------------------------- */
49
+ .btn {
50
+ display: inline-flex; align-items: center; justify-content: center; gap: .5em;
51
+ font-family: var(--font-display); font-weight: 600; font-size: .95rem;
52
+ padding: .8em 1.6em; border-radius: var(--radius-full); border: 2px solid transparent;
53
+ transition: transform .12s ease, background .15s ease, color .15s ease, box-shadow .15s ease;
54
+ }
55
+ .btn:hover { transform: translateY(-1px); }
56
+ .btn-primary { background: var(--color-primary); color: var(--color-primary-contrast); }
57
+ .btn-primary:hover { background: var(--color-primary-hover); box-shadow: var(--shadow-md); }
58
+ .btn-secondary { background: var(--color-accent); color: var(--color-primary-contrast); }
59
+ .btn-secondary:hover { background: var(--color-accent-hover); }
60
+ .btn-line { border-color: var(--color-primary-contrast); color: var(--color-primary-contrast); }
61
+ .btn-line:hover { background: var(--color-primary-contrast); color: var(--color-primary); }
62
+ .btn-ghost { border-color: var(--color-border); color: var(--color-primary); background: var(--color-surface); }
63
+ .btn-ghost:hover { border-color: var(--color-primary); }
64
+ .btn-outline { background: transparent; color: var(--color-accent); border-color: var(--color-border); }
65
+ .btn-outline:hover { border-color: var(--color-accent); }
66
+
67
+ /* --- Announcement variants ------------------------------------------------ */
68
+ /* (1) solid brand bar */
69
+ .announce {
70
+ background: var(--color-primary); color: var(--color-primary-contrast); text-align: center;
71
+ font-size: .85rem; font-weight: 600; letter-spacing: .01em; padding: 9px 16px;
72
+ }
73
+ /* (2) scrolling sale marquee */
74
+ .announce-marquee { background: var(--color-sale); color: var(--color-primary-contrast); overflow: hidden; }
75
+ .announce-marquee__track {
76
+ display: flex; gap: 3rem; white-space: nowrap; width: max-content;
77
+ padding: 8px 0; font-size: .82rem; font-weight: 600;
78
+ animation: chrome-marquee 22s linear infinite;
79
+ }
80
+ .announce-marquee__track span { padding-inline: 1.5rem; }
81
+ @keyframes chrome-marquee { from { transform: translateX(0); } to { transform: translateX(-50%); } }
82
+ @media (prefers-reduced-motion: reduce) {
83
+ .announce-marquee__track { animation: none; justify-content: center; width: 100%; }
84
+ }
85
+ /* (3) dismissible promo bar (accent fill + contrast text — accent is a FILL
86
+ token like --color-primary, so its text follows the same on-primary
87
+ contrast token; --color-text assumed accent was always light, which axe
88
+ caught as a real color-contrast failure once accent is a deep brand hue). */
89
+ .announce-bar { background: var(--color-accent); color: var(--color-primary-contrast); }
90
+ .announce-bar[hidden] { display: none; }
91
+ .announce-bar .container { display: flex; align-items: center; justify-content: center; gap: 14px; padding-block: 9px; position: relative; }
92
+ .announce-bar__msg { font-size: .84rem; font-weight: 700; letter-spacing: .01em; text-align: center; }
93
+ .announce-bar__msg a { color: var(--color-primary-contrast); text-decoration: underline; }
94
+ .announce-bar__close { position: absolute; right: clamp(16px, 4vw, 40px); background: none; border: 0; color: var(--color-primary-contrast); font-size: 1.1rem; line-height: 1; padding: 4px 6px; border-radius: var(--radius-sm); }
95
+ .announce-bar__close:hover { background: color-mix(in srgb, var(--color-primary-contrast) 12%, transparent); }
96
+
97
+ /* --- Top utility bar (FDA warning + utility nav) -------------------------- */
98
+ .topbar-warning {
99
+ background: var(--color-surface); border-bottom: 1px solid var(--color-border);
100
+ text-align: center; padding: 10px 16px;
101
+ }
102
+ .topbar-warning p { color: var(--color-text); font-weight: 700; font-size: .82rem; letter-spacing: .01em; }
103
+
104
+ .utility-nav { border-bottom: 1px solid var(--color-border); background: var(--color-surface); }
105
+ .utility-nav .container { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding-block: 8px; }
106
+ .utility-nav ul { display: flex; flex-wrap: wrap; gap: 20px; }
107
+ .utility-nav a { font-size: .82rem; font-weight: 600; color: var(--color-text); }
108
+ .utility-nav a:hover { color: var(--color-primary); }
109
+ .utility-nav__right { display: flex; align-items: center; gap: 16px; }
110
+ .utility-nav__right .sel { font-size: .8rem; color: var(--color-muted); }
111
+ .utility-nav__right .social { display: flex; gap: 12px; }
112
+ .utility-nav__right .social a svg { width: 16px; height: 16px; fill: var(--color-text); }
113
+
114
+ /* Member-price affordance (persistent, guests) */
115
+ .member-cue { display: inline-flex; align-items: center; gap: 7px; font-size: .8rem; font-weight: 700; color: var(--color-accent); }
116
+ .member-cue::before { content: ""; width: 8px; height: 8px; border-radius: var(--radius-full); background: var(--color-accent); }
117
+ .member-cue:hover { color: var(--color-accent-hover); text-decoration: underline; }
118
+
119
+ /* --- Header / nav --------------------------------------------------------- */
120
+ .site-header { position: sticky; top: 0; z-index: 100; background: var(--color-surface); border-bottom: 1px solid var(--color-border); }
121
+ .site-header .header-row { gap: clamp(12px, 3vw, 40px); }
122
+ .header-row { display: flex; flex-wrap: wrap; align-items: center; gap: clamp(12px, 2.5vw, 32px); padding-block: 14px; }
123
+ /* Logo: image mark (bigd/giant) or text wordmark (PVC) both supported. */
124
+ .brand img { height: 46px; width: auto; }
125
+ .brand { display: inline-flex; align-items: baseline; gap: .3ch; font-family: var(--font-display); font-weight: 800; font-size: clamp(1.1rem, 2.2vw, 1.4rem); letter-spacing: .02em; white-space: nowrap; }
126
+ .brand .b-prime { color: var(--color-accent); }
127
+ .brand .b-vapor { color: var(--color-text); }
128
+ .brand .b-club { color: var(--color-primary); }
129
+
130
+ .header-search { flex: 1; max-width: 560px; position: relative; }
131
+ .header-search input {
132
+ width: 100%; border: 1px solid var(--color-border); border-radius: var(--radius-full);
133
+ padding: 11px 46px 11px 18px; font-size: .9rem; background: var(--color-surface); color: var(--color-text);
134
+ }
135
+ .header-search input::placeholder { color: var(--color-muted); }
136
+ .header-search button { position: absolute; right: 6px; top: 50%; transform: translateY(-50%); background: none; border: 0; padding: 6px; }
137
+ .header-search svg { width: 20px; height: 20px; stroke: var(--color-primary); fill: none; stroke-width: 2; }
138
+
139
+ .header-actions { display: flex; align-items: center; gap: 16px; }
140
+ .header-actions a { display: inline-flex; align-items: center; color: var(--color-text); }
141
+ .header-actions a:hover { color: var(--color-primary); }
142
+ .header-actions svg { width: 24px; height: 24px; stroke: currentColor; fill: none; stroke-width: 1.8; }
143
+ .header-actions .cart { position: relative; }
144
+ .header-actions .cart .count {
145
+ position: absolute; top: -6px; right: -8px; background: var(--color-primary); color: var(--color-primary-contrast);
146
+ font-size: .62rem; font-weight: 700; min-width: 16px; height: 16px; border-radius: var(--radius-full);
147
+ display: grid; place-items: center; padding: 0 4px;
148
+ }
149
+ .hamburger { display: none; flex-direction: column; gap: 5px; background: none; border: 0; padding: 6px; }
150
+ .hamburger span { width: 26px; height: 2px; background: var(--color-text); border-radius: 2px; }
151
+
152
+ /* --- Primary nav + mega-menu ---------------------------------------------- */
153
+ .primary-nav { flex: 1; }
154
+ .primary-nav > ul { display: flex; flex-wrap: wrap; align-items: center; gap: clamp(10px, 1.6vw, 22px); }
155
+ /* Flat links (bigd/giant) */
156
+ .primary-nav a { font-weight: 600; font-size: .92rem; color: var(--color-text); padding: 6px 2px; border-bottom: 2px solid transparent; transition: color .15s, border-color .15s; }
157
+ .primary-nav a:hover { color: var(--color-primary); border-color: var(--color-primary); }
158
+ .primary-nav a.is-flag { color: var(--color-sale); }
159
+ /* Nav items with dropdowns (PVC mega) */
160
+ .primary-nav .nav-item { position: relative; }
161
+ .primary-nav .nav-item > a { display: inline-flex; align-items: center; gap: 5px; font-weight: 600; font-size: .92rem; color: var(--color-text); padding: 14px 2px; border-bottom: 2px solid transparent; transition: color .15s, border-color .15s; }
162
+ .primary-nav .nav-item > a:hover, .primary-nav .nav-item:focus-within > a { color: var(--color-primary); border-color: var(--color-primary); }
163
+ .primary-nav .nav-item > a.is-flag { color: var(--color-sale); }
164
+ .primary-nav .nav-item > a.is-flag:hover, .primary-nav .nav-item.is-flag:focus-within > a { border-color: var(--color-sale); }
165
+ .primary-nav .caret, .primary-nav a .caret { font-size: .7em; margin-left: 4px; opacity: .7; }
166
+ .primary-nav a .new-badge, .primary-nav .new-badge {
167
+ display: inline-block; background: var(--color-sale); color: var(--color-primary-contrast); font-size: .58rem; font-weight: 800;
168
+ letter-spacing: .06em; padding: 2px 5px; border-radius: var(--radius-sm); margin-left: 6px; vertical-align: middle;
169
+ }
170
+ /* Second-row nav bar (bigd) + nav-bar container (PVC) */
171
+ .primary-nav-bar { border-top: 1px solid var(--color-border); background: var(--color-surface); }
172
+ .primary-nav-bar .container { padding-block: 4px; }
173
+ .primary-nav-bar .primary-nav > ul { gap: clamp(10px, 1.4vw, 20px); }
174
+ .primary-nav-bar .primary-nav a { padding-block: 10px; }
175
+
176
+ .mega { position: absolute; top: 100%; left: 0; z-index: 90; min-width: 260px; background: var(--color-surface); border: 1px solid var(--color-border); border-radius: 0 0 var(--radius-md) var(--radius-md); box-shadow: var(--shadow-md); padding: 14px; opacity: 0; visibility: hidden; transform: translateY(6px); transition: opacity .14s ease, transform .14s ease, visibility .14s; }
177
+ .mega--wide { min-width: 460px; }
178
+ .nav-item:hover > .mega, .nav-item:focus-within > .mega { opacity: 1; visibility: visible; transform: none; }
179
+ .mega__cols { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 4px 22px; }
180
+ .mega__col h4 { font-size: .72rem; letter-spacing: .1em; text-transform: uppercase; color: var(--color-muted); margin: 6px 8px 4px; }
181
+ .mega a { display: block; padding: 8px; border-radius: var(--radius-sm); font-size: .9rem; color: var(--color-text); font-weight: 500; }
182
+ .mega a:hover { background: var(--color-surface-2); color: var(--color-primary); text-decoration: none; }
183
+ /* Keyboard parity: tabbing a nav trigger opens its panel (:focus-within above);
184
+ give every keyboard-focused nav + mega link a visible token-tinted ring. */
185
+ .primary-nav a:focus-visible,
186
+ .primary-nav .nav-item > a:focus-visible,
187
+ .mega a:focus-visible {
188
+ outline: 2px solid var(--color-primary); outline-offset: 2px; border-radius: var(--radius-sm);
189
+ }
190
+
191
+ /* --- Section headers ------------------------------------------------------ */
192
+ .sec-head { text-align: center; max-width: 62ch; margin: 0 auto clamp(1.5rem, 3vw, 2.5rem); }
193
+ .sec-head .eyebrow { display: inline-block; font-family: var(--font-display); font-weight: 700; font-size: .78rem; letter-spacing: .12em; text-transform: uppercase; color: var(--color-primary); margin-bottom: .55rem; }
194
+ .sec-head p { margin-top: .7rem; font-size: 1.02rem; }
195
+ .sec-head--between { display: flex; align-items: baseline; justify-content: space-between; text-align: left; max-width: none; gap: 16px; }
196
+ .link-more { font-family: var(--font-display); font-weight: 600; color: var(--color-primary); white-space: nowrap; }
197
+ .link-more:hover { text-decoration: underline; }
198
+
199
+ /* --- Brand / category tile grid ------------------------------------------- */
200
+ .tile-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: clamp(12px, 1.6vw, 20px); }
201
+ .tile {
202
+ display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 14px;
203
+ background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius-lg);
204
+ padding: 26px 18px; min-height: 148px; text-align: center; transition: transform .14s, box-shadow .14s, border-color .14s;
205
+ }
206
+ .tile:hover { transform: translateY(-3px); box-shadow: var(--shadow-md); border-color: var(--color-surface-2); }
207
+ .tile img { max-height: 66px; width: auto; object-fit: contain; }
208
+ .tile span { font-family: var(--font-display); font-weight: 600; font-size: .95rem; color: var(--color-text); }
209
+
210
+ /* --- Brand grid / logo wall (Shop by Brands) ------------------------------ */
211
+ /* Denser than .tile-grid: auto-fills many brand logos, with a text wordmark
212
+ fallback (.brand-tile__text or bare anchor text) when a logo isn't rehosted.
213
+ Raw-chrome parity for the shared <BrandGrid> component (unit W1). Covers both
214
+ image-logo walls (~dozens of logos) and text-wordmark tiles. */
215
+ .brand-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: clamp(10px, 1.4vw, 16px); }
216
+ .brand-tile {
217
+ display: flex; align-items: center; justify-content: center;
218
+ min-height: 92px; padding: 18px 16px; text-align: center;
219
+ background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius-md);
220
+ font-family: var(--font-display); font-weight: 700; font-size: .95rem; letter-spacing: .01em; color: var(--color-text);
221
+ transition: transform .14s, box-shadow .14s, border-color .14s, color .14s;
222
+ }
223
+ .brand-tile:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); border-color: var(--color-primary); color: var(--color-primary); }
224
+ .brand-tile img { max-height: 46px; max-width: 100%; width: auto; object-fit: contain; }
225
+ .brand-tile__text { display: inline-block; }
226
+
227
+ /* --- Blog / article cards + tag-filter cloud (W4) -------------------------- */
228
+ /* Raw-chrome parity for the shared <ArticleCard> + <TagFilterCloud> components.
229
+ Presentational over imported/source-copied blog data — never fabricated. */
230
+ .article-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: clamp(16px, 2.2vw, 28px); }
231
+ .article-card { position: relative; display: flex; flex-direction: column; overflow: hidden; background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius-lg); transition: transform .14s ease, box-shadow .14s ease, border-color .14s ease; }
232
+ .article-card:hover { transform: translateY(-3px); box-shadow: var(--shadow-md); border-color: var(--color-surface-2); }
233
+ .article-card__media { display: block; aspect-ratio: 16 / 10; overflow: hidden; background: var(--color-surface-2); }
234
+ .article-card__media img { width: 100%; height: 100%; object-fit: cover; transition: transform .3s ease; }
235
+ .article-card:hover .article-card__media img { transform: scale(1.04); }
236
+ .article-card__ph { display: block; width: 100%; height: 100%; background: linear-gradient(135deg, var(--color-surface-2), color-mix(in srgb, var(--color-primary) 12%, var(--color-surface-2))); }
237
+ .article-card__body { display: flex; flex-direction: column; gap: .5rem; padding: clamp(16px, 2vw, 22px); }
238
+ .article-card__tag { align-self: flex-start; position: relative; z-index: 1; font-family: var(--font-display); font-weight: 700; font-size: .68rem; letter-spacing: .08em; text-transform: uppercase; color: var(--color-primary); }
239
+ a.article-card__tag:hover { text-decoration: underline; }
240
+ .article-card__title { font-family: var(--font-display); font-weight: 700; font-size: clamp(1.05rem, 1.6vw, 1.2rem); line-height: 1.25; color: var(--color-text); }
241
+ .article-card__title a { color: inherit; }
242
+ .article-card__title a::after { content: ""; position: absolute; inset: 0; }
243
+ .article-card__title a:hover { color: var(--color-primary); }
244
+ .article-card__excerpt { font-size: .92rem; line-height: 1.5; color: var(--color-muted); display: -webkit-box; -webkit-line-clamp: 3; line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
245
+ .article-card__meta { display: flex; flex-wrap: wrap; align-items: center; gap: .35rem .75rem; margin-top: .15rem; font-size: .78rem; color: var(--color-muted); }
246
+ .article-card__meta > * + *::before { content: "\00b7"; margin-right: .75rem; color: var(--color-border); }
247
+ .article-card__author { font-weight: 600; color: var(--color-text); }
248
+
249
+ .tag-cloud__title { font-family: var(--font-display); font-weight: 700; font-size: .72rem; letter-spacing: .1em; text-transform: uppercase; color: var(--color-muted); margin-bottom: .6rem; }
250
+ .tag-cloud { display: flex; flex-wrap: wrap; gap: 8px; }
251
+ .tag-chip { display: inline-flex; align-items: center; gap: 6px; padding: 6px 14px; border: 1px solid var(--color-border); border-radius: var(--radius-full); background: var(--color-surface); color: var(--color-text); font-size: .85rem; font-weight: 600; transition: background .15s ease, color .15s ease, border-color .15s ease; }
252
+ .tag-chip:hover { border-color: var(--color-primary); color: var(--color-primary); }
253
+ .tag-chip.is-active { background: var(--color-primary); border-color: var(--color-primary); color: var(--color-primary-contrast); }
254
+ .tag-chip__count { font-size: .72rem; font-weight: 700; color: var(--color-muted); }
255
+ .tag-chip.is-active .tag-chip__count { color: color-mix(in srgb, var(--color-primary-contrast) 78%, transparent); }
256
+ .tag-chip:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 2px; }
257
+
258
+ /* --- Category cards (shop by category) ------------------------------------ */
259
+ .cat-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: clamp(14px, 2vw, 24px); }
260
+ .cat-card {
261
+ position: relative; display: flex; align-items: flex-end; overflow: hidden;
262
+ min-height: 190px; border-radius: var(--radius-lg); color: var(--color-primary-contrast);
263
+ background: linear-gradient(135deg, var(--color-primary), var(--color-primary-hover));
264
+ padding: 22px; transition: transform .14s, box-shadow .14s;
265
+ }
266
+ .cat-card:hover { transform: translateY(-3px); box-shadow: var(--shadow-lg); }
267
+ .cat-card::after { content: "→"; position: absolute; top: 20px; right: 22px; font-size: 1.3rem; opacity: .85; }
268
+ .cat-card h3 { color: var(--color-primary-contrast); font-size: 1.35rem; }
269
+ .cat-card p { color: color-mix(in srgb, var(--color-primary-contrast) 82%, transparent); margin-top: 4px; font-size: .9rem; }
270
+
271
+ /* --- Compliance strip (source-copied legal text; not fabricated) ---------- */
272
+ .compliance-strip { background: var(--color-surface-2); border-top: 1px solid var(--color-border); border-bottom: 1px solid var(--color-border); }
273
+ .compliance-strip .container { padding-block: 14px; }
274
+ .compliance-strip p { color: var(--color-text); font-size: .82rem; font-weight: 600; text-align: center; letter-spacing: .01em; }
275
+
276
+ /* --- Age gate (21+ interstitial; remembered per device) ------------------- */
277
+ /* Overlay + on-dark surfaces derive from --color-text (deep ink) so no brand hex. */
278
+ .age-gate { position: fixed; inset: 0; z-index: 1000; display: none; align-items: center; justify-content: center; padding: 24px; background: color-mix(in srgb, var(--color-text) 78%, transparent); }
279
+ .age-gate.is-open { display: flex; }
280
+ .age-gate__panel { width: min(30rem, 100%); background: var(--color-surface); border-radius: var(--radius-lg); box-shadow: var(--shadow-lg); padding: clamp(24px, 4vw, 40px); text-align: center; }
281
+ .age-gate__mark { font-family: var(--font-display); font-weight: 800; letter-spacing: .04em; font-size: 1.05rem; color: var(--color-text); }
282
+ .age-gate__mark .is-red { color: var(--color-primary); }
283
+ .age-gate h2 { margin-top: 14px; font-size: clamp(1.4rem, 3vw, 1.85rem); }
284
+ .age-gate p { margin-top: 10px; color: var(--color-text); }
285
+ .age-gate__actions { display: flex; flex-wrap: wrap; gap: 12px; justify-content: center; margin-top: 24px; }
286
+ .age-gate__actions .btn { min-width: 9rem; }
287
+ .age-gate__deny { display: none; }
288
+ .age-gate.is-denied .age-gate__enter, .age-gate.is-denied .age-gate__lede { display: none; }
289
+ .age-gate.is-denied .age-gate__deny { display: block; margin-top: 18px; color: var(--color-error); font-weight: 600; }
290
+ .age-gate__fine { margin-top: 18px; font-size: .78rem; color: var(--color-muted); }
291
+
292
+ /* --- Footer --------------------------------------------------------------- */
293
+ /* Deep-ink surface (= --color-text) with light contrast text; dimmer tiers and
294
+ hairlines derive from --color-primary-contrast via color-mix. */
295
+ .site-footer { background: var(--color-text); color: color-mix(in srgb, var(--color-primary-contrast) 82%, transparent); padding-block: clamp(2.5rem, 5vw, 4rem) 1.5rem; }
296
+ .footer-grid { display: grid; grid-template-columns: 1.4fr repeat(3, 1fr); gap: clamp(24px, 4vw, 56px); }
297
+ .footer-brand img { height: 40px; filter: brightness(0) invert(1); opacity: .95; }
298
+ .footer-brand p { color: color-mix(in srgb, var(--color-primary-contrast) 66%, transparent); margin-top: 14px; font-size: .9rem; max-width: 34ch; }
299
+ /* The header .brand .b-prime/.b-vapor/.b-club rule set (line ~123) is tuned
300
+ for the light --color-surface header; on the dark --color-text footer
301
+ background every segment needs the on-dark contrast token, not just
302
+ .b-vapor — .b-prime/.b-club fell through to the header colors uncorrected,
303
+ which axe caught as color-contrast failures. */
304
+ .footer-brand .brand .b-prime,
305
+ .footer-brand .brand .b-vapor,
306
+ .footer-brand .brand .b-club { color: var(--color-primary-contrast); }
307
+ .footer-col h4 { color: var(--color-primary-contrast); font-size: .82rem; letter-spacing: .1em; text-transform: uppercase; margin-bottom: 14px; }
308
+ .footer-col ul { display: flex; flex-direction: column; gap: 9px; }
309
+ .footer-col a { color: color-mix(in srgb, var(--color-primary-contrast) 78%, transparent); font-size: .92rem; }
310
+ .footer-col a:hover { color: var(--color-primary-contrast); text-decoration: underline; }
311
+ /* Newsletter signup (PVC) */
312
+ .footer-news { margin-top: 14px; }
313
+ .footer-news__row { display: flex; gap: 8px; }
314
+ .footer-news input[type="email"] { flex: 1; min-width: 0; border: 1px solid color-mix(in srgb, var(--color-primary-contrast) 22%, transparent); border-radius: var(--radius-md); padding: 10px 12px; font-size: .88rem; background: color-mix(in srgb, var(--color-text) 88%, var(--color-primary-contrast)); color: var(--color-primary-contrast); }
315
+ .footer-news input[type="email"]::placeholder { color: color-mix(in srgb, var(--color-primary-contrast) 55%, transparent); }
316
+ .footer-news button { flex: none; background: var(--color-primary); color: var(--color-primary-contrast); border: 0; border-radius: var(--radius-md); padding: 0 16px; font-weight: 700; }
317
+ .footer-news button:hover { background: var(--color-primary-hover); }
318
+ .footer-attest { display: flex; gap: 8px; align-items: flex-start; margin-top: 10px; font-size: .76rem; color: color-mix(in srgb, var(--color-primary-contrast) 66%, transparent); }
319
+ .footer-attest input { margin-top: 3px; }
320
+ .pay-badges { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 16px; }
321
+ .pay-badges span { font-size: .62rem; font-weight: 700; letter-spacing: .04em; color: color-mix(in srgb, var(--color-primary-contrast) 82%, transparent); border: 1px solid color-mix(in srgb, var(--color-primary-contrast) 22%, transparent); border-radius: var(--radius-sm); padding: 4px 7px; }
322
+ /* Trust marks strip (PVC) */
323
+ .trust-marks { display: flex; flex-wrap: wrap; gap: 8px 18px; margin-top: 30px; padding-top: 20px; border-top: 1px solid color-mix(in srgb, var(--color-primary-contrast) 12%, transparent); }
324
+ .trust-marks span { display: inline-flex; align-items: center; gap: 7px; font-size: .82rem; font-weight: 600; color: color-mix(in srgb, var(--color-primary-contrast) 82%, transparent); }
325
+ .trust-marks svg { width: 17px; height: 17px; stroke: var(--color-accent); fill: none; stroke-width: 1.8; }
326
+ .footer-warning { margin-top: 30px; padding: 16px 18px; border: 1px solid color-mix(in srgb, var(--color-primary-contrast) 22%, transparent); border-radius: var(--radius-md); }
327
+ .footer-warning strong { color: var(--color-primary-contrast); }
328
+ .footer-warning p { color: color-mix(in srgb, var(--color-primary-contrast) 78%, transparent); font-size: .82rem; }
329
+ .footer-bottom { margin-top: 28px; padding-top: 18px; border-top: 1px solid color-mix(in srgb, var(--color-primary-contrast) 16%, transparent); display: flex; flex-wrap: wrap; gap: 10px 24px; justify-content: space-between; align-items: center; }
330
+ .footer-bottom p, .footer-bottom a { color: color-mix(in srgb, var(--color-primary-contrast) 60%, transparent); font-size: .82rem; }
331
+
332
+ /* --- Reveal-on-scroll (raw-chrome markup uses .reveal/.in) ---------------- */
333
+ .reveal { opacity: 0; transform: translateY(18px); transition: opacity .5s ease, transform .5s ease; }
334
+ .reveal.in { opacity: 1; transform: none; }
335
+ @media (prefers-reduced-motion: reduce) {
336
+ .reveal { opacity: 1; transform: none; transition: none; }
337
+ }
338
+
339
+ /* --- Responsive ----------------------------------------------------------- */
340
+ @media (max-width: 960px) {
341
+ .header-search { order: 3; flex-basis: 100%; max-width: none; }
342
+ .utility-nav ul { gap: 12px; }
343
+ .footer-grid { grid-template-columns: 1fr 1fr; }
344
+ }
345
+ @media (max-width: 1000px) {
346
+ .tile-grid { grid-template-columns: repeat(3, 1fr); }
347
+ }
348
+ @media (max-width: 860px) {
349
+ .primary-nav { position: fixed; inset: 0 0 0 auto; width: min(84vw, 340px); background: var(--color-surface); box-shadow: var(--shadow-lg); transform: translateX(100%); transition: transform .25s ease; padding: 78px 20px 24px; overflow-y: auto; z-index: 120; }
350
+ .primary-nav.open { transform: none; }
351
+ .primary-nav > ul { flex-direction: column; align-items: stretch; gap: 4px; }
352
+ .primary-nav a, .primary-nav .nav-item > a { display: block; padding: 12px 4px; border-bottom: 1px solid var(--color-border); }
353
+ .hamburger { display: flex; }
354
+ /* Mega panels: inline accordion on mobile */
355
+ .mega { position: static; opacity: 1; visibility: visible; transform: none; box-shadow: none; border: 0; border-radius: 0; padding: 4px 4px 8px 14px; min-width: 0; }
356
+ .mega--wide { min-width: 0; }
357
+ .mega__cols { grid-template-columns: 1fr; }
358
+ .cat-grid { grid-template-columns: 1fr 1fr; }
359
+ .article-grid { grid-template-columns: 1fr 1fr; }
360
+ .footer-grid { grid-template-columns: 1fr 1fr; }
361
+ }
362
+ @media (max-width: 560px) {
363
+ .tile-grid { grid-template-columns: repeat(2, 1fr); }
364
+ .brand-grid { grid-template-columns: repeat(auto-fill, minmax(44%, 1fr)); }
365
+ .cat-grid { grid-template-columns: 1fr; }
366
+ .article-grid { grid-template-columns: 1fr; }
367
+ .footer-grid { grid-template-columns: 1fr; }
368
+ .utility-nav .container { flex-wrap: wrap; }
369
+ }
@@ -0,0 +1,34 @@
1
+ ---
2
+ /**
3
+ * <Badge> — tiny mono-uppercase pill used over product imagery and inline.
4
+ * Theme-agnostic: colors map to CSS-var-backed Tailwind utilities.
5
+ */
6
+ interface Props {
7
+ /** `soldout` is the out-of-stock state; `compliant` marks a
8
+ * compliance-verified item (age/lab/regulatory). */
9
+ variant: "sale" | "new" | "low-stock" | "soldout" | "compliant";
10
+ /** Visible text. May also be supplied via the default slot. */
11
+ label?: string;
12
+ class?: string;
13
+ }
14
+
15
+ const { variant, label, class: className } = Astro.props;
16
+
17
+ const variants: Record<Props["variant"], string> = {
18
+ sale: "bg-sale text-white",
19
+ new: "bg-primary text-primary-contrast",
20
+ "low-stock": "bg-accent text-white",
21
+ soldout: "bg-surface text-muted border border-border",
22
+ compliant: "bg-[var(--color-success)] text-white",
23
+ };
24
+ ---
25
+
26
+ <span
27
+ class:list={[
28
+ "inline-flex items-center rounded-full px-2 py-0.5 font-mono text-[0.62rem] uppercase tracking-[0.12em] leading-none whitespace-nowrap",
29
+ variants[variant],
30
+ className,
31
+ ]}
32
+ >
33
+ {label}<slot />
34
+ </span>
@@ -0,0 +1,45 @@
1
+ ---
2
+ /**
3
+ * <Breadcrumbs> — semantic breadcrumb trail. The last crumb is the current
4
+ * page (aria-current, not a link). No own container — inherits parent width.
5
+ */
6
+ import { withBase } from "@/lib/basePath";
7
+
8
+ interface Props {
9
+ crumbs: { label: string; href: string }[];
10
+ }
11
+
12
+ const { crumbs } = Astro.props;
13
+ const { basePath } = Astro.locals;
14
+ ---
15
+
16
+ <nav aria-label="Breadcrumb" class="text-sm text-muted">
17
+ <ol class="flex flex-wrap items-center gap-x-1 gap-y-0.5">
18
+ {
19
+ crumbs.map((crumb, i) => {
20
+ const isLast = i === crumbs.length - 1;
21
+ return (
22
+ <li class="flex items-center gap-x-1">
23
+ {i > 0 && (
24
+ <span aria-hidden="true" class="select-none text-border">
25
+ /
26
+ </span>
27
+ )}
28
+ {isLast ? (
29
+ <span aria-current="page" class="text-text">
30
+ {crumb.label}
31
+ </span>
32
+ ) : (
33
+ <a
34
+ href={withBase(basePath, crumb.href)}
35
+ class="transition-colors hover:text-primary"
36
+ >
37
+ {crumb.label}
38
+ </a>
39
+ )}
40
+ </li>
41
+ );
42
+ })
43
+ }
44
+ </ol>
45
+ </nav>
@@ -0,0 +1,56 @@
1
+ ---
2
+ /**
3
+ * <CollectionCard> — editorial collection tile with a dark gradient scrim so
4
+ * the overlaid title reads on any image. Theme-agnostic (CSS-var-backed
5
+ * utilities only).
6
+ */
7
+ import type { CatalogCollection } from "@tot/public-runtime";
8
+ import Img from "@/components/Img.astro";
9
+ import { withBase } from "@/lib/basePath";
10
+
11
+ interface Props {
12
+ collection: CatalogCollection;
13
+ productCount?: number;
14
+ }
15
+
16
+ const { collection, productCount } = Astro.props;
17
+ const { basePath } = Astro.locals;
18
+ const image = collection.image;
19
+ ---
20
+
21
+ <a
22
+ href={withBase(basePath, `/collections/${collection.handle}`)}
23
+ class="group relative block aspect-[3/2] overflow-hidden rounded-md bg-border"
24
+ data-reveal
25
+ >
26
+ <Img
27
+ src={image?.url}
28
+ rehosted={image?.rehosted}
29
+ seed={image?.id ?? collection.handle}
30
+ label={collection.title}
31
+ alt={image?.alt ?? collection.title}
32
+ width={image?.width ?? 800}
33
+ height={image?.height ?? 600}
34
+ sizes="(min-width:1024px) 33vw, (min-width:640px) 50vw, 100vw"
35
+ class="h-full w-full object-cover transition-transform duration-500 motion-safe:group-hover:scale-[1.03]"
36
+ />
37
+ <!-- Scrim: deep enough at the foot that white text passes AA over any image
38
+ (incl. the lighter placeholder pairs). -->
39
+ <div
40
+ class="absolute inset-0 bg-gradient-to-t from-black/75 via-black/20 to-transparent"
41
+ aria-hidden="true"
42
+ >
43
+ </div>
44
+ <div class="absolute inset-x-0 bottom-0 p-4 sm:p-5">
45
+ <h3 class="font-display text-xl font-semibold text-white drop-shadow-sm sm:text-2xl">
46
+ {collection.title}
47
+ </h3>
48
+ {
49
+ productCount != null && (
50
+ <p class="mt-0.5 font-mono text-[0.68rem] uppercase tracking-[0.12em] text-white/80">
51
+ {productCount} {productCount === 1 ? "piece" : "pieces"}
52
+ </p>
53
+ )
54
+ }
55
+ </div>
56
+ </a>