efficient-storefront-blocks 0.57.0 → 0.58.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 (1111) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/dist/chunks/{EditorPreviewCheckout-7Y7WL5BM.js → EditorPreviewCheckout-4NGRJA3T.js} +1 -1
  3. package/dist/chunks/{SiteEditorProvider-5MWJMLLI.js → SiteEditorProvider-V5FE5UD6.js} +1 -1
  4. package/dist/chunks/{chunk-BPVXTVPM.js → chunk-4AKN63X5.js} +1 -1
  5. package/dist/chunks/chunk-K4AZ5SUU.js +1 -0
  6. package/dist/chunks/chunk-PHH7L5FJ.js +255 -0
  7. package/dist/chunks/{chunk-FSSCNRRE.js → chunk-SJQTKWV3.js} +1 -1
  8. package/dist/chunks/{fitment-data-UVMEA7WW.js → fitment-data-OIWU6HGJ.js} +1 -1
  9. package/dist/editor/components/BlogSource.d.ts +11 -0
  10. package/dist/editor/components/BlogSource.js +1 -0
  11. package/dist/editor/components/CategorySource.d.ts +7 -0
  12. package/dist/editor/components/CategorySource.js +1 -0
  13. package/dist/editor/components/CollectionPicker.d.ts +7 -0
  14. package/dist/editor/components/CollectionPicker.js +1 -0
  15. package/dist/editor/components/ConsentBannerLayoutTiles.d.ts +3 -0
  16. package/dist/editor/components/ConsentBannerLayoutTiles.js +1 -0
  17. package/dist/editor/components/CountryShowAsTiles.d.ts +5 -0
  18. package/dist/editor/components/CountryShowAsTiles.js +1 -0
  19. package/dist/editor/components/EditProductLink.d.ts +1 -0
  20. package/dist/editor/components/EditProductLink.js +1 -0
  21. package/dist/editor/components/EditorCurrencyProvider.d.ts +7 -0
  22. package/dist/editor/components/EditorCurrencyProvider.js +1 -0
  23. package/dist/editor/components/EditorPreviewChat.d.ts +1 -0
  24. package/dist/editor/components/EditorPreviewChat.js +11 -0
  25. package/dist/editor/components/EditorPreviewCheckout.d.ts +1 -0
  26. package/dist/editor/components/EditorPreviewCheckout.js +1 -0
  27. package/dist/editor/components/EditorPreviewSurface.d.ts +5 -0
  28. package/dist/editor/components/EditorPreviewSurface.js +1 -0
  29. package/dist/editor/components/FinishPicker.d.ts +6 -0
  30. package/dist/editor/components/FinishPicker.js +1 -0
  31. package/dist/editor/components/FooterColumnsField.d.ts +6 -0
  32. package/dist/editor/components/FooterColumnsField.js +1 -0
  33. package/dist/editor/components/FooterLogoFillField.d.ts +6 -0
  34. package/dist/editor/components/FooterLogoFillField.js +1 -0
  35. package/dist/editor/components/FooterLogoRowFillField.d.ts +6 -0
  36. package/dist/editor/components/FooterLogoRowFillField.js +1 -0
  37. package/dist/editor/components/FormPicker.d.ts +33 -0
  38. package/dist/editor/components/FormPicker.js +1 -0
  39. package/dist/editor/components/GalleryProductBinding.d.ts +7 -0
  40. package/dist/editor/components/GalleryProductBinding.js +1 -0
  41. package/dist/editor/components/GalleryShowcaseSource.d.ts +7 -0
  42. package/dist/editor/components/GalleryShowcaseSource.js +1 -0
  43. package/dist/editor/components/HeaderSection.d.ts +3 -0
  44. package/dist/editor/components/HeaderSection.js +1 -0
  45. package/dist/editor/components/HeaderSettingsPanel.d.ts +132 -0
  46. package/dist/editor/components/HeaderSettingsPanel.js +1 -0
  47. package/dist/editor/components/MenuLinkRangeField.d.ts +5 -0
  48. package/dist/editor/components/MenuLinkRangeField.js +1 -0
  49. package/dist/editor/components/MenuPicker.d.ts +7 -0
  50. package/dist/editor/components/MenuPicker.js +1 -0
  51. package/dist/editor/components/PerViewStepperField.d.ts +13 -0
  52. package/dist/editor/components/PerViewStepperField.js +1 -0
  53. package/dist/editor/components/PreviewHeader.d.ts +2 -0
  54. package/dist/editor/components/PreviewHeader.js +11 -0
  55. package/dist/editor/components/PreviewHeaderPrimer.d.ts +4 -0
  56. package/dist/editor/components/PreviewHeaderPrimer.js +1 -0
  57. package/dist/editor/components/PreviewMenusPrimer.d.ts +3 -0
  58. package/dist/editor/components/PreviewMenusPrimer.js +1 -0
  59. package/dist/editor/components/PreviewProductPicker.d.ts +20 -0
  60. package/dist/editor/components/PreviewProductPicker.js +1 -0
  61. package/dist/editor/components/ProductHeroSource.d.ts +19 -0
  62. package/dist/editor/components/ProductHeroSource.js +1 -0
  63. package/dist/editor/components/ProductPicker.d.ts +6 -0
  64. package/dist/editor/components/ProductPicker.js +1 -0
  65. package/dist/editor/components/SocialLinksDoor.d.ts +1 -0
  66. package/dist/editor/components/SocialLinksDoor.js +1 -0
  67. package/dist/editor/components/SpecTableSource.d.ts +7 -0
  68. package/dist/editor/components/SpecTableSource.js +1 -0
  69. package/dist/editor/components/StorefrontEditorFurniture.d.ts +6 -0
  70. package/dist/editor/components/StorefrontEditorFurniture.js +1 -0
  71. package/dist/editor/components/StorefrontHeaderPanel.d.ts +2 -0
  72. package/dist/editor/components/StorefrontHeaderPanel.js +1 -0
  73. package/dist/editor/components/StorefrontLinkRows.d.ts +5 -0
  74. package/dist/editor/components/StorefrontLinkRows.js +1 -0
  75. package/dist/editor/components/StorefrontShopRows.d.ts +2 -0
  76. package/dist/editor/components/StorefrontShopRows.js +1 -0
  77. package/dist/editor/components/SwitchToBoxHeader.d.ts +1 -0
  78. package/dist/editor/components/SwitchToBoxHeader.js +1 -0
  79. package/dist/editor/components/WheelsPerRowField.d.ts +6 -0
  80. package/dist/editor/components/WheelsPerRowField.js +1 -0
  81. package/dist/editor/components/useCategoryFilter.d.ts +11 -0
  82. package/dist/editor/components/useCategoryFilter.js +1 -0
  83. package/dist/editor/components/useGridSourceLock.d.ts +1 -0
  84. package/dist/editor/components/useGridSourceLock.js +1 -0
  85. package/dist/editor/components/useStorefrontCategories.d.ts +2 -0
  86. package/dist/editor/components/useStorefrontCategories.js +1 -0
  87. package/dist/editor/components/useStorefrontForms.d.ts +2 -0
  88. package/dist/editor/components/useStorefrontForms.js +1 -0
  89. package/dist/editor/components/useWheelMediaGroups.d.ts +2 -0
  90. package/dist/editor/components/useWheelMediaGroups.js +1 -0
  91. package/dist/editor/lib/api/section-templates.d.ts +43 -0
  92. package/dist/editor/lib/api/section-templates.js +1 -0
  93. package/dist/editor/lib/editor-currency-source.d.ts +11 -0
  94. package/dist/editor/lib/editor-currency-source.js +1 -0
  95. package/dist/editor/lib/efficient-backend.d.ts +21 -0
  96. package/dist/editor/lib/efficient-backend.js +1 -0
  97. package/dist/editor/lib/header-settings-source.d.ts +7 -0
  98. package/dist/editor/lib/header-settings-source.js +1 -0
  99. package/dist/editor/lib/puck/blocks/commerce/AccountBlock/fields.d.ts +2 -0
  100. package/dist/editor/lib/puck/blocks/commerce/AccountBlock/fields.js +1 -0
  101. package/dist/editor/lib/puck/blocks/commerce/AlloyWheelsListing/fields.d.ts +2 -0
  102. package/dist/editor/lib/puck/blocks/commerce/AlloyWheelsListing/fields.js +1 -0
  103. package/dist/editor/lib/puck/blocks/commerce/BundleOffer/fields.d.ts +2 -0
  104. package/dist/editor/lib/puck/blocks/commerce/BundleOffer/fields.js +1 -0
  105. package/dist/editor/lib/puck/blocks/commerce/CartBlock/fields.d.ts +2 -0
  106. package/dist/editor/lib/puck/blocks/commerce/CartBlock/fields.js +1 -0
  107. package/dist/editor/lib/puck/blocks/commerce/CatalogFilterBar/fields.d.ts +2 -0
  108. package/dist/editor/lib/puck/blocks/commerce/CatalogFilterBar/fields.js +1 -0
  109. package/dist/editor/lib/puck/blocks/commerce/CheckoutCTA/fields.d.ts +2 -0
  110. package/dist/editor/lib/puck/blocks/commerce/CheckoutCTA/fields.js +1 -0
  111. package/dist/editor/lib/puck/blocks/commerce/CollectionCard/fields.d.ts +2 -0
  112. package/dist/editor/lib/puck/blocks/commerce/CollectionCard/fields.js +1 -0
  113. package/dist/editor/lib/puck/blocks/commerce/Collections/fields.d.ts +2 -0
  114. package/dist/editor/lib/puck/blocks/commerce/Collections/fields.js +1 -0
  115. package/dist/editor/lib/puck/blocks/commerce/CompatibleTyres/fields.d.ts +2 -0
  116. package/dist/editor/lib/puck/blocks/commerce/CompatibleTyres/fields.js +1 -0
  117. package/dist/editor/lib/puck/blocks/commerce/CountrySwitchBlock/fields.d.ts +2 -0
  118. package/dist/editor/lib/puck/blocks/commerce/CountrySwitchBlock/fields.js +1 -0
  119. package/dist/editor/lib/puck/blocks/commerce/DeliveryOptions/fields.d.ts +2 -0
  120. package/dist/editor/lib/puck/blocks/commerce/DeliveryOptions/fields.js +1 -0
  121. package/dist/editor/lib/puck/blocks/commerce/DetailRows/fields.d.ts +2 -0
  122. package/dist/editor/lib/puck/blocks/commerce/DetailRows/fields.js +1 -0
  123. package/dist/editor/lib/puck/blocks/commerce/EuTyreLabel/fields.d.ts +2 -0
  124. package/dist/editor/lib/puck/blocks/commerce/EuTyreLabel/fields.js +1 -0
  125. package/dist/editor/lib/puck/blocks/commerce/FitmentFinder/fields.d.ts +2 -0
  126. package/dist/editor/lib/puck/blocks/commerce/FitmentFinder/fields.js +1 -0
  127. package/dist/editor/lib/puck/blocks/commerce/FitmentTable/fields.d.ts +2 -0
  128. package/dist/editor/lib/puck/blocks/commerce/FitmentTable/fields.js +1 -0
  129. package/dist/editor/lib/puck/blocks/commerce/Gallery/fields.d.ts +2 -0
  130. package/dist/editor/lib/puck/blocks/commerce/Gallery/fields.js +1 -0
  131. package/dist/editor/lib/puck/blocks/commerce/GalleryB/fields.d.ts +2 -0
  132. package/dist/editor/lib/puck/blocks/commerce/GalleryB/fields.js +1 -0
  133. package/dist/editor/lib/puck/blocks/commerce/GalleryItem/fields.d.ts +2 -0
  134. package/dist/editor/lib/puck/blocks/commerce/GalleryItem/fields.js +1 -0
  135. package/dist/editor/lib/puck/blocks/commerce/Header/fields.d.ts +2 -0
  136. package/dist/editor/lib/puck/blocks/commerce/Header/fields.js +1 -0
  137. package/dist/editor/lib/puck/blocks/commerce/ProductGrid/fields.d.ts +2 -0
  138. package/dist/editor/lib/puck/blocks/commerce/ProductGrid/fields.js +1 -0
  139. package/dist/editor/lib/puck/blocks/commerce/ProductHero/fields.d.ts +2 -0
  140. package/dist/editor/lib/puck/blocks/commerce/ProductHero/fields.js +1 -0
  141. package/dist/editor/lib/puck/blocks/commerce/ProductRef/fields.d.ts +2 -0
  142. package/dist/editor/lib/puck/blocks/commerce/ProductRef/fields.js +1 -0
  143. package/dist/editor/lib/puck/blocks/commerce/RelatedProducts/fields.d.ts +2 -0
  144. package/dist/editor/lib/puck/blocks/commerce/RelatedProducts/fields.js +1 -0
  145. package/dist/editor/lib/puck/blocks/commerce/SearchBlock/fields.d.ts +2 -0
  146. package/dist/editor/lib/puck/blocks/commerce/SearchBlock/fields.js +1 -0
  147. package/dist/editor/lib/puck/blocks/commerce/SpecTable/fields.d.ts +2 -0
  148. package/dist/editor/lib/puck/blocks/commerce/SpecTable/fields.js +1 -0
  149. package/dist/editor/lib/puck/blocks/commerce/ThemeSwitchBlock/fields.d.ts +2 -0
  150. package/dist/editor/lib/puck/blocks/commerce/ThemeSwitchBlock/fields.js +1 -0
  151. package/dist/editor/lib/puck/blocks/commerce/TyreFitment/fields.d.ts +2 -0
  152. package/dist/editor/lib/puck/blocks/commerce/TyreFitment/fields.js +1 -0
  153. package/dist/editor/lib/puck/blocks/commerce/TyreSizes/fields.d.ts +2 -0
  154. package/dist/editor/lib/puck/blocks/commerce/TyreSizes/fields.js +1 -0
  155. package/dist/editor/lib/puck/blocks/commerce/controls.d.ts +15 -0
  156. package/dist/editor/lib/puck/blocks/commerce/controls.js +1 -0
  157. package/dist/editor/lib/puck/blocks/commerce/index.d.ts +1 -0
  158. package/dist/editor/lib/puck/blocks/commerce/index.js +1 -0
  159. package/dist/editor/lib/puck/blocks/commerce/page-rules.d.ts +8 -0
  160. package/dist/editor/lib/puck/blocks/commerce/page-rules.js +1 -0
  161. package/dist/editor/lib/puck/blocks/commerce/shared-fields.d.ts +5 -0
  162. package/dist/editor/lib/puck/blocks/commerce/shared-fields.js +1 -0
  163. package/dist/editor/lib/puck/blocks/service/AppointmentBooking/fields.d.ts +2 -0
  164. package/dist/editor/lib/puck/blocks/service/AppointmentBooking/fields.js +1 -0
  165. package/dist/editor/lib/puck/blocks/service/BookingBox/fields.d.ts +2 -0
  166. package/dist/editor/lib/puck/blocks/service/BookingBox/fields.js +1 -0
  167. package/dist/editor/lib/puck/blocks/service/ContactRouting/fields.d.ts +2 -0
  168. package/dist/editor/lib/puck/blocks/service/ContactRouting/fields.js +1 -0
  169. package/dist/editor/lib/puck/blocks/service/Events/fields.d.ts +2 -0
  170. package/dist/editor/lib/puck/blocks/service/Events/fields.js +1 -0
  171. package/dist/editor/lib/puck/blocks/service/Footer/fields.d.ts +2 -0
  172. package/dist/editor/lib/puck/blocks/service/Footer/fields.js +1 -0
  173. package/dist/editor/lib/puck/blocks/service/FooterDivider/fields.d.ts +2 -0
  174. package/dist/editor/lib/puck/blocks/service/FooterDivider/fields.js +1 -0
  175. package/dist/editor/lib/puck/blocks/service/FooterEmailSignup/fields.d.ts +2 -0
  176. package/dist/editor/lib/puck/blocks/service/FooterEmailSignup/fields.js +1 -0
  177. package/dist/editor/lib/puck/blocks/service/FooterGroup/fields.d.ts +2 -0
  178. package/dist/editor/lib/puck/blocks/service/FooterGroup/fields.js +1 -0
  179. package/dist/editor/lib/puck/blocks/service/FooterLogo/fields.d.ts +2 -0
  180. package/dist/editor/lib/puck/blocks/service/FooterLogo/fields.js +1 -0
  181. package/dist/editor/lib/puck/blocks/service/FooterMenu/fields.d.ts +2 -0
  182. package/dist/editor/lib/puck/blocks/service/FooterMenu/fields.js +1 -0
  183. package/dist/editor/lib/puck/blocks/service/FooterSocial/fields.d.ts +2 -0
  184. package/dist/editor/lib/puck/blocks/service/FooterSocial/fields.js +1 -0
  185. package/dist/editor/lib/puck/blocks/service/FooterText/fields.d.ts +2 -0
  186. package/dist/editor/lib/puck/blocks/service/FooterText/fields.js +1 -0
  187. package/dist/editor/lib/puck/blocks/service/Form/fields.d.ts +2 -0
  188. package/dist/editor/lib/puck/blocks/service/Form/fields.js +1 -0
  189. package/dist/editor/lib/puck/blocks/service/LegalLineBlock/fields.d.ts +2 -0
  190. package/dist/editor/lib/puck/blocks/service/LegalLineBlock/fields.js +1 -0
  191. package/dist/editor/lib/puck/blocks/service/ReviewForm/fields.d.ts +2 -0
  192. package/dist/editor/lib/puck/blocks/service/ReviewForm/fields.js +1 -0
  193. package/dist/editor/lib/puck/blocks/service/Reviews/fields.d.ts +2 -0
  194. package/dist/editor/lib/puck/blocks/service/Reviews/fields.js +1 -0
  195. package/dist/editor/lib/puck/blocks/service/SiteFooter/fields.d.ts +2 -0
  196. package/dist/editor/lib/puck/blocks/service/SiteFooter/fields.js +1 -0
  197. package/dist/editor/lib/puck/blocks/service/SiteHeader/fields.d.ts +2 -0
  198. package/dist/editor/lib/puck/blocks/service/SiteHeader/fields.js +1 -0
  199. package/dist/editor/lib/puck/blocks/service/SubmitBox/fields.d.ts +2 -0
  200. package/dist/editor/lib/puck/blocks/service/SubmitBox/fields.js +1 -0
  201. package/dist/editor/lib/puck/blocks/service/SubmitBuild/fields.d.ts +2 -0
  202. package/dist/editor/lib/puck/blocks/service/SubmitBuild/fields.js +1 -0
  203. package/dist/editor/lib/puck/blocks/service/Subscribe/fields.d.ts +2 -0
  204. package/dist/editor/lib/puck/blocks/service/Subscribe/fields.js +1 -0
  205. package/dist/editor/lib/puck/blocks/service/SubscribeBlogs/fields.d.ts +2 -0
  206. package/dist/editor/lib/puck/blocks/service/SubscribeBlogs/fields.js +1 -0
  207. package/dist/editor/lib/puck/blocks/service/controls.d.ts +2 -0
  208. package/dist/editor/lib/puck/blocks/service/controls.js +1 -0
  209. package/dist/editor/lib/puck/blocks/service/index.d.ts +1 -0
  210. package/dist/editor/lib/puck/blocks/service/index.js +1 -0
  211. package/dist/editor/lib/puck/blocks/service/placeholders.d.ts +1 -0
  212. package/dist/editor/lib/puck/blocks/service/placeholders.js +1 -0
  213. package/dist/editor/lib/puck/box-header-for-site.d.ts +9 -0
  214. package/dist/editor/lib/puck/box-header-for-site.js +1 -0
  215. package/dist/editor/lib/puck/box-header.d.ts +52 -0
  216. package/dist/editor/lib/puck/box-header.js +1 -0
  217. package/dist/editor/lib/puck/config.d.ts +73 -0
  218. package/dist/editor/lib/puck/config.js +1 -0
  219. package/dist/editor/lib/say-with.d.ts +2 -0
  220. package/dist/editor/lib/say-with.js +1 -0
  221. package/dist/editor/lib/storefront-document.d.ts +2 -0
  222. package/dist/editor/lib/storefront-document.js +1 -0
  223. package/dist/editor/lib/storefront-editor-words.d.ts +693 -0
  224. package/dist/editor/lib/storefront-editor-words.js +1 -0
  225. package/dist/editor/lib/storefront-editor.d.ts +9 -0
  226. package/dist/editor/lib/storefront-editor.js +1 -0
  227. package/dist/editor/lib/storefront-shop-surfaces.d.ts +8 -0
  228. package/dist/editor/lib/storefront-shop-surfaces.js +1 -0
  229. package/dist/editor/lib/use-prime-and-refresh.d.ts +16 -0
  230. package/dist/editor/lib/use-prime-and-refresh.js +1 -0
  231. package/dist/editor.css +1 -1
  232. package/dist/index.d.ts +14 -0
  233. package/dist/index.js +69 -52
  234. package/dist/render/app/fonts/archivo/OFL.txt +93 -0
  235. package/dist/render/app/fonts/archivo/latin-ext-italic.woff2 +0 -0
  236. package/dist/render/app/fonts/archivo/latin-ext.woff2 +0 -0
  237. package/dist/render/app/fonts/archivo/latin-italic.woff2 +0 -0
  238. package/dist/render/app/fonts/archivo/latin.woff2 +0 -0
  239. package/dist/render/app/fonts/archivo/vietnamese-italic.woff2 +0 -0
  240. package/dist/render/app/fonts/archivo/vietnamese.woff2 +0 -0
  241. package/dist/render/app/fonts/assistant/OFL.txt +95 -0
  242. package/dist/render/app/fonts/assistant/hebrew.woff2 +0 -0
  243. package/dist/render/app/fonts/assistant/latin-ext.woff2 +0 -0
  244. package/dist/render/app/fonts/assistant/latin.woff2 +0 -0
  245. package/dist/render/app/fonts/big-shoulders/OFL.txt +93 -0
  246. package/dist/render/app/fonts/big-shoulders/latin-ext.woff2 +0 -0
  247. package/dist/render/app/fonts/big-shoulders/latin.woff2 +0 -0
  248. package/dist/render/app/fonts/big-shoulders/vietnamese.woff2 +0 -0
  249. package/dist/render/app/fonts/cormorant/OFL.txt +93 -0
  250. package/dist/render/app/fonts/cormorant/cyrillic-ext-italic.woff2 +0 -0
  251. package/dist/render/app/fonts/cormorant/cyrillic-ext.woff2 +0 -0
  252. package/dist/render/app/fonts/cormorant/cyrillic-italic.woff2 +0 -0
  253. package/dist/render/app/fonts/cormorant/cyrillic.woff2 +0 -0
  254. package/dist/render/app/fonts/cormorant/latin-ext-italic.woff2 +0 -0
  255. package/dist/render/app/fonts/cormorant/latin-ext.woff2 +0 -0
  256. package/dist/render/app/fonts/cormorant/latin-italic.woff2 +0 -0
  257. package/dist/render/app/fonts/cormorant/latin.woff2 +0 -0
  258. package/dist/render/app/fonts/cormorant/vietnamese-italic.woff2 +0 -0
  259. package/dist/render/app/fonts/cormorant/vietnamese.woff2 +0 -0
  260. package/dist/render/app/fonts/dm-sans/OFL.txt +93 -0
  261. package/dist/render/app/fonts/dm-sans/latin-ext-italic.woff2 +0 -0
  262. package/dist/render/app/fonts/dm-sans/latin-ext.woff2 +0 -0
  263. package/dist/render/app/fonts/dm-sans/latin-italic.woff2 +0 -0
  264. package/dist/render/app/fonts/dm-sans/latin.woff2 +0 -0
  265. package/dist/render/app/fonts/fallbacks.css +133 -0
  266. package/dist/render/app/fonts/fraunces/OFL.txt +93 -0
  267. package/dist/render/app/fonts/fraunces/latin-ext-italic.woff2 +0 -0
  268. package/dist/render/app/fonts/fraunces/latin-ext.woff2 +0 -0
  269. package/dist/render/app/fonts/fraunces/latin-italic.woff2 +0 -0
  270. package/dist/render/app/fonts/fraunces/latin.woff2 +0 -0
  271. package/dist/render/app/fonts/fraunces/vietnamese-italic.woff2 +0 -0
  272. package/dist/render/app/fonts/fraunces/vietnamese.woff2 +0 -0
  273. package/dist/render/app/fonts/inter/OFL.txt +93 -0
  274. package/dist/render/app/fonts/inter/cyrillic-ext-italic.woff2 +0 -0
  275. package/dist/render/app/fonts/inter/cyrillic-ext.woff2 +0 -0
  276. package/dist/render/app/fonts/inter/cyrillic-italic.woff2 +0 -0
  277. package/dist/render/app/fonts/inter/cyrillic.woff2 +0 -0
  278. package/dist/render/app/fonts/inter/greek-ext-italic.woff2 +0 -0
  279. package/dist/render/app/fonts/inter/greek-ext.woff2 +0 -0
  280. package/dist/render/app/fonts/inter/greek-italic.woff2 +0 -0
  281. package/dist/render/app/fonts/inter/greek.woff2 +0 -0
  282. package/dist/render/app/fonts/inter/latin-ext-italic.woff2 +0 -0
  283. package/dist/render/app/fonts/inter/latin-ext.woff2 +0 -0
  284. package/dist/render/app/fonts/inter/latin-italic.woff2 +0 -0
  285. package/dist/render/app/fonts/inter/latin.woff2 +0 -0
  286. package/dist/render/app/fonts/inter/vietnamese-italic.woff2 +0 -0
  287. package/dist/render/app/fonts/inter/vietnamese.woff2 +0 -0
  288. package/dist/render/app/fonts/jetbrains-mono/OFL.txt +93 -0
  289. package/dist/render/app/fonts/jetbrains-mono/cyrillic-ext-italic.woff2 +0 -0
  290. package/dist/render/app/fonts/jetbrains-mono/cyrillic-ext.woff2 +0 -0
  291. package/dist/render/app/fonts/jetbrains-mono/cyrillic-italic.woff2 +0 -0
  292. package/dist/render/app/fonts/jetbrains-mono/cyrillic.woff2 +0 -0
  293. package/dist/render/app/fonts/jetbrains-mono/greek-italic.woff2 +0 -0
  294. package/dist/render/app/fonts/jetbrains-mono/greek.woff2 +0 -0
  295. package/dist/render/app/fonts/jetbrains-mono/latin-ext-italic.woff2 +0 -0
  296. package/dist/render/app/fonts/jetbrains-mono/latin-ext.woff2 +0 -0
  297. package/dist/render/app/fonts/jetbrains-mono/latin-italic.woff2 +0 -0
  298. package/dist/render/app/fonts/jetbrains-mono/latin.woff2 +0 -0
  299. package/dist/render/app/fonts/jetbrains-mono/vietnamese-italic.woff2 +0 -0
  300. package/dist/render/app/fonts/jetbrains-mono/vietnamese.woff2 +0 -0
  301. package/dist/render/app/fonts/manrope/OFL.txt +93 -0
  302. package/dist/render/app/fonts/manrope/cyrillic-ext.woff2 +0 -0
  303. package/dist/render/app/fonts/manrope/cyrillic.woff2 +0 -0
  304. package/dist/render/app/fonts/manrope/greek.woff2 +0 -0
  305. package/dist/render/app/fonts/manrope/latin-ext.woff2 +0 -0
  306. package/dist/render/app/fonts/manrope/latin.woff2 +0 -0
  307. package/dist/render/app/fonts/manrope/vietnamese.woff2 +0 -0
  308. package/dist/render/app/fonts/michroma/OFL.txt +93 -0
  309. package/dist/render/app/fonts/michroma/latin-ext.woff2 +0 -0
  310. package/dist/render/app/fonts/michroma/latin.woff2 +0 -0
  311. package/dist/render/app/fonts/oswald/OFL.txt +93 -0
  312. package/dist/render/app/fonts/oswald/cyrillic-ext.woff2 +0 -0
  313. package/dist/render/app/fonts/oswald/cyrillic.woff2 +0 -0
  314. package/dist/render/app/fonts/oswald/latin-ext.woff2 +0 -0
  315. package/dist/render/app/fonts/oswald/latin.woff2 +0 -0
  316. package/dist/render/app/fonts/oswald/vietnamese.woff2 +0 -0
  317. package/dist/render/app/fonts/outfit/OFL.txt +93 -0
  318. package/dist/render/app/fonts/outfit/latin-ext.woff2 +0 -0
  319. package/dist/render/app/fonts/outfit/latin.woff2 +0 -0
  320. package/dist/render/app/fonts/playfair-display/OFL.txt +93 -0
  321. package/dist/render/app/fonts/playfair-display/cyrillic-italic.woff2 +0 -0
  322. package/dist/render/app/fonts/playfair-display/cyrillic.woff2 +0 -0
  323. package/dist/render/app/fonts/playfair-display/latin-ext-italic.woff2 +0 -0
  324. package/dist/render/app/fonts/playfair-display/latin-ext.woff2 +0 -0
  325. package/dist/render/app/fonts/playfair-display/latin-italic.woff2 +0 -0
  326. package/dist/render/app/fonts/playfair-display/latin.woff2 +0 -0
  327. package/dist/render/app/fonts/playfair-display/vietnamese-italic.woff2 +0 -0
  328. package/dist/render/app/fonts/playfair-display/vietnamese.woff2 +0 -0
  329. package/dist/render/app/fonts/poppins/OFL.txt +93 -0
  330. package/dist/render/app/fonts/poppins/devanagari-400.woff2 +0 -0
  331. package/dist/render/app/fonts/poppins/devanagari-500.woff2 +0 -0
  332. package/dist/render/app/fonts/poppins/devanagari-600.woff2 +0 -0
  333. package/dist/render/app/fonts/poppins/devanagari-italic-400.woff2 +0 -0
  334. package/dist/render/app/fonts/poppins/devanagari-italic-500.woff2 +0 -0
  335. package/dist/render/app/fonts/poppins/devanagari-italic-600.woff2 +0 -0
  336. package/dist/render/app/fonts/poppins/latin-400.woff2 +0 -0
  337. package/dist/render/app/fonts/poppins/latin-500.woff2 +0 -0
  338. package/dist/render/app/fonts/poppins/latin-600.woff2 +0 -0
  339. package/dist/render/app/fonts/poppins/latin-ext-400.woff2 +0 -0
  340. package/dist/render/app/fonts/poppins/latin-ext-500.woff2 +0 -0
  341. package/dist/render/app/fonts/poppins/latin-ext-600.woff2 +0 -0
  342. package/dist/render/app/fonts/poppins/latin-ext-italic-400.woff2 +0 -0
  343. package/dist/render/app/fonts/poppins/latin-ext-italic-500.woff2 +0 -0
  344. package/dist/render/app/fonts/poppins/latin-ext-italic-600.woff2 +0 -0
  345. package/dist/render/app/fonts/poppins/latin-italic-400.woff2 +0 -0
  346. package/dist/render/app/fonts/poppins/latin-italic-500.woff2 +0 -0
  347. package/dist/render/app/fonts/poppins/latin-italic-600.woff2 +0 -0
  348. package/dist/render/app/fonts/sora/OFL.txt +93 -0
  349. package/dist/render/app/fonts/sora/latin-ext.woff2 +0 -0
  350. package/dist/render/app/fonts/sora/latin.woff2 +0 -0
  351. package/dist/render/app/fonts/space-grotesk/OFL.txt +93 -0
  352. package/dist/render/app/fonts/space-grotesk/latin-ext.woff2 +0 -0
  353. package/dist/render/app/fonts/space-grotesk/latin.woff2 +0 -0
  354. package/dist/render/app/fonts/space-grotesk/vietnamese.woff2 +0 -0
  355. package/dist/render/app/globals.css +3264 -0
  356. package/dist/render/built-with.json +3 -0
  357. package/dist/render/components/AppointmentBooking.d.ts +14 -0
  358. package/dist/render/components/AppointmentBooking.js +1 -0
  359. package/dist/render/components/AssistantChatMount.d.ts +8 -0
  360. package/dist/render/components/AssistantChatMount.js +1 -0
  361. package/dist/render/components/AssistantChatPanel.d.ts +7 -0
  362. package/dist/render/components/AssistantChatPanel.js +1 -0
  363. package/dist/render/components/BoxHeaderBar.d.ts +10 -0
  364. package/dist/render/components/BoxHeaderBar.js +1 -0
  365. package/dist/render/components/BrandPlate.d.ts +4 -0
  366. package/dist/render/components/BrandPlate.js +1 -0
  367. package/dist/render/components/BundleCard.d.ts +6 -0
  368. package/dist/render/components/BundleCard.js +1 -0
  369. package/dist/render/components/CartContext.d.ts +91 -0
  370. package/dist/render/components/CartContext.js +1 -0
  371. package/dist/render/components/CartDrawer.d.ts +12 -0
  372. package/dist/render/components/CartDrawer.js +4 -0
  373. package/dist/render/components/CartLineThumbnail.d.ts +8 -0
  374. package/dist/render/components/CartLineThumbnail.js +1 -0
  375. package/dist/render/components/CatalogFilterBarSection.d.ts +12 -0
  376. package/dist/render/components/CatalogFilterBarSection.js +1 -0
  377. package/dist/render/components/ChatContext.d.ts +23 -0
  378. package/dist/render/components/ChatContext.js +1 -0
  379. package/dist/render/components/ChatLauncher.d.ts +18 -0
  380. package/dist/render/components/ChatLauncher.js +1 -0
  381. package/dist/render/components/ChatWidget.d.ts +12 -0
  382. package/dist/render/components/ChatWidget.js +1 -0
  383. package/dist/render/components/CheckoutCTA.d.ts +12 -0
  384. package/dist/render/components/CheckoutCTA.js +1 -0
  385. package/dist/render/components/CollectionCard.d.ts +27 -0
  386. package/dist/render/components/CollectionCard.js +1 -0
  387. package/dist/render/components/Collections.d.ts +17 -0
  388. package/dist/render/components/Collections.js +1 -0
  389. package/dist/render/components/CompatibleTyres.d.ts +23 -0
  390. package/dist/render/components/CompatibleTyres.js +1 -0
  391. package/dist/render/components/ConsentBanner.d.ts +20 -0
  392. package/dist/render/components/ConsentBanner.js +1 -0
  393. package/dist/render/components/ConsentContext.d.ts +22 -0
  394. package/dist/render/components/ConsentContext.js +1 -0
  395. package/dist/render/components/ConsentPanel.d.ts +15 -0
  396. package/dist/render/components/ConsentPanel.js +1 -0
  397. package/dist/render/components/ConsentProvider.d.ts +7 -0
  398. package/dist/render/components/ConsentProvider.js +1 -0
  399. package/dist/render/components/ContactRouting.d.ts +33 -0
  400. package/dist/render/components/ContactRouting.js +1 -0
  401. package/dist/render/components/CountrySelect.d.ts +7 -0
  402. package/dist/render/components/CountrySelect.js +1 -0
  403. package/dist/render/components/CouponField.d.ts +3 -0
  404. package/dist/render/components/CouponField.js +1 -0
  405. package/dist/render/components/CurrencyContext.d.ts +27 -0
  406. package/dist/render/components/CurrencyContext.js +1 -0
  407. package/dist/render/components/DeliveryOptions.d.ts +18 -0
  408. package/dist/render/components/DeliveryOptions.js +1 -0
  409. package/dist/render/components/DeliveryOptionsSource.d.ts +8 -0
  410. package/dist/render/components/DeliveryOptionsSource.js +1 -0
  411. package/dist/render/components/DetailRows.d.ts +22 -0
  412. package/dist/render/components/DetailRows.js +1 -0
  413. package/dist/render/components/DimensionArrowMarkers.d.ts +3 -0
  414. package/dist/render/components/DimensionArrowMarkers.js +1 -0
  415. package/dist/render/components/EditorCurrencyNoticeContext.d.ts +7 -0
  416. package/dist/render/components/EditorCurrencyNoticeContext.js +1 -0
  417. package/dist/render/components/EmailCaptureForm.d.ts +15 -0
  418. package/dist/render/components/EmailCaptureForm.js +1 -0
  419. package/dist/render/components/EnglishVariant.d.ts +7 -0
  420. package/dist/render/components/EnglishVariant.js +1 -0
  421. package/dist/render/components/EuTyreLabelCard.d.ts +4 -0
  422. package/dist/render/components/EuTyreLabelCard.js +1 -0
  423. package/dist/render/components/EuTyreLabels.d.ts +7 -0
  424. package/dist/render/components/EuTyreLabels.js +1 -0
  425. package/dist/render/components/Events.d.ts +62 -0
  426. package/dist/render/components/Events.js +1 -0
  427. package/dist/render/components/FinishSwatches.d.ts +14 -0
  428. package/dist/render/components/FinishSwatches.js +1 -0
  429. package/dist/render/components/FitmentTable.d.ts +17 -0
  430. package/dist/render/components/FitmentTable.js +1 -0
  431. package/dist/render/components/FooterDivider.d.ts +6 -0
  432. package/dist/render/components/FooterDivider.js +1 -0
  433. package/dist/render/components/FooterEmailSignup.d.ts +6 -0
  434. package/dist/render/components/FooterEmailSignup.js +1 -0
  435. package/dist/render/components/FooterGroup.d.ts +8 -0
  436. package/dist/render/components/FooterGroup.js +1 -0
  437. package/dist/render/components/FooterLogo.d.ts +13 -0
  438. package/dist/render/components/FooterLogo.js +1 -0
  439. package/dist/render/components/FooterMenu.d.ts +25 -0
  440. package/dist/render/components/FooterMenu.js +1 -0
  441. package/dist/render/components/FooterSection.d.ts +11 -0
  442. package/dist/render/components/FooterSection.js +1 -0
  443. package/dist/render/components/FooterSocial.d.ts +16 -0
  444. package/dist/render/components/FooterSocial.js +1 -0
  445. package/dist/render/components/FooterText.d.ts +7 -0
  446. package/dist/render/components/FooterText.js +1 -0
  447. package/dist/render/components/FormBlock.d.ts +19 -0
  448. package/dist/render/components/FormBlock.js +1 -0
  449. package/dist/render/components/GalleryCard.d.ts +28 -0
  450. package/dist/render/components/GalleryCard.js +1 -0
  451. package/dist/render/components/GalleryGrid.d.ts +20 -0
  452. package/dist/render/components/GalleryGrid.js +1 -0
  453. package/dist/render/components/GalleryItem.d.ts +41 -0
  454. package/dist/render/components/GalleryItem.js +1 -0
  455. package/dist/render/components/GalleryShowcase.d.ts +51 -0
  456. package/dist/render/components/GalleryShowcase.js +1 -0
  457. package/dist/render/components/Header.d.ts +13 -0
  458. package/dist/render/components/Header.js +1 -0
  459. package/dist/render/components/HeaderNavItem.d.ts +17 -0
  460. package/dist/render/components/HeaderNavItem.js +1 -0
  461. package/dist/render/components/HeaderToolBlocks.d.ts +18 -0
  462. package/dist/render/components/HeaderToolBlocks.js +1 -0
  463. package/dist/render/components/HeaderTools.d.ts +21 -0
  464. package/dist/render/components/HeaderTools.js +1 -0
  465. package/dist/render/components/LegalLine.d.ts +4 -0
  466. package/dist/render/components/LegalLine.js +1 -0
  467. package/dist/render/components/Lightbox.d.ts +19 -0
  468. package/dist/render/components/Lightbox.js +1 -0
  469. package/dist/render/components/LinkedProductHero.d.ts +34 -0
  470. package/dist/render/components/LinkedProductHero.js +1 -0
  471. package/dist/render/components/LiveSiteMapsProvider.d.ts +6 -0
  472. package/dist/render/components/LiveSiteMapsProvider.js +1 -0
  473. package/dist/render/components/MenuPlaceholderLabel.d.ts +1 -0
  474. package/dist/render/components/MenuPlaceholderLabel.js +1 -0
  475. package/dist/render/components/NoCurrencyNote.d.ts +4 -0
  476. package/dist/render/components/NoCurrencyNote.js +1 -0
  477. package/dist/render/components/PreOrderPanel.d.ts +5 -0
  478. package/dist/render/components/PreOrderPanel.js +1 -0
  479. package/dist/render/components/PreviewMenusContext.d.ts +13 -0
  480. package/dist/render/components/PreviewMenusContext.js +1 -0
  481. package/dist/render/components/ProductCard.d.ts +6 -0
  482. package/dist/render/components/ProductCard.js +1 -0
  483. package/dist/render/components/ProductCardChrome.d.ts +37 -0
  484. package/dist/render/components/ProductCardChrome.js +1 -0
  485. package/dist/render/components/ProductCardEyebrow.d.ts +14 -0
  486. package/dist/render/components/ProductCardEyebrow.js +1 -0
  487. package/dist/render/components/ProductGridSection.d.ts +65 -0
  488. package/dist/render/components/ProductGridSection.js +1 -0
  489. package/dist/render/components/ProductHeroPanel.d.ts +22 -0
  490. package/dist/render/components/ProductHeroPanel.js +1 -0
  491. package/dist/render/components/ProductLightbox.d.ts +5 -0
  492. package/dist/render/components/ProductLightbox.js +1 -0
  493. package/dist/render/components/ProductRef.d.ts +16 -0
  494. package/dist/render/components/ProductRef.js +1 -0
  495. package/dist/render/components/PublishedFooter.d.ts +16 -0
  496. package/dist/render/components/PublishedFooter.js +1 -0
  497. package/dist/render/components/PublishedHeader.d.ts +4 -0
  498. package/dist/render/components/PublishedHeader.js +1 -0
  499. package/dist/render/components/QuantityStepper.d.ts +7 -0
  500. package/dist/render/components/QuantityStepper.js +1 -0
  501. package/dist/render/components/QuoteLink.d.ts +4 -0
  502. package/dist/render/components/QuoteLink.js +1 -0
  503. package/dist/render/components/ReviewForm.d.ts +8 -0
  504. package/dist/render/components/ReviewForm.js +1 -0
  505. package/dist/render/components/Reviews.d.ts +12 -0
  506. package/dist/render/components/Reviews.js +1 -0
  507. package/dist/render/components/SearchContext.d.ts +13 -0
  508. package/dist/render/components/SearchContext.js +1 -0
  509. package/dist/render/components/SectionBand.d.ts +9 -0
  510. package/dist/render/components/SectionBand.js +1 -0
  511. package/dist/render/components/SectionFootnote.d.ts +5 -0
  512. package/dist/render/components/SectionFootnote.js +1 -0
  513. package/dist/render/components/SectionGroupBody.d.ts +9 -0
  514. package/dist/render/components/SectionGroupBody.js +1 -0
  515. package/dist/render/components/SectionNotice.d.ts +7 -0
  516. package/dist/render/components/SectionNotice.js +1 -0
  517. package/dist/render/components/SiteRecordContext.d.ts +17 -0
  518. package/dist/render/components/SiteRecordContext.js +1 -0
  519. package/dist/render/components/SiteSwitch.d.ts +10 -0
  520. package/dist/render/components/SiteSwitch.js +1 -0
  521. package/dist/render/components/SpecTable.d.ts +18 -0
  522. package/dist/render/components/SpecTable.js +1 -0
  523. package/dist/render/components/SpecTablePartBridge.d.ts +5 -0
  524. package/dist/render/components/SpecTablePartBridge.js +1 -0
  525. package/dist/render/components/StorefrontProviders.d.ts +15 -0
  526. package/dist/render/components/StorefrontProviders.js +1 -0
  527. package/dist/render/components/SubmitBuild.d.ts +23 -0
  528. package/dist/render/components/SubmitBuild.js +1 -0
  529. package/dist/render/components/Subscribe.d.ts +27 -0
  530. package/dist/render/components/Subscribe.js +1 -0
  531. package/dist/render/components/SubscribePopup.d.ts +19 -0
  532. package/dist/render/components/SubscribePopup.js +7 -0
  533. package/dist/render/components/SubscribePopupContext.d.ts +33 -0
  534. package/dist/render/components/SubscribePopupContext.js +1 -0
  535. package/dist/render/components/ThemeToggle.d.ts +1 -0
  536. package/dist/render/components/ThemeToggle.js +1 -0
  537. package/dist/render/components/ThemedSelect.d.ts +59 -0
  538. package/dist/render/components/ThemedSelect.js +1 -0
  539. package/dist/render/components/TyreCard.d.ts +19 -0
  540. package/dist/render/components/TyreCard.js +1 -0
  541. package/dist/render/components/TyreFitVerdicts.d.ts +7 -0
  542. package/dist/render/components/TyreFitVerdicts.js +1 -0
  543. package/dist/render/components/TyreImage.d.ts +10 -0
  544. package/dist/render/components/TyreImage.js +1 -0
  545. package/dist/render/components/TyrePlpCard.d.ts +19 -0
  546. package/dist/render/components/TyrePlpCard.js +1 -0
  547. package/dist/render/components/TyreRenderSvg.d.ts +7 -0
  548. package/dist/render/components/TyreRenderSvg.js +1 -0
  549. package/dist/render/components/TyreSummaryCard.d.ts +11 -0
  550. package/dist/render/components/TyreSummaryCard.js +1 -0
  551. package/dist/render/components/TyreVehicleFitment.d.ts +18 -0
  552. package/dist/render/components/TyreVehicleFitment.js +1 -0
  553. package/dist/render/components/ViewMoreLink.d.ts +4 -0
  554. package/dist/render/components/ViewMoreLink.js +1 -0
  555. package/dist/render/components/WebFormField.d.ts +17 -0
  556. package/dist/render/components/WebFormField.js +1 -0
  557. package/dist/render/components/WheelCardFrame.d.ts +67 -0
  558. package/dist/render/components/WheelCardFrame.js +1 -0
  559. package/dist/render/components/WheelImage.d.ts +20 -0
  560. package/dist/render/components/WheelImage.js +1 -0
  561. package/dist/render/components/WheelPlaceholder.d.ts +7 -0
  562. package/dist/render/components/WheelPlaceholder.js +1 -0
  563. package/dist/render/components/WheelSvg.d.ts +7 -0
  564. package/dist/render/components/WheelSvg.js +1 -0
  565. package/dist/render/components/WishlistContext.d.ts +13 -0
  566. package/dist/render/components/WishlistContext.js +1 -0
  567. package/dist/render/components/booking/BookingBox.d.ts +16 -0
  568. package/dist/render/components/booking/BookingBox.js +1 -0
  569. package/dist/render/components/booking/BookingFormState.d.ts +46 -0
  570. package/dist/render/components/booking/BookingFormState.js +1 -0
  571. package/dist/render/components/booking/BookingRegions.d.ts +16 -0
  572. package/dist/render/components/booking/BookingRegions.js +1 -0
  573. package/dist/render/components/buy-row.d.ts +1 -0
  574. package/dist/render/components/buy-row.js +1 -0
  575. package/dist/render/components/catalog/CatalogFilterBar.d.ts +55 -0
  576. package/dist/render/components/catalog/CatalogFilterBar.js +1 -0
  577. package/dist/render/components/catalog/CatalogFilterContext.d.ts +39 -0
  578. package/dist/render/components/catalog/CatalogFilterContext.js +1 -0
  579. package/dist/render/components/catalog/CatalogFilterLabels.d.ts +6 -0
  580. package/dist/render/components/catalog/CatalogFilterLabels.js +1 -0
  581. package/dist/render/components/catalog/CatalogVehiclePicker.d.ts +22 -0
  582. package/dist/render/components/catalog/CatalogVehiclePicker.js +1 -0
  583. package/dist/render/components/catalog/FilterPill.d.ts +25 -0
  584. package/dist/render/components/catalog/FilterPill.js +1 -0
  585. package/dist/render/components/catalog/SiteMenuLinks.d.ts +6 -0
  586. package/dist/render/components/catalog/SiteMenuLinks.js +1 -0
  587. package/dist/render/components/checkout/CheckoutPageClient.d.ts +25 -0
  588. package/dist/render/components/checkout/CheckoutPageClient.js +3 -0
  589. package/dist/render/components/fitment/FitmentFinder.d.ts +15 -0
  590. package/dist/render/components/fitment/FitmentFinder.js +1 -0
  591. package/dist/render/components/fitment/FitmentPickerGrid.d.ts +6 -0
  592. package/dist/render/components/fitment/FitmentPickerGrid.js +1 -0
  593. package/dist/render/components/fitment/FitmentRowFit.d.ts +4 -0
  594. package/dist/render/components/fitment/FitmentRowFit.js +1 -0
  595. package/dist/render/components/fitment/PickerField.d.ts +16 -0
  596. package/dist/render/components/fitment/PickerField.js +1 -0
  597. package/dist/render/components/fitment/StockFitmentCard.d.ts +8 -0
  598. package/dist/render/components/fitment/StockFitmentCard.js +1 -0
  599. package/dist/render/components/fitment/useFitmentCascade.d.ts +31 -0
  600. package/dist/render/components/fitment/useFitmentCascade.js +1 -0
  601. package/dist/render/components/fitment/useVehicleFit.d.ts +29 -0
  602. package/dist/render/components/fitment/useVehicleFit.js +1 -0
  603. package/dist/render/components/fitment/vehicle-control.d.ts +10 -0
  604. package/dist/render/components/fitment/vehicle-control.js +0 -0
  605. package/dist/render/components/fitment/vehicle-level-state.d.ts +14 -0
  606. package/dist/render/components/fitment/vehicle-level-state.js +1 -0
  607. package/dist/render/components/formSubmission.d.ts +32 -0
  608. package/dist/render/components/formSubmission.js +1 -0
  609. package/dist/render/components/pdp-dynamic/DynamicBundleOffer.d.ts +13 -0
  610. package/dist/render/components/pdp-dynamic/DynamicBundleOffer.js +1 -0
  611. package/dist/render/components/pdp-dynamic/DynamicDetailRows.d.ts +14 -0
  612. package/dist/render/components/pdp-dynamic/DynamicDetailRows.js +1 -0
  613. package/dist/render/components/pdp-dynamic/DynamicEuTyreLabel.d.ts +8 -0
  614. package/dist/render/components/pdp-dynamic/DynamicEuTyreLabel.js +1 -0
  615. package/dist/render/components/pdp-dynamic/DynamicFitmentTable.d.ts +16 -0
  616. package/dist/render/components/pdp-dynamic/DynamicFitmentTable.js +1 -0
  617. package/dist/render/components/pdp-dynamic/DynamicGalleryGrid.d.ts +18 -0
  618. package/dist/render/components/pdp-dynamic/DynamicGalleryGrid.js +1 -0
  619. package/dist/render/components/pdp-dynamic/DynamicProductHero.d.ts +25 -0
  620. package/dist/render/components/pdp-dynamic/DynamicProductHero.js +1 -0
  621. package/dist/render/components/pdp-dynamic/DynamicRelatedProducts.d.ts +31 -0
  622. package/dist/render/components/pdp-dynamic/DynamicRelatedProducts.js +1 -0
  623. package/dist/render/components/pdp-dynamic/DynamicSpecTable.d.ts +13 -0
  624. package/dist/render/components/pdp-dynamic/DynamicSpecTable.js +1 -0
  625. package/dist/render/components/pdp-dynamic/DynamicTyreFitment.d.ts +13 -0
  626. package/dist/render/components/pdp-dynamic/DynamicTyreFitment.js +1 -0
  627. package/dist/render/components/pdp-dynamic/DynamicTyreSizes.d.ts +11 -0
  628. package/dist/render/components/pdp-dynamic/DynamicTyreSizes.js +1 -0
  629. package/dist/render/components/pdp-dynamic/FitmentTableBlock.d.ts +7 -0
  630. package/dist/render/components/pdp-dynamic/FitmentTableBlock.js +1 -0
  631. package/dist/render/components/pdp-dynamic/KindGate.d.ts +8 -0
  632. package/dist/render/components/pdp-dynamic/KindGate.js +1 -0
  633. package/dist/render/components/pdp-dynamic/useSampleProduct.d.ts +5 -0
  634. package/dist/render/components/pdp-dynamic/useSampleProduct.js +1 -0
  635. package/dist/render/components/product-hero/TyreHeroFitment.d.ts +17 -0
  636. package/dist/render/components/product-hero/TyreHeroFitment.js +1 -0
  637. package/dist/render/components/product-hero/VehicleQuestion.d.ts +18 -0
  638. package/dist/render/components/product-hero/VehicleQuestion.js +1 -0
  639. package/dist/render/components/product-hero/WheelHeroFitment.d.ts +9 -0
  640. package/dist/render/components/product-hero/WheelHeroFitment.js +1 -0
  641. package/dist/render/components/product-hero/hero-content.d.ts +7 -0
  642. package/dist/render/components/product-hero/hero-content.js +1 -0
  643. package/dist/render/components/product-hero/hero-view.d.ts +37 -0
  644. package/dist/render/components/product-hero/hero-view.js +1 -0
  645. package/dist/render/components/product-hero/stage-photo.d.ts +10 -0
  646. package/dist/render/components/product-hero/stage-photo.js +3 -0
  647. package/dist/render/components/submit/SubmitBox.d.ts +19 -0
  648. package/dist/render/components/submit/SubmitBox.js +1 -0
  649. package/dist/render/components/submit/SubmitRegions.d.ts +17 -0
  650. package/dist/render/components/submit/SubmitRegions.js +1 -0
  651. package/dist/render/components/useEuTyreLabels.d.ts +3 -0
  652. package/dist/render/components/useEuTyreLabels.js +1 -0
  653. package/dist/render/components/useFinishSelection.d.ts +17 -0
  654. package/dist/render/components/useFinishSelection.js +1 -0
  655. package/dist/render/components/useHeaderBar.d.ts +9 -0
  656. package/dist/render/components/useHeaderBar.js +1 -0
  657. package/dist/render/components/useLightbox.d.ts +10 -0
  658. package/dist/render/components/useLightbox.js +1 -0
  659. package/dist/render/components/useModalDismiss.d.ts +8 -0
  660. package/dist/render/components/useModalDismiss.js +1 -0
  661. package/dist/render/components/useNewsletterSignup.d.ts +12 -0
  662. package/dist/render/components/useNewsletterSignup.js +1 -0
  663. package/dist/render/components/usePreOrderIntent.d.ts +14 -0
  664. package/dist/render/components/usePreOrderIntent.js +1 -0
  665. package/dist/render/components/useProductCatalog.d.ts +54 -0
  666. package/dist/render/components/useProductCatalog.js +1 -0
  667. package/dist/render/components/useProductSelection.d.ts +39 -0
  668. package/dist/render/components/useProductSelection.js +1 -0
  669. package/dist/render/components/useStockAlertSignup.d.ts +14 -0
  670. package/dist/render/components/useStockAlertSignup.js +1 -0
  671. package/dist/render/components/useStorefrontReviews.d.ts +2 -0
  672. package/dist/render/components/useStorefrontReviews.js +1 -0
  673. package/dist/render/components/useTyreCatalog.d.ts +2 -0
  674. package/dist/render/components/useTyreCatalog.js +1 -0
  675. package/dist/render/components/useTyreDetail.d.ts +6 -0
  676. package/dist/render/components/useTyreDetail.js +1 -0
  677. package/dist/render/components/useTyreFitAnswer.d.ts +5 -0
  678. package/dist/render/components/useTyreFitAnswer.js +1 -0
  679. package/dist/render/components/useWheelDetail.d.ts +6 -0
  680. package/dist/render/components/useWheelDetail.js +1 -0
  681. package/dist/render/components/wheels-plp/AlloyWheelsListing.d.ts +42 -0
  682. package/dist/render/components/wheels-plp/AlloyWheelsListing.js +1 -0
  683. package/dist/render/components/wheels-plp/PlpActiveChips.d.ts +9 -0
  684. package/dist/render/components/wheels-plp/PlpActiveChips.js +1 -0
  685. package/dist/render/components/wheels-plp/PlpCard.d.ts +27 -0
  686. package/dist/render/components/wheels-plp/PlpCard.js +1 -0
  687. package/dist/render/components/wheels-plp/PlpInStockToggle.d.ts +6 -0
  688. package/dist/render/components/wheels-plp/PlpInStockToggle.js +1 -0
  689. package/dist/render/components/wheels-plp/PlpNotifyMeDialog.d.ts +10 -0
  690. package/dist/render/components/wheels-plp/PlpNotifyMeDialog.js +1 -0
  691. package/dist/render/components/wheels-plp/PlpPagination.d.ts +5 -0
  692. package/dist/render/components/wheels-plp/PlpPagination.js +1 -0
  693. package/dist/render/components/wheels-plp/PlpSidebar.d.ts +25 -0
  694. package/dist/render/components/wheels-plp/PlpSidebar.js +1 -0
  695. package/dist/render/components/wheels-plp/PlpTypePicker.d.ts +7 -0
  696. package/dist/render/components/wheels-plp/PlpTypePicker.js +1 -0
  697. package/dist/render/components/wheels-plp/PlpTyreSizeBoxes.d.ts +9 -0
  698. package/dist/render/components/wheels-plp/PlpTyreSizeBoxes.js +1 -0
  699. package/dist/render/components/wheels-plp/PlpVehicleBar.d.ts +15 -0
  700. package/dist/render/components/wheels-plp/PlpVehicleBar.js +1 -0
  701. package/dist/render/components/wheels-plp/PlpVehicleGate.d.ts +10 -0
  702. package/dist/render/components/wheels-plp/PlpVehicleGate.js +1 -0
  703. package/dist/render/components/wheels-plp/usePlpCardSelection.d.ts +18 -0
  704. package/dist/render/components/wheels-plp/usePlpCardSelection.js +1 -0
  705. package/dist/render/components/wheels-plp/usePlpFiltersInUrl.d.ts +10 -0
  706. package/dist/render/components/wheels-plp/usePlpFiltersInUrl.js +1 -0
  707. package/dist/render/components/wheels-plp/useVehicleCatalog.d.ts +10 -0
  708. package/dist/render/components/wheels-plp/useVehicleCatalog.js +1 -0
  709. package/dist/render/components/wheels-plp/useWheelCardBottom.d.ts +20 -0
  710. package/dist/render/components/wheels-plp/useWheelCardBottom.js +1 -0
  711. package/dist/render/lib/api/chat.d.ts +31 -0
  712. package/dist/render/lib/api/chat.js +1 -0
  713. package/dist/render/lib/api/fitment-data.d.ts +18 -0
  714. package/dist/render/lib/api/fitment-data.js +1 -0
  715. package/dist/render/lib/api/gallery-builds-data.d.ts +4 -0
  716. package/dist/render/lib/api/gallery-builds-data.js +1 -0
  717. package/dist/render/lib/api/generated-upload-limits.d.ts +1292 -0
  718. package/dist/render/lib/api/generated-upload-limits.js +1 -0
  719. package/dist/render/lib/api/index.d.ts +806 -0
  720. package/dist/render/lib/api/index.js +1 -0
  721. package/dist/render/lib/api/origins.d.ts +2 -0
  722. package/dist/render/lib/api/origins.js +1 -0
  723. package/dist/render/lib/api/types.d.ts +3928 -0
  724. package/dist/render/lib/api/types.js +0 -0
  725. package/dist/render/lib/api/upload-limits.d.ts +10 -0
  726. package/dist/render/lib/api/upload-limits.js +1 -0
  727. package/dist/render/lib/assistant-chat.d.ts +35 -0
  728. package/dist/render/lib/assistant-chat.js +1 -0
  729. package/dist/render/lib/auth.d.ts +6 -0
  730. package/dist/render/lib/auth.js +1 -0
  731. package/dist/render/lib/booking/text-kinds.d.ts +11 -0
  732. package/dist/render/lib/booking/text-kinds.js +1 -0
  733. package/dist/render/lib/booking-dates.d.ts +23 -0
  734. package/dist/render/lib/booking-dates.js +1 -0
  735. package/dist/render/lib/cache-evict.d.ts +4 -0
  736. package/dist/render/lib/cache-evict.js +1 -0
  737. package/dist/render/lib/cart/coupon-preview.d.ts +12 -0
  738. package/dist/render/lib/cart/coupon-preview.js +1 -0
  739. package/dist/render/lib/cart/shipping-status.d.ts +12 -0
  740. package/dist/render/lib/cart/shipping-status.js +1 -0
  741. package/dist/render/lib/cart/snapshot-lines.d.ts +3 -0
  742. package/dist/render/lib/cart/snapshot-lines.js +1 -0
  743. package/dist/render/lib/catalog/from-tyre.d.ts +3 -0
  744. package/dist/render/lib/catalog/from-tyre.js +1 -0
  745. package/dist/render/lib/catalog/from-wheel.d.ts +7 -0
  746. package/dist/render/lib/catalog/from-wheel.js +1 -0
  747. package/dist/render/lib/catalog/listing-links.d.ts +11 -0
  748. package/dist/render/lib/catalog/listing-links.js +1 -0
  749. package/dist/render/lib/catalog/plp-item.d.ts +39 -0
  750. package/dist/render/lib/catalog/plp-item.js +1 -0
  751. package/dist/render/lib/catalog/plp-vehicle-copy.d.ts +20 -0
  752. package/dist/render/lib/catalog/plp-vehicle-copy.js +1 -0
  753. package/dist/render/lib/catalog/plp-vehicle-slice.d.ts +13 -0
  754. package/dist/render/lib/catalog/plp-vehicle-slice.js +1 -0
  755. package/dist/render/lib/catalog/product-options.d.ts +27 -0
  756. package/dist/render/lib/catalog/product-options.js +1 -0
  757. package/dist/render/lib/catalog/purchase.d.ts +46 -0
  758. package/dist/render/lib/catalog/purchase.js +1 -0
  759. package/dist/render/lib/catalog/related-cards.d.ts +12 -0
  760. package/dist/render/lib/catalog/related-cards.js +1 -0
  761. package/dist/render/lib/catalog/relationships.d.ts +28 -0
  762. package/dist/render/lib/catalog/relationships.js +1 -0
  763. package/dist/render/lib/catalog/store-product.d.ts +56 -0
  764. package/dist/render/lib/catalog/store-product.js +1 -0
  765. package/dist/render/lib/chat-socket.d.ts +8 -0
  766. package/dist/render/lib/chat-socket.js +1 -0
  767. package/dist/render/lib/checkout/wallet-return.d.ts +52 -0
  768. package/dist/render/lib/checkout/wallet-return.js +1 -0
  769. package/dist/render/lib/chrome-cache.d.ts +6 -0
  770. package/dist/render/lib/chrome-cache.js +1 -0
  771. package/dist/render/lib/chrome-load.d.ts +28 -0
  772. package/dist/render/lib/chrome-load.js +1 -0
  773. package/dist/render/lib/click-ids.d.ts +8 -0
  774. package/dist/render/lib/click-ids.js +1 -0
  775. package/dist/render/lib/commerce.d.ts +42 -0
  776. package/dist/render/lib/commerce.js +1 -0
  777. package/dist/render/lib/consent/choice.d.ts +35 -0
  778. package/dist/render/lib/consent/choice.js +1 -0
  779. package/dist/render/lib/consent/rules.d.ts +60 -0
  780. package/dist/render/lib/consent/rules.js +1 -0
  781. package/dist/render/lib/content/catalog-filter-labels.d.ts +6 -0
  782. package/dist/render/lib/content/catalog-filter-labels.js +1 -0
  783. package/dist/render/lib/content/country-show-as.d.ts +9 -0
  784. package/dist/render/lib/content/country-show-as.js +1 -0
  785. package/dist/render/lib/content/english-variant.d.ts +4 -0
  786. package/dist/render/lib/content/english-variant.js +1 -0
  787. package/dist/render/lib/content/field-usage.d.ts +35 -0
  788. package/dist/render/lib/content/field-usage.js +1 -0
  789. package/dist/render/lib/content/header-bar.d.ts +8 -0
  790. package/dist/render/lib/content/header-bar.js +1 -0
  791. package/dist/render/lib/content/header-theme.d.ts +7 -0
  792. package/dist/render/lib/content/header-theme.js +1 -0
  793. package/dist/render/lib/content/menu-placeholder.d.ts +8 -0
  794. package/dist/render/lib/content/menu-placeholder.js +1 -0
  795. package/dist/render/lib/content/menu-row.d.ts +9 -0
  796. package/dist/render/lib/content/menu-row.js +1 -0
  797. package/dist/render/lib/content/product-hero.d.ts +13 -0
  798. package/dist/render/lib/content/product-hero.js +1 -0
  799. package/dist/render/lib/content/site.d.ts +62 -0
  800. package/dist/render/lib/content/site.js +1 -0
  801. package/dist/render/lib/content/source-values.d.ts +43 -0
  802. package/dist/render/lib/content/source-values.js +0 -0
  803. package/dist/render/lib/content/stock-words.d.ts +10 -0
  804. package/dist/render/lib/content/stock-words.js +1 -0
  805. package/dist/render/lib/countries.d.ts +5 -0
  806. package/dist/render/lib/countries.js +1 -0
  807. package/dist/render/lib/editor-route.d.ts +3 -0
  808. package/dist/render/lib/editor-route.js +1 -0
  809. package/dist/render/lib/email-shape.d.ts +1 -0
  810. package/dist/render/lib/email-shape.js +1 -0
  811. package/dist/render/lib/font-table.json +2588 -0
  812. package/dist/render/lib/forwarded-for.d.ts +6 -0
  813. package/dist/render/lib/forwarded-for.js +1 -0
  814. package/dist/render/lib/google-tag.d.ts +37 -0
  815. package/dist/render/lib/google-tag.js +1 -0
  816. package/dist/render/lib/header-settings-fetch.d.ts +5 -0
  817. package/dist/render/lib/header-settings-fetch.js +1 -0
  818. package/dist/render/lib/links/link-rules.d.ts +80 -0
  819. package/dist/render/lib/links/link-rules.js +1 -0
  820. package/dist/render/lib/logo/surface-is-light.d.ts +1 -0
  821. package/dist/render/lib/logo/surface-is-light.js +1 -0
  822. package/dist/render/lib/menu-link.d.ts +1 -0
  823. package/dist/render/lib/menu-link.js +1 -0
  824. package/dist/render/lib/menu-source.d.ts +23 -0
  825. package/dist/render/lib/menu-source.js +1 -0
  826. package/dist/render/lib/meta-pixel.d.ts +19 -0
  827. package/dist/render/lib/meta-pixel.js +1 -0
  828. package/dist/render/lib/newsletter-signup.d.ts +16 -0
  829. package/dist/render/lib/newsletter-signup.js +1 -0
  830. package/dist/render/lib/next-fonts.d.ts +3 -0
  831. package/dist/render/lib/next-fonts.js +1 -0
  832. package/dist/render/lib/popup/popup-rules.d.ts +25 -0
  833. package/dist/render/lib/popup/popup-rules.js +1 -0
  834. package/dist/render/lib/popup/text-kind-style.d.ts +3 -0
  835. package/dist/render/lib/popup/text-kind-style.js +1 -0
  836. package/dist/render/lib/puck/blocks/commerce/AccountBlock/render.d.ts +2 -0
  837. package/dist/render/lib/puck/blocks/commerce/AccountBlock/render.js +1 -0
  838. package/dist/render/lib/puck/blocks/commerce/AlloyWheelsListing/render.d.ts +6 -0
  839. package/dist/render/lib/puck/blocks/commerce/AlloyWheelsListing/render.js +1 -0
  840. package/dist/render/lib/puck/blocks/commerce/BundleOffer/render.d.ts +2 -0
  841. package/dist/render/lib/puck/blocks/commerce/BundleOffer/render.js +1 -0
  842. package/dist/render/lib/puck/blocks/commerce/CartBlock/render.d.ts +2 -0
  843. package/dist/render/lib/puck/blocks/commerce/CartBlock/render.js +1 -0
  844. package/dist/render/lib/puck/blocks/commerce/CatalogFilterBar/render.d.ts +2 -0
  845. package/dist/render/lib/puck/blocks/commerce/CatalogFilterBar/render.js +1 -0
  846. package/dist/render/lib/puck/blocks/commerce/CheckoutCTA/render.d.ts +6 -0
  847. package/dist/render/lib/puck/blocks/commerce/CheckoutCTA/render.js +1 -0
  848. package/dist/render/lib/puck/blocks/commerce/CollectionCard/render.d.ts +2 -0
  849. package/dist/render/lib/puck/blocks/commerce/CollectionCard/render.js +1 -0
  850. package/dist/render/lib/puck/blocks/commerce/Collections/render.d.ts +7 -0
  851. package/dist/render/lib/puck/blocks/commerce/Collections/render.js +1 -0
  852. package/dist/render/lib/puck/blocks/commerce/CompatibleTyres/render.d.ts +9 -0
  853. package/dist/render/lib/puck/blocks/commerce/CompatibleTyres/render.js +1 -0
  854. package/dist/render/lib/puck/blocks/commerce/CountrySwitchBlock/render.d.ts +2 -0
  855. package/dist/render/lib/puck/blocks/commerce/CountrySwitchBlock/render.js +1 -0
  856. package/dist/render/lib/puck/blocks/commerce/DeliveryOptions/render.d.ts +6 -0
  857. package/dist/render/lib/puck/blocks/commerce/DeliveryOptions/render.js +1 -0
  858. package/dist/render/lib/puck/blocks/commerce/DetailRows/render.d.ts +9 -0
  859. package/dist/render/lib/puck/blocks/commerce/DetailRows/render.js +1 -0
  860. package/dist/render/lib/puck/blocks/commerce/EuTyreLabel/render.d.ts +2 -0
  861. package/dist/render/lib/puck/blocks/commerce/EuTyreLabel/render.js +1 -0
  862. package/dist/render/lib/puck/blocks/commerce/FitmentFinder/render.d.ts +8 -0
  863. package/dist/render/lib/puck/blocks/commerce/FitmentFinder/render.js +1 -0
  864. package/dist/render/lib/puck/blocks/commerce/FitmentTable/render.d.ts +9 -0
  865. package/dist/render/lib/puck/blocks/commerce/FitmentTable/render.js +1 -0
  866. package/dist/render/lib/puck/blocks/commerce/Gallery/render.d.ts +14 -0
  867. package/dist/render/lib/puck/blocks/commerce/Gallery/render.js +1 -0
  868. package/dist/render/lib/puck/blocks/commerce/GalleryB/render.d.ts +13 -0
  869. package/dist/render/lib/puck/blocks/commerce/GalleryB/render.js +1 -0
  870. package/dist/render/lib/puck/blocks/commerce/GalleryItem/render.d.ts +2 -0
  871. package/dist/render/lib/puck/blocks/commerce/GalleryItem/render.js +1 -0
  872. package/dist/render/lib/puck/blocks/commerce/Header/render.d.ts +2 -0
  873. package/dist/render/lib/puck/blocks/commerce/Header/render.js +1 -0
  874. package/dist/render/lib/puck/blocks/commerce/ProductGrid/render.d.ts +7 -0
  875. package/dist/render/lib/puck/blocks/commerce/ProductGrid/render.js +1 -0
  876. package/dist/render/lib/puck/blocks/commerce/ProductHero/render.d.ts +2 -0
  877. package/dist/render/lib/puck/blocks/commerce/ProductHero/render.js +1 -0
  878. package/dist/render/lib/puck/blocks/commerce/ProductRef/render.d.ts +2 -0
  879. package/dist/render/lib/puck/blocks/commerce/ProductRef/render.js +1 -0
  880. package/dist/render/lib/puck/blocks/commerce/RelatedProducts/render.d.ts +6 -0
  881. package/dist/render/lib/puck/blocks/commerce/RelatedProducts/render.js +1 -0
  882. package/dist/render/lib/puck/blocks/commerce/SearchBlock/render.d.ts +2 -0
  883. package/dist/render/lib/puck/blocks/commerce/SearchBlock/render.js +1 -0
  884. package/dist/render/lib/puck/blocks/commerce/SpecTable/render.d.ts +7 -0
  885. package/dist/render/lib/puck/blocks/commerce/SpecTable/render.js +1 -0
  886. package/dist/render/lib/puck/blocks/commerce/ThemeSwitchBlock/render.d.ts +2 -0
  887. package/dist/render/lib/puck/blocks/commerce/ThemeSwitchBlock/render.js +1 -0
  888. package/dist/render/lib/puck/blocks/commerce/TyreFitment/render.d.ts +2 -0
  889. package/dist/render/lib/puck/blocks/commerce/TyreFitment/render.js +1 -0
  890. package/dist/render/lib/puck/blocks/commerce/TyreSizes/render.d.ts +2 -0
  891. package/dist/render/lib/puck/blocks/commerce/TyreSizes/render.js +1 -0
  892. package/dist/render/lib/puck/blocks/commerce/placeholders.d.ts +4 -0
  893. package/dist/render/lib/puck/blocks/commerce/placeholders.js +1 -0
  894. package/dist/render/lib/puck/blocks/commerce/renders.d.ts +32 -0
  895. package/dist/render/lib/puck/blocks/commerce/renders.js +1 -0
  896. package/dist/render/lib/puck/blocks/service/AppointmentBooking/render.d.ts +8 -0
  897. package/dist/render/lib/puck/blocks/service/AppointmentBooking/render.js +1 -0
  898. package/dist/render/lib/puck/blocks/service/BookingBox/render.d.ts +2 -0
  899. package/dist/render/lib/puck/blocks/service/BookingBox/render.js +1 -0
  900. package/dist/render/lib/puck/blocks/service/ContactRouting/render.d.ts +2 -0
  901. package/dist/render/lib/puck/blocks/service/ContactRouting/render.js +1 -0
  902. package/dist/render/lib/puck/blocks/service/Events/render.d.ts +7 -0
  903. package/dist/render/lib/puck/blocks/service/Events/render.js +1 -0
  904. package/dist/render/lib/puck/blocks/service/Footer/render.d.ts +8 -0
  905. package/dist/render/lib/puck/blocks/service/Footer/render.js +1 -0
  906. package/dist/render/lib/puck/blocks/service/FooterDivider/render.d.ts +2 -0
  907. package/dist/render/lib/puck/blocks/service/FooterDivider/render.js +1 -0
  908. package/dist/render/lib/puck/blocks/service/FooterEmailSignup/render.d.ts +2 -0
  909. package/dist/render/lib/puck/blocks/service/FooterEmailSignup/render.js +1 -0
  910. package/dist/render/lib/puck/blocks/service/FooterGroup/render.d.ts +8 -0
  911. package/dist/render/lib/puck/blocks/service/FooterGroup/render.js +1 -0
  912. package/dist/render/lib/puck/blocks/service/FooterLogo/render.d.ts +2 -0
  913. package/dist/render/lib/puck/blocks/service/FooterLogo/render.js +1 -0
  914. package/dist/render/lib/puck/blocks/service/FooterMenu/render.d.ts +2 -0
  915. package/dist/render/lib/puck/blocks/service/FooterMenu/render.js +1 -0
  916. package/dist/render/lib/puck/blocks/service/FooterSocial/render.d.ts +2 -0
  917. package/dist/render/lib/puck/blocks/service/FooterSocial/render.js +1 -0
  918. package/dist/render/lib/puck/blocks/service/FooterText/render.d.ts +2 -0
  919. package/dist/render/lib/puck/blocks/service/FooterText/render.js +1 -0
  920. package/dist/render/lib/puck/blocks/service/Form/render.d.ts +2 -0
  921. package/dist/render/lib/puck/blocks/service/Form/render.js +1 -0
  922. package/dist/render/lib/puck/blocks/service/LegalLineBlock/render.d.ts +2 -0
  923. package/dist/render/lib/puck/blocks/service/LegalLineBlock/render.js +1 -0
  924. package/dist/render/lib/puck/blocks/service/ReviewForm/render.d.ts +6 -0
  925. package/dist/render/lib/puck/blocks/service/ReviewForm/render.js +1 -0
  926. package/dist/render/lib/puck/blocks/service/Reviews/render.d.ts +6 -0
  927. package/dist/render/lib/puck/blocks/service/Reviews/render.js +1 -0
  928. package/dist/render/lib/puck/blocks/service/SiteFooter/render.d.ts +7 -0
  929. package/dist/render/lib/puck/blocks/service/SiteFooter/render.js +1 -0
  930. package/dist/render/lib/puck/blocks/service/SiteHeader/render.d.ts +7 -0
  931. package/dist/render/lib/puck/blocks/service/SiteHeader/render.js +1 -0
  932. package/dist/render/lib/puck/blocks/service/SubmitBox/render.d.ts +2 -0
  933. package/dist/render/lib/puck/blocks/service/SubmitBox/render.js +1 -0
  934. package/dist/render/lib/puck/blocks/service/SubmitBuild/render.d.ts +8 -0
  935. package/dist/render/lib/puck/blocks/service/SubmitBuild/render.js +1 -0
  936. package/dist/render/lib/puck/blocks/service/Subscribe/render.d.ts +2 -0
  937. package/dist/render/lib/puck/blocks/service/Subscribe/render.js +1 -0
  938. package/dist/render/lib/puck/blocks/service/SubscribeBlogs/render.d.ts +17 -0
  939. package/dist/render/lib/puck/blocks/service/SubscribeBlogs/render.js +1 -0
  940. package/dist/render/lib/puck/blocks/service/renders.d.ts +26 -0
  941. package/dist/render/lib/puck/blocks/service/renders.js +1 -0
  942. package/dist/render/lib/puck/blocks/service/subscribe.d.ts +33 -0
  943. package/dist/render/lib/puck/blocks/service/subscribe.js +1 -0
  944. package/dist/render/lib/puck/blocks/text-part-shapes.json +1649 -0
  945. package/dist/render/lib/puck/current-product.d.ts +19 -0
  946. package/dist/render/lib/puck/current-product.js +1 -0
  947. package/dist/render/lib/puck/current-record.d.ts +44 -0
  948. package/dist/render/lib/puck/current-record.js +1 -0
  949. package/dist/render/lib/puck/fitment-table-dynamic.d.ts +4 -0
  950. package/dist/render/lib/puck/fitment-table-dynamic.js +1 -0
  951. package/dist/render/lib/puck/footer-page-blocks.d.ts +7 -0
  952. package/dist/render/lib/puck/footer-page-blocks.js +1 -0
  953. package/dist/render/lib/puck/preview-pick.d.ts +7 -0
  954. package/dist/render/lib/puck/preview-pick.js +1 -0
  955. package/dist/render/lib/puck/preview-product.d.ts +6 -0
  956. package/dist/render/lib/puck/preview-product.js +1 -0
  957. package/dist/render/lib/puck/render-client.d.ts +5 -0
  958. package/dist/render/lib/puck/render-client.js +1 -0
  959. package/dist/render/lib/puck/render-config.d.ts +4 -0
  960. package/dist/render/lib/puck/render-config.js +1 -0
  961. package/dist/render/lib/puck/slot-seeds.d.ts +3 -0
  962. package/dist/render/lib/puck/slot-seeds.js +1 -0
  963. package/dist/render/lib/puck/storefront-root.d.ts +18 -0
  964. package/dist/render/lib/puck/storefront-root.js +1 -0
  965. package/dist/render/lib/puck/subscribe-content-type.d.ts +10 -0
  966. package/dist/render/lib/puck/subscribe-content-type.js +1 -0
  967. package/dist/render/lib/puck/template-product-kind.d.ts +7 -0
  968. package/dist/render/lib/puck/template-product-kind.js +1 -0
  969. package/dist/render/lib/sections/card-image-sizes.d.ts +12 -0
  970. package/dist/render/lib/sections/card-image-sizes.js +1 -0
  971. package/dist/render/lib/sections/card-show-defaults.d.ts +10 -0
  972. package/dist/render/lib/sections/card-show-defaults.js +1 -0
  973. package/dist/render/lib/sections/catalog-filter-layout.d.ts +7 -0
  974. package/dist/render/lib/sections/catalog-filter-layout.js +1 -0
  975. package/dist/render/lib/sections/fill-row.d.ts +16 -0
  976. package/dist/render/lib/sections/fill-row.js +1 -0
  977. package/dist/render/lib/sections/hero-arrangement.d.ts +11 -0
  978. package/dist/render/lib/sections/hero-arrangement.js +1 -0
  979. package/dist/render/lib/sections/hero-gallery.d.ts +14 -0
  980. package/dist/render/lib/sections/hero-gallery.js +1 -0
  981. package/dist/render/lib/sections/hero-price-placement.d.ts +7 -0
  982. package/dist/render/lib/sections/hero-price-placement.js +1 -0
  983. package/dist/render/lib/select-keys.d.ts +18 -0
  984. package/dist/render/lib/select-keys.js +1 -0
  985. package/dist/render/lib/site-headers.d.ts +3 -0
  986. package/dist/render/lib/site-headers.js +1 -0
  987. package/dist/render/lib/storefront-page-groups.d.ts +16 -0
  988. package/dist/render/lib/storefront-page-groups.js +1 -0
  989. package/dist/render/lib/storefront-page-order.d.ts +3 -0
  990. package/dist/render/lib/storefront-page-order.js +1 -0
  991. package/dist/render/lib/submit/text-kinds.d.ts +11 -0
  992. package/dist/render/lib/submit/text-kinds.js +1 -0
  993. package/dist/render/lib/tyres/eu-tyre-label.d.ts +29 -0
  994. package/dist/render/lib/tyres/eu-tyre-label.js +1 -0
  995. package/dist/render/lib/tyres/placeholder-tyre.d.ts +2 -0
  996. package/dist/render/lib/tyres/placeholder-tyre.js +1 -0
  997. package/dist/render/lib/tyres/spec-sheet.d.ts +12 -0
  998. package/dist/render/lib/tyres/spec-sheet.js +1 -0
  999. package/dist/render/lib/tyres/tyre-brand-name.d.ts +2 -0
  1000. package/dist/render/lib/tyres/tyre-brand-name.js +1 -0
  1001. package/dist/render/lib/tyres/tyre-fit-copy.d.ts +3 -0
  1002. package/dist/render/lib/tyres/tyre-fit-copy.js +1 -0
  1003. package/dist/render/lib/tyres/tyre-page-path.d.ts +2 -0
  1004. package/dist/render/lib/tyres/tyre-page-path.js +1 -0
  1005. package/dist/render/lib/tyres/tyre-purchase.d.ts +12 -0
  1006. package/dist/render/lib/tyres/tyre-purchase.js +1 -0
  1007. package/dist/render/lib/tyres/tyre-size-fit.d.ts +12 -0
  1008. package/dist/render/lib/tyres/tyre-size-fit.js +1 -0
  1009. package/dist/render/lib/tyres/tyre-size.d.ts +11 -0
  1010. package/dist/render/lib/tyres/tyre-size.js +1 -0
  1011. package/dist/render/lib/tyres/tyre-vehicle-fitment.d.ts +17 -0
  1012. package/dist/render/lib/tyres/tyre-vehicle-fitment.js +1 -0
  1013. package/dist/render/lib/uuid.d.ts +1 -0
  1014. package/dist/render/lib/uuid.js +1 -0
  1015. package/dist/render/lib/wheels/applied-vehicle.d.ts +1 -0
  1016. package/dist/render/lib/wheels/applied-vehicle.js +1 -0
  1017. package/dist/render/lib/wheels/catalog-default-layout.d.ts +14 -0
  1018. package/dist/render/lib/wheels/catalog-default-layout.js +1 -0
  1019. package/dist/render/lib/wheels/catalog-filters-to-listing.d.ts +9 -0
  1020. package/dist/render/lib/wheels/catalog-filters-to-listing.js +1 -0
  1021. package/dist/render/lib/wheels/catalog-filters.d.ts +19 -0
  1022. package/dist/render/lib/wheels/catalog-filters.js +1 -0
  1023. package/dist/render/lib/wheels/collections.d.ts +22 -0
  1024. package/dist/render/lib/wheels/collections.js +1 -0
  1025. package/dist/render/lib/wheels/cover-rendering.d.ts +23 -0
  1026. package/dist/render/lib/wheels/cover-rendering.js +1 -0
  1027. package/dist/render/lib/wheels/detail-shots.d.ts +10 -0
  1028. package/dist/render/lib/wheels/detail-shots.js +1 -0
  1029. package/dist/render/lib/wheels/finish-colour-match.d.ts +4 -0
  1030. package/dist/render/lib/wheels/finish-colour-match.js +1 -0
  1031. package/dist/render/lib/wheels/finish-defaults.d.ts +16 -0
  1032. package/dist/render/lib/wheels/finish-defaults.js +1 -0
  1033. package/dist/render/lib/wheels/finish-photos.d.ts +2 -0
  1034. package/dist/render/lib/wheels/finish-photos.js +1 -0
  1035. package/dist/render/lib/wheels/fitment-columns.d.ts +9 -0
  1036. package/dist/render/lib/wheels/fitment-columns.js +1 -0
  1037. package/dist/render/lib/wheels/fitment-options.d.ts +18 -0
  1038. package/dist/render/lib/wheels/fitment-options.js +1 -0
  1039. package/dist/render/lib/wheels/fitment-row-fit.d.ts +17 -0
  1040. package/dist/render/lib/wheels/fitment-row-fit.js +1 -0
  1041. package/dist/render/lib/wheels/fitments.d.ts +13 -0
  1042. package/dist/render/lib/wheels/fitments.js +1 -0
  1043. package/dist/render/lib/wheels/grid-source.d.ts +12 -0
  1044. package/dist/render/lib/wheels/grid-source.js +1 -0
  1045. package/dist/render/lib/wheels/listable-wheels.d.ts +3 -0
  1046. package/dist/render/lib/wheels/listable-wheels.js +1 -0
  1047. package/dist/render/lib/wheels/lug-count.d.ts +10 -0
  1048. package/dist/render/lib/wheels/lug-count.js +1 -0
  1049. package/dist/render/lib/wheels/pdp-fit-verdict.d.ts +27 -0
  1050. package/dist/render/lib/wheels/pdp-fit-verdict.js +1 -0
  1051. package/dist/render/lib/wheels/picker-goes-to-the-shop.d.ts +10 -0
  1052. package/dist/render/lib/wheels/picker-goes-to-the-shop.js +1 -0
  1053. package/dist/render/lib/wheels/placeholder-wheel.d.ts +3 -0
  1054. package/dist/render/lib/wheels/placeholder-wheel.js +1 -0
  1055. package/dist/render/lib/wheels/plp-filters.d.ts +100 -0
  1056. package/dist/render/lib/wheels/plp-filters.js +1 -0
  1057. package/dist/render/lib/wheels/plp-fitment-match.d.ts +61 -0
  1058. package/dist/render/lib/wheels/plp-fitment-match.js +1 -0
  1059. package/dist/render/lib/wheels/plp-grid.d.ts +18 -0
  1060. package/dist/render/lib/wheels/plp-grid.js +1 -0
  1061. package/dist/render/lib/wheels/plp-hydrate.d.ts +21 -0
  1062. package/dist/render/lib/wheels/plp-hydrate.js +1 -0
  1063. package/dist/render/lib/wheels/plp-vehicle-params.d.ts +10 -0
  1064. package/dist/render/lib/wheels/plp-vehicle-params.js +1 -0
  1065. package/dist/render/lib/wheels/pre-order-intent.d.ts +3 -0
  1066. package/dist/render/lib/wheels/pre-order-intent.js +1 -0
  1067. package/dist/render/lib/wheels/pre-order.d.ts +7 -0
  1068. package/dist/render/lib/wheels/pre-order.js +1 -0
  1069. package/dist/render/lib/wheels/price-display.d.ts +47 -0
  1070. package/dist/render/lib/wheels/price-display.js +1 -0
  1071. package/dist/render/lib/wheels/render-fit.d.ts +3 -0
  1072. package/dist/render/lib/wheels/render-fit.js +1 -0
  1073. package/dist/render/lib/wheels/renderable-finishes.d.ts +6 -0
  1074. package/dist/render/lib/wheels/renderable-finishes.js +1 -0
  1075. package/dist/render/lib/wheels/selected-studs.d.ts +9 -0
  1076. package/dist/render/lib/wheels/selected-studs.js +1 -0
  1077. package/dist/render/lib/wheels/set-mode.d.ts +34 -0
  1078. package/dist/render/lib/wheels/set-mode.js +1 -0
  1079. package/dist/render/lib/wheels/size-range.d.ts +10 -0
  1080. package/dist/render/lib/wheels/size-range.js +1 -0
  1081. package/dist/render/lib/wheels/spec-format.d.ts +7 -0
  1082. package/dist/render/lib/wheels/spec-format.js +1 -0
  1083. package/dist/render/lib/wheels/spec-sheet.d.ts +58 -0
  1084. package/dist/render/lib/wheels/spec-sheet.js +1 -0
  1085. package/dist/render/lib/wheels/tyre-render.d.ts +34 -0
  1086. package/dist/render/lib/wheels/tyre-render.js +1 -0
  1087. package/dist/render/lib/wheels/variant-spec.d.ts +9 -0
  1088. package/dist/render/lib/wheels/variant-spec.js +1 -0
  1089. package/dist/render/lib/wheels/variants.d.ts +8 -0
  1090. package/dist/render/lib/wheels/variants.js +1 -0
  1091. package/dist/render/lib/wheels/vehicle-field-focus.d.ts +7 -0
  1092. package/dist/render/lib/wheels/vehicle-field-focus.js +1 -0
  1093. package/dist/render/lib/wheels/vehicle-memory.d.ts +25 -0
  1094. package/dist/render/lib/wheels/vehicle-memory.js +1 -0
  1095. package/dist/render/lib/wheels/wheel-name.d.ts +4 -0
  1096. package/dist/render/lib/wheels/wheel-name.js +1 -0
  1097. package/dist/render/lib/wheels/wheel-specs.d.ts +14 -0
  1098. package/dist/render/lib/wheels/wheel-specs.js +1 -0
  1099. package/dist/render/lib/wheels/wheel-summary-casing.d.ts +4 -0
  1100. package/dist/render/lib/wheels/wheel-summary-casing.js +1 -0
  1101. package/dist/render/lib/wheels/wheels.d.ts +38 -0
  1102. package/dist/render/lib/wheels/wheels.js +1 -0
  1103. package/dist/render/mocks/fitment-snapshot.json +3911 -0
  1104. package/dist/render/mocks/gallery-builds.json +213 -0
  1105. package/dist/words.d.ts +68 -7
  1106. package/dist/words.js +5 -5
  1107. package/package.json +37 -5
  1108. package/dist/chunks/chunk-BDITPYS5.js +0 -1
  1109. package/dist/chunks/chunk-EHKAKDFN.js +0 -255
  1110. /package/dist/chunks/{chunk-4NM2MDGN.js → chunk-76HTJBKU.js} +0 -0
  1111. /package/dist/chunks/{chunk-EHKAKDFN.js.LEGAL.txt → chunk-PHH7L5FJ.js.LEGAL.txt} +0 -0
@@ -0,0 +1,3264 @@
1
+ /* THE SITE'S STYLESHEET — the design tokens, the theme-derived surfaces, every
2
+ section's and card's own rules, the word styles, the motion. It is the blocks'
3
+ look, so it lives with the blocks (efficient-storefront doc/adr/0009).
4
+
5
+ It carries no `@import "tailwindcss"` and no `@source`: those are the HOST's,
6
+ because only the host knows which files its pages are built from. The
7
+ storefront's entry is its `app/tailwind.css` (`@import "tailwindcss"`, then
8
+ this file, then the storefront's `@source` exclusions); the package's canvas
9
+ is built from this repository's own. Everything below is the storefront's
10
+ `app/globals.css` from line 79 on, as it stood on 2026-10-10. */
11
+
12
+ /* ─── Storefront design tokens ───────────────────────────────────── */
13
+ /* ⚠️ THE SURFACE, LINE AND RADIUS TOKENS ARE `--site-*`, AND THAT IS A NAME
14
+ COLLISION FIXED, NOT A STYLE. efficient-shell's UI kit declares `--surface`,
15
+ `--line` and `--radius-sm…2xl` as ITS tokens, and this sheet publishes the
16
+ site's on <body> (the surface mapping below, the corner tokens in
17
+ app/layout.tsx). Where both are on one page — the storefront's editor harnesses — the kit's
18
+ dialogs, popovers and colour picker are portalled to <body> and inherited the
19
+ SITE'S values: light-on-light in dark mode, square menus on a site whose
20
+ Corners is 0. Harness UI-24 says editor chrome resolves to the kit's tokens,
21
+ so the site's own carry a prefix the kit never reads. Tailwind's class names
22
+ are unchanged: `bg-surface`, `border-line` and `rounded-*` reach the prefixed
23
+ properties through the theme block (`inline`) with the named colours below.
24
+
25
+ The `-base` inputs and `--bg` / `--fg` / `--accent` keep their names: the kit
26
+ declares none of them. A STORED fill that names the old spelling —
27
+ `var(--surface)` in a published section's background — is translated where it
28
+ is painted (`paintedFill` in lib/content/section-background), not here. */
29
+ /* Dark theme (default) */
30
+ :root {
31
+ /* Surface BASES — the light/dark page surfaces. Split out from the live token
32
+ for the same reason --fg-base is (below): the colour Theme's per-site
33
+ surfaces (--theme-bg / --theme-surface / --theme-line / --theme-muted) remap
34
+ these on `.sf-theme-surfaces`, in EVERY mode since 66238, and an un-themed
35
+ site falls back to the base. A base is a plain literal; the live token is
36
+ always `var(<base>)`.
37
+
38
+ ⏳ THIS USED TO SAY "in DARK MODE ONLY … so a near-black themed background
39
+ can never end up under the light theme's near-black ink". That protection
40
+ went when a theme became one flat palette and the surfaces started applying
41
+ in both modes. The sentence stayed, and the gap it described re-opened in
42
+ the CHROME, which was still taking its ink from the mode. Fixed by mapping
43
+ --fg beside --bg on `.sf-theme-surfaces`; see the note there. */
44
+ --bg-base: #0a0a0a;
45
+ --bg: var(--bg-base);
46
+ --surface-base: #131313;
47
+ --site-surface: var(--surface-base);
48
+ --surface-2-base: #1c1c1c;
49
+ --site-surface-2: var(--surface-2-base);
50
+ /* Foreground BASE — the light/dark page ink. Split out from --fg so the colour
51
+ Theme's per-site ink (--theme-fg) can remap --fg on `.sf-theme-surfaces` and
52
+ `.sf-themed-content`, in EVERY mode; a site that has chosen no theme falls
53
+ back to this base.
54
+
55
+ ⏳ THIS USED TO SAY "in DARK mode only … so a dark-mode cream text can't land
56
+ on the cream light-mode page". True when a preset carried an ink and no
57
+ background of its own. A flat palette (66238) carries both, so the ink can
58
+ only ever land on that palette's own background — the case this guarded
59
+ against cannot arise, and the guard itself became the bug in the chrome. */
60
+ --fg-base: #f5f1e8;
61
+ --fg: var(--fg-base);
62
+ --fg-dim-base: #9a9388;
63
+ --fg-dim: var(--fg-dim-base);
64
+ --line-base: #2a2a2a;
65
+ --site-line: var(--line-base);
66
+ /* Theme-colour BASES. Split out from the live token for exactly the reason
67
+ --fg-base and the surface bases are: the merchant's theme colour is now TWO
68
+ colours, one per mode, and .sf-theme-accent below maps whichever the mode
69
+ calls for onto --accent. A base is a plain literal per mode; the live token is
70
+ always `var(<base>)`, so a site with no theme of its own resolves to
71
+ precisely the values it uses today. */
72
+ --accent-base: #c9a461;
73
+ --accent: var(--accent-base);
74
+ --accent-2-base: #b08a44;
75
+ --accent-2: var(--accent-2-base);
76
+
77
+ /* ⛔ THE FOUR MODE-PINNED accent bases (--accent-base-on-dark / -on-light and
78
+ their -2 pair, card 66401.g) WERE DELETED HERE on 2026-09-04. They existed
79
+ for one reader — the `.sf-section-surface-*` blocks, which pinned a mode and
80
+ so could not read the page-following --accent-base. Nothing pins a mode any
81
+ more (see the note above `.sf-section-accent` below), so nothing read them.
82
+ Adding them back without a mode-pinned block to read them is dead weight. */
83
+ /* The ink a filled control puts ON the accent. A BASE like the others, and a
84
+ literal here because :root has no theme to derive from: it is accentInk()'s
85
+ answer for the house gold above (black, 8.97:1). A themed site overrides it
86
+ from the carrier; see .sf-theme-accent. */
87
+ --accent-ink-base: #000000;
88
+ --accent-ink: var(--accent-ink-base);
89
+ /* THE BUTTON'S FILL, and by default it IS the accent (card 66504). A theme
90
+ may carry its own button colour; one that does not — every theme that
91
+ exists today — resolves straight back to the accent triple beside this, so
92
+ the button is byte-identical to what it painted before this token existed.
93
+
94
+ ⚠️ DECLARED IN EVERY SCOPE THAT REBINDS --accent, not only here. A custom
95
+ property is resolved where it is DECLARED, so a single `--btn: var(--accent)`
96
+ on the carrier would freeze the site accent and a section that pinned its own
97
+ accent would stop moving its buttons. Same rule, same reason, as --accent-ink
98
+ next door; scripts/test-button-ink.ts holds it for that one. */
99
+ --btn: var(--theme-btn, var(--accent));
100
+ --btn-2: var(--theme-btn-2, var(--accent-2));
101
+ --btn-ink: var(--theme-btn-ink, var(--accent-ink));
102
+ /* Discount / "savings" + "fits perfect" success accent. Tuned to
103
+ read on the dark page bg without becoming a neon highlight. */
104
+ --green: #7aa672;
105
+ /* Foreground text that sits over dark media — video posters, photo
106
+ overlays — and must stay cream in BOTH themes (unlike --fg, which
107
+ flips to near-black in light mode). Defined once here, deliberately
108
+ not overridden in the [data-theme="light"] block below. */
109
+ --overlay-fg: #f5f1e8;
110
+ /* Its mirror: foreground text that sits over a LIGHT surface and must stay
111
+ near-black in BOTH themes. Same contract as --overlay-fg above — declared
112
+ once here and deliberately NOT overridden in the [data-theme="light"] block
113
+ below. Its one consumer is the site header's "Text colour at the top =
114
+ Dark" override, which is a merchant saying "the thing under my nav is
115
+ light" about a page in either theme, so it cannot be var(--fg). The value
116
+ is the same near-black the deleted light surface class used to paint. */
117
+ --overlay-fg-on-light: #0a0a0a;
118
+ /* Alert / destructive accent — a muted terracotta. Used for inline
119
+ error messages and the cart "remove" hover. Theme-independent. */
120
+ --alert: #c97a6a;
121
+ /* Deep warm tint used as the far stop of the decorative card-panel
122
+ gradients (gallery card hover panel, PDP process panel). */
123
+ --panel-tint: #181410;
124
+ /* Pure black / white reserved for chrome composited over media —
125
+ video letterbox scrims and the controls + borders layered on top.
126
+ Theme-independent: media renders dark regardless of page theme. */
127
+ --overlay-scrim: #000;
128
+ --overlay-chrome: #fff;
129
+ /* Tells the browser to paint OS chrome (select dropdown panels,
130
+ scrollbars, native checkboxes) in dark mode so they match the
131
+ site palette instead of flashing white. */
132
+ color-scheme: dark;
133
+ /* ─── Card-surface tokens ─────────────────────────────────────
134
+ Used by the home page ProductCard, the wheels CatalogCard, and
135
+ the journal ArticleCard / Cover article. Dark mode lifts each
136
+ card to a medium-dark grey (~#4d4b48 — a clear step lighter than
137
+ the page bg #0a0a0a so cards don't melt into the page). The
138
+ hover variant nudges another 10pp toward the foreground for a
139
+ visible "lifted" state. Light mode keeps the originals so the
140
+ cream-on-cream card system stays untouched.
141
+ The journal "Recent issues" panel uses --journal-panel-bg, which
142
+ dark mode collapses to --bg (no visible grey rectangle in the
143
+ bottom-right) while light mode keeps the original --site-surface fill.
144
+ The wheels catalog grid container uses --catalog-grid-bg, which
145
+ dark mode collapses to --bg (so empty grid cells in partial last
146
+ rows match the page) while light mode keeps the original --site-line
147
+ fill (which paints the hairline-divider effect through the 2px
148
+ gap between cards).
149
+ Card mix ratios pushed down again per request — another 20%
150
+ darker step. Now 92/8 rest, 82/18 hover (was 85/15 / 75/25),
151
+ resolving to roughly ~#2d2d2c rest / ~#3c3a37 hover. Each step
152
+ shaves more cream/foreground out of the mix and pulls the result
153
+ closer to the underlying --site-surface-2 #1c1c1c base, while still
154
+ keeping enough lift to read as a clear card surface against the
155
+ #0a0a0a page background.
156
+ The five recipes themselves live in the "theme-derived" block
157
+ below rather than here — they read --site-surface-2 / --bg / --fg,
158
+ and a custom property substitutes its var()s where it is
159
+ DECLARED, so declaring them at :root alone would freeze the
160
+ built-in surfaces and ignore the merchant's palette. */
161
+ /* ─── Component-scoped tokens ──────────────────────────────────
162
+ Used by single components but defined here so the underlying
163
+ colour stays a one-place change. Theme-independent unless the
164
+ light block overrides. */
165
+ /* Muted accent tint used by the PDP fitment-diagram outline. */
166
+ --accent-dim: #9a8050;
167
+ /* PhotoPlate's plate band (card 029) — the spec strip docked over the
168
+ plate photograph's bottom edge (or below it on phones). PHOTOGRAPHY
169
+ FURNITURE: like --overlay-fg it stays dark on BOTH page themes, so it
170
+ is deliberately NOT overridden in the light block below — a light-theme
171
+ tenant (XM) still gets the dark band the design draws. A tenant theme
172
+ may override all three. Values are the XM draft's: band #131313, ink
173
+ #C9C9C5, dim ink #6A6A64 (the mobile artboard's second line). */
174
+ --plate-band-bg: #131313;
175
+ --plate-band-fg: #c9c9c5;
176
+ --plate-band-fg-dim: #6a6a64;
177
+ /* Always-dark tooltip surface — overlaid on light + dark pages. */
178
+ --tooltip-bg: #3a3a3a;
179
+ /* The stage a wheel render sits on inside the product cards — the shared
180
+ components/WheelCardFrame (Product Grid + Alloy Wheels Listing) and the
181
+ /wheels CatalogCard. This is the DARK value; light mode overrides it in
182
+ the theme-derived light block below.
183
+ 🔁 REVERSAL (66035, 2026-08-09) — do NOT "fix" this back. Between
184
+ 1.25.1 and now this token was deliberately always-black, so the render
185
+ sat on one consistent backdrop in both themes. The owner has reversed
186
+ that: in light mode the stage now FOLLOWS THE LIGHT THEME (cream) and
187
+ only dark mode keeps the black. Dark mode is unchanged. */
188
+ --wheel-stage: #0a0a0a;
189
+ /* Radial gradient backing the Subscribe popup's left panel. The popup
190
+ now follows the page theme, so the DARK value lives here and the cream
191
+ value is overridden in the [data-theme="light"] block below. Warm
192
+ near-black tones echo the cream panel's warmth; the gold rings +
193
+ crosshatch layered on top read stronger against this dark ground. */
194
+ --subscribe-panel-bg: radial-gradient(80% 100% at 50% 40%, #221d15 0%, #151210 65%, #0a0908 100%);
195
+ /* ─── Font roles ───────────────────────────────────────────────
196
+ The swappable faces behind the font utilities below. Each defaults
197
+ to the site's built-in next/font face, so an un-themed page is
198
+ unchanged. The editor's "Fonts" control (Website settings)
199
+ overrides these tokens on the SITE-WIDE accent carrier — see
200
+ fontRoleStyle() in lib/fonts.ts and root.render in lib/puck/config
201
+ — so picking a font re-sets every section AND the header/footer
202
+ chrome around them without touching the loaded @font-face vars.
203
+ Published on <main> alone until card 8824, which is why a themed
204
+ site used to render in three typefaces. Theme-independent (no
205
+ light override). */
206
+ --font-role-heading: var(--font-display);
207
+ /* Heading 2 and 3 are styles of their own (Site styles lists six peers), and
208
+ their default IS Heading 1's face: unset means "the same as Heading 1", so a
209
+ site that has never opened those rows keeps one typeface across all three.
210
+ `fontRoleStyle` overrides these on the carrier when a face is chosen. */
211
+ --font-role-heading2: var(--font-role-heading);
212
+ --font-role-heading3: var(--font-role-heading);
213
+ --font-role-body: var(--font-body);
214
+ --font-role-accent: var(--font-serif);
215
+ /* The small-caps eyebrow/label face behind the `font-mono` utility below.
216
+ Defaults to the built-in JetBrains Mono, swappable via the Appearance
217
+ panel's "Labels" font (fontRoleStyle → --font-role-label). */
218
+ --font-role-label: var(--font-mono-loaded);
219
+ /* The OS-monospace tail that sits BEHIND whichever face the label role names.
220
+ Split out of --font-mono so a rule can put the LIVE --font-role-label in
221
+ front of it (see .mono-eyebrow) without restating eight family names.
222
+ Deliberately contains no var() of its own: a custom property's var() is
223
+ substituted where the property is DECLARED, so anything referencing
224
+ --font-role-label from here would freeze to the :root face — which is the
225
+ whole reason --font-mono cannot be used for this (card 8824). */
226
+ --font-mono-fallback: ui-monospace, "SF Mono", "Cascadia Mono", "Cascadia Code",
227
+ "Consolas", "DejaVu Sans Mono", Menlo, monospace;
228
+ }
229
+
230
+ /* Light theme — applied when <html data-theme="light"> */
231
+ [data-theme="light"] {
232
+ /* Light-mode surfaces + ink. Only the BASES flip here — the live tokens
233
+ inherit the `var(<base>)` mapping declared at :root, so every surface has
234
+ exactly one light value and one dark value, in one place each. */
235
+ --bg-base: #f5f1e8;
236
+ --surface-base: #ffffff;
237
+ --surface-2-base: #ebe6d9;
238
+ --fg-base: #0a0a0a;
239
+ --fg-dim-base: #6b6357;
240
+ --line-base: #d8d2c2;
241
+ /* Only the BASES flip here — the live tokens inherit the `var(<base>)`
242
+ mapping declared at :root. This pair is what an UN-themed light page uses,
243
+ and what a themed one falls back to when its preset carries no light
244
+ colour. */
245
+ --accent-base: #8a6a2a;
246
+ --accent-2-base: #c9a461;
247
+ /* accentInk() for the light-page accent above — WHITE, not the :root black.
248
+ #8a6a2a scores 4.18:1 under black and 5.03:1 under white, so the ink flips
249
+ with the accent even between two shades of the house gold. */
250
+ --accent-ink-base: #ffffff;
251
+ /* Same role as the dark-theme --green, but darkened for the cream
252
+ bg. The dark-theme #7aa672 reads as pastel-bright on cream — this
253
+ #4a6b42 is the same hue, lower lightness. */
254
+ --green: #4a6b42;
255
+ /* Switch OS chrome back to light when the user toggles light mode. */
256
+ color-scheme: light;
257
+ /* Light-mode card overrides — preserve the original behaviour the
258
+ light theme shipped with: card body = --site-surface (the white surface
259
+ the design used pre-cleanup), hover = --site-surface-2 (the warm tan
260
+ step). The dark-mode lifted-grey treatment does NOT apply here.
261
+ The journal panel reverts to --site-surface (its original cream-white
262
+ fill).
263
+ The catalog grid container uses --bg in BOTH themes now — the
264
+ original --site-line fill was painting a visible warm-tan rectangle
265
+ in the bottom-right empty grid cell on partial last rows (e.g.
266
+ Monoblock Cast = 7 wheels in 4 cols leaves 1 empty track). With
267
+ --bg, that empty cell now matches the page background. The
268
+ side-effect is that the inter-card hairline dividers (line color
269
+ painting through the 2px gap) are gone in light mode too, but
270
+ the white card surface (--site-surface) on the cream page bg already
271
+ gives cards clear visual separation.
272
+ These five recipes, and the light --tooltip-bg, live in the
273
+ theme-derived block below for the reason given there. */
274
+ /* Muted accent for the fitment-diagram outline — slightly deeper than
275
+ the light-theme --accent (#8a6a2a) so the silhouette still reads
276
+ against the cream page background. */
277
+ --accent-dim: #7a5d23;
278
+ /* Cream radial gradient backing the Subscribe popup's left panel in
279
+ light mode — the original design value (dark override lives in :root). */
280
+ --subscribe-panel-bg: radial-gradient(80% 100% at 50% 40%, #f5edd8 0%, #ebe6d9 65%, #ddd5c0 100%);
281
+ }
282
+
283
+ /* ─── Theme-derived surface tokens ──────────────────────────────────────
284
+ The card / panel / section fills, which are all recipes over --site-surface-2,
285
+ --site-surface, --bg and --fg rather than colours of their own.
286
+
287
+ Declared BOTH at the root and on `.sf-theme-surfaces` — the element that
288
+ carries the merchant's palette (<body> on the live site, the preview wrapper
289
+ in the editor). This is not a second copy: a custom property has its var()s
290
+ substituted where it is DECLARED, so a recipe declared only at :root would
291
+ bake in the built-in surfaces and quietly ignore the theme. Re-declaring the
292
+ same recipes on the themed element is what lets a merchant's surface colour
293
+ actually reach the cards, the journal panel and the section bands.
294
+
295
+ The dark block must stay ABOVE the light one: `:root` and
296
+ `[data-theme="light"]` have equal specificity, so source order decides. */
297
+ :root,
298
+ .sf-theme-surfaces {
299
+ --card-bg: color-mix(in oklab, var(--site-surface-2) 92%, var(--fg));
300
+ --card-bg-hover: color-mix(in oklab, var(--site-surface-2) 82%, var(--fg));
301
+ --journal-panel-bg: var(--bg);
302
+ --catalog-grid-bg: var(--bg);
303
+ /* ─── Section-level "highlighted" background ────────────────
304
+ Used by alternating sections across the contact / about / fitment
305
+ / PDP pages to break up long dark-on-dark stretches with a tonal
306
+ band. Resolves to --card-bg (medium-grey ~#2d2d2c) in dark mode.
307
+ Tweak in this one place to update every section at once. */
308
+ --section-bg: var(--card-bg);
309
+ }
310
+
311
+ [data-theme="light"],
312
+ [data-theme="light"] .sf-theme-surfaces {
313
+ --card-bg: var(--site-surface);
314
+ --card-bg-hover: var(--site-surface-2);
315
+ --journal-panel-bg: var(--site-surface);
316
+ --catalog-grid-bg: var(--bg);
317
+ /* Light mode skips the gold-band — sections fall back to the cream
318
+ page bg so the dark-mode gold treatment doesn't leak through. */
319
+ --section-bg: var(--bg);
320
+ /* Tooltip surface follows the cream palette in light mode (was the
321
+ always-dark #3a3a3a, which read as a hard black highlight when it
322
+ popped over a hovered fitment row). Uses --site-surface so the tooltip
323
+ contrasts with the row's --site-surface-2 hover state. */
324
+ --tooltip-bg: var(--site-surface);
325
+ /* The wheel image well follows the light theme instead of staying black
326
+ (66035). --bg, not --site-surface-2: --site-surface-2 is exactly --card-bg-hover
327
+ in light, so a surface-2 stage would vanish into the card the moment
328
+ the shopper hovered it. --bg keeps the well readable in BOTH the rest
329
+ and hover states, and matches what the COMPACT ProductCard variant has
330
+ always used for its own image well (`bg-bg`), so the two wells finally
331
+ agree. Lives in the theme-derived block, not the plain light block,
332
+ because it is a var() recipe: declared only at :root it would bake in
333
+ the built-in cream and ignore the merchant's palette. */
334
+ --wheel-stage: var(--bg);
335
+ }
336
+
337
+ /* Colour-Theme content text. themeColorStyle() sets --theme-fg on the page
338
+ CONTENT wrapper (the live <main> in LayoutChrome, the editor preview body in
339
+ lib/puck/config) — the merchant's per-site text colour. Mapped onto --fg in
340
+ EVERY mode: a theme is one flat palette (66238), so the preset states one ink
341
+ and it paints wherever the theme paints.
342
+
343
+ It used to map in DARK mode only, with a parallel light-page ink token,
344
+ because --theme-fg was dark-mode ink and mapping it unconditionally put
345
+ cream text on the cream light page — the bug where the Product Grid header
346
+ vanished. A flat preset carries its background as well as its ink, so the two
347
+ travel together and there is no mode left for them to disagree across.
348
+
349
+ The chrome (Header / Footer) sits OUTSIDE this wrapper and keeps the plain
350
+ light/dark --fg. */
351
+ .sf-themed-content {
352
+ --fg: var(--theme-fg, var(--fg-base));
353
+ }
354
+
355
+ /* Colour-Theme SURFACES. themeSurfaceStyle() sets --theme-bg / --theme-surface /
356
+ --theme-surface-2 / --theme-line / --theme-muted on the element carrying this
357
+ class: <body> on the live site, the preview wrapper inside the editor.
358
+
359
+ Why <body> and not the accent carrier: <body> is the element that PAINTS the
360
+ page canvas (`bg-bg`), and the carrier is one of its children — custom
361
+ properties inherit downwards, so a background set on the carrier could never
362
+ reach the canvas behind the content. The values are server-computed from the
363
+ site's theme row and identical on both renders, so they add no hydration
364
+ surface beyond the --sf-header-h length var already set there.
365
+
366
+ The mapping reads --theme-*, exactly as --theme-fg does, and applies in
367
+ EVERY mode. Each fallback is the token's own base, which is why an un-themed
368
+ site — and any preset stored before these colours existed — resolves to
369
+ precisely the values it uses today.
370
+
371
+ There used to be a SECOND set of names for light pages, and two routes that
372
+ emitted them (a per-surface light colour, card 22014; a whole-palette opt-in,
373
+ card 11039). Both existed because a preset's surfaces were dark-mode colours
374
+ and the light page had to be protected from them — a near-black background
375
+ under the light theme's near-black ink is a site nobody can read. A theme is
376
+ one flat palette now (66238): the preset states its ink and its background
377
+ together, so they are readable or unreadable as a pair and there is no half
378
+ to protect the other from. One set of names, one rule. */
379
+ .sf-theme-surfaces {
380
+ --bg: var(--theme-bg, var(--bg-base));
381
+ --site-surface: var(--theme-surface, var(--surface-base));
382
+ --site-surface-2: var(--theme-surface-2, var(--surface-2-base));
383
+ --site-line: var(--theme-line, var(--line-base));
384
+ --fg-dim: var(--theme-muted, var(--fg-dim-base));
385
+ /* THE INK TRAVELS WITH THE BACKGROUND, and it has to be on THIS element.
386
+ Lewis, 2026-09-08: on SHARPEN he scrolled past the hero and the header text
387
+ was white on a white bar. Measured on the live page, header solid, at rest:
388
+ a light preset in dark mode gave a bar of #f4f4f6 under ink of #f5f1e8 —
389
+ dLum 3. The mirror broke too: a dark preset in light mode, #0a0a0a on
390
+ #2e2e2e, dLum 36.
391
+
392
+ Both had one cause. This element is <body>, so everything above maps the
393
+ merchant's SURFACES onto the whole page, chrome included. The merchant's
394
+ INK was mapped only on `.sf-themed-content` (<main>) below, and the Header
395
+ and Footer are that element's SIBLINGS, not its children. So the chrome
396
+ took its background from the palette and its ink from whichever light/dark
397
+ mode was on, and the two halves were free to disagree.
398
+
399
+ A theme is one flat palette (66238): the preset states its background and
400
+ its ink together, so they are readable or unreadable AS A PAIR. That is
401
+ only true while both halves reach the same elements. Declaring it here is
402
+ what makes it true for the chrome.
403
+
404
+ ⚠️ This is the line the old "chrome keeps the plain light/dark --fg" rule
405
+ used to be, and that rule was SAFE ONLY WHILE THE SURFACES WERE DARK-MODE
406
+ ONLY. 66238 made them apply in every mode; the protection went with it and
407
+ nobody noticed, because the comments around here still said "dark mode
408
+ only" for another year. They are corrected now — do not restore the split.
409
+ `scripts/test-chrome-theme-pair.ts` §2 fails if one half ever travels
410
+ without the other.
411
+
412
+ `.sf-themed-content` still re-declares --fg on <main>. That is NOT dead:
413
+ a custom property has its var()s substituted where it is DECLARED, so the
414
+ value resolved here freezes against THIS element's --theme-fg. Re-declaring
415
+ it down there is what lets a section pin its own ink and have it resolve
416
+ against the section's value instead of the page's. */
417
+ --fg: var(--theme-fg, var(--fg-base));
418
+ }
419
+ /* Colour-Theme ACCENT — the merchant's theme colour.
420
+ themeAccentStyle() sets --theme-accent (+ its companion shade) on the element
421
+ carrying this class: the boxless accent carrier in app/layout.tsx on the live
422
+ site, the preview wrapper inside the editor.
423
+
424
+ Why the indirection rather than setting --accent inline: a custom property set
425
+ in a style attribute beats every stylesheet rule on that same element, so an
426
+ inline --accent could never be overridden — and the per-SECTION accent
427
+ override below has to be able to win. The colour arrives as data and lets CSS
428
+ pick, the same reason --theme-fg and the themed surfaces exist.
429
+
430
+ It used to carry a second colour for light pages, with a two-step fallback to
431
+ the dark one. A theme is one flat palette now (66238): one accent, one rule. */
432
+ .sf-theme-accent {
433
+ --accent: var(--theme-accent, var(--accent-base));
434
+ --accent-2: var(--theme-accent-2, var(--accent-2-base));
435
+ --accent-ink: var(--theme-accent-ink, var(--accent-ink-base));
436
+ --btn: var(--theme-btn, var(--accent));
437
+ --btn-2: var(--theme-btn-2, var(--accent-2));
438
+ --btn-ink: var(--theme-btn-ink, var(--accent-ink));
439
+ }
440
+
441
+ /* ── A SECTION'S THEME PAINTS IT (card ss.section-theme-picker) ─────────────
442
+ Lewis, 2026-09-03: he set a Box to Theme 6, a blue preset, and nothing moved.
443
+ Nothing could — only a preset's ACCENT was ever published per preset, so the
444
+ wrapper could reach one of six colours, and an empty Box draws nothing that
445
+ uses an accent. `ThemeColors.background` had always said what this should be:
446
+ "THE PAGE when the page is on this theme, THE SECTION when a section is."
447
+
448
+ IT USED TO SIT BEFORE THE SURFACE CLASSES AND THE POSITION WAS LOAD-BEARING:
449
+ `.sf-section-surface-*` and `.sf-section-accent` were both (0,1,0), so source
450
+ order decided --bg between them, and this block after them cost a pinned
451
+ accent bar its fill (`scripts/test-accent-colour-parity` caught it). The
452
+ surface classes were deleted on 2026-09-04, so there is nothing left at equal
453
+ specificity to order against — but do not move this block on the strength of
454
+ that: a section's theme is what paints everything, and a new class in this
455
+ family would bring the whole question back.
456
+
457
+ The fallbacks live here rather than in the emitted style attribute so the
458
+ wrapper stays a pure reference with no hex in it (DD's rule, 2026-08-25). */
459
+ /* ⚠️ TWO SELECTORS, ONE COPY OF THE MAPPING — the map/paint split the
460
+ `[ADD] The Header Can Take Its Own Space, And Pick A Theme` card owed an
461
+ answer for, and it is a SELECTOR LIST rather than a second block on purpose.
462
+
463
+ `.sf-section-accent` both MAPS a preset's tokens onto --bg/--fg/… and PAINTS
464
+ with them (`background: var(--bg); color: var(--fg)`, at the end of this
465
+ block). A SECTION wants both. The HEADER wants only the first half: its bar
466
+ must stay see-through at the top of a `Shared` page, so a rule that painted
467
+ unconditionally would fill it in. Copying the nine references into a second
468
+ block would be the duplication DD's rule and card 66152 both exist to stop —
469
+ a copy is cheap the day it is made and permanent afterwards.
470
+
471
+ So the mapping is declared ONCE, for both, and only the paint is
472
+ `.sf-section-accent`'s alone below. `scripts/test-header-theme.ts` §3 fails if
473
+ the header class ever picks up a `background` or `color` here. */
474
+ .sf-section-accent,
475
+ .sf-header-theme {
476
+ /* ⛔ THE SURFACES BELOW ARE CARD ss.section-theme-picker, AND WITHOUT THEM THE
477
+ CONTROL DOES ALMOST NOTHING. Lewis, 2026-09-03: he set a Box to Theme 6, a
478
+ blue preset, and nothing moved. Only the accent triple above existed, so a
479
+ section could reach one of a preset's six colours — and an empty Box uses no
480
+ accent at all, which is why every theme looked identical on it.
481
+
482
+ `ThemeColors.background` has always said what this should be: "THE PAGE when
483
+ the page is on this theme, THE SECTION when a section is." This is the
484
+ section half, finally wired.
485
+
486
+ THE FALLBACKS ARE IN THE REFERENCE, NOT HERE — `sectionAccentVars` builds
487
+ each `--sf-sec-*` as `var(--theme-preset-<i>-x, var(--theme-x, <base>))`, so
488
+ a stale preset index lands on the page's own token rather than on nothing. A
489
+ bare `var(--sf-sec-bg)` here would be the cycle-free half of that chain; the
490
+ second value is kept only for a wrapper written before this card. */
491
+ --bg: var(--sf-sec-bg, var(--theme-bg, #0a0a0a));
492
+ --fg: var(--sf-sec-fg, var(--theme-fg, #f5f1e8));
493
+ --site-surface: var(--sf-sec-surface, var(--theme-surface, #131313));
494
+ --site-surface-2: var(--sf-sec-surface-2, var(--theme-surface-2, #1c1c1c));
495
+ --site-line: var(--sf-sec-line, var(--theme-line, #2a2a2a));
496
+ --fg-dim: var(--sf-sec-muted, var(--theme-muted, #9a9388));
497
+ /* The derived fills that read --bg / --site-surface-2, restated because a custom
498
+ property resolves where it is DECLARED: the :root copies froze against the
499
+ PAGE's tokens and would keep the page's card colour inside a themed band.
500
+ Same rule, same reason, as --btn above. */
501
+ --card-bg: color-mix(in oklab, var(--site-surface-2) 92%, var(--fg));
502
+ --card-bg-hover: color-mix(in oklab, var(--site-surface-2) 82%, var(--fg));
503
+ --journal-panel-bg: var(--bg);
504
+ --catalog-grid-bg: var(--bg);
505
+ --section-bg: var(--card-bg);
506
+ }
507
+ /* THE PAINT HALF, and a SECTION's alone. The header takes the mapping above and
508
+ paints from its own branches instead: `<Header/>` sets background and colour
509
+ inline once the bar is solid, and the `data-top-space="own"` rule further up
510
+ paints a page that takes its own space. Both read the very --bg / --fg the
511
+ list above just re-pointed, which is how picking a theme moves the bar without
512
+ this declaration ever reaching it. */
513
+ .sf-section-accent {
514
+ background: var(--bg);
515
+ color: var(--fg);
516
+ }
517
+
518
+ /* ⛔ THERE ARE NO `.sf-section-surface-*` CLASSES ANY MORE, AND THAT IS A
519
+ DECISION — do not write a new one here.
520
+ Card 22018 gave a SECTION a pinned dark / light / accent surface;
521
+ ss.section-theme-picker took that away on 2026-09-03 (a section is on a THEME
522
+ and nothing else), leaving the HEADER as the only element that wore one; and
523
+ the header's own Background control went on 2026-09-04 (Lewis: the header
524
+ follows the theme). With no element left to wear them, the three surface
525
+ blocks, their three `.sf-section-accent` pairings and the four
526
+ `--accent-*-base-on-{dark,light}` tokens they alone read were removed.
527
+ NOTHING ON ANY SITE MOVED: all 9 stored header `surface` values were empty.
528
+ Bringing one back means giving some element two colour rules again, with no
529
+ panel able to say which of them paints. */
530
+
531
+
532
+ /* Per-SECTION accent. The wrapper stores only a preset INDEX and emits
533
+ --sf-sec-accent* as `var(--theme-preset-<i>-accent…)` REFERENCES into the
534
+ per-preset accents the carrier publishes (themePresetAccentVars in
535
+ lib/theme.ts) — no copied colour anywhere, so editing the palette re-paints
536
+ every overridden section. A stale index resolves nothing and every chain
537
+ below falls back to the page accent: degraded, readable, never garbage.
538
+
539
+ Mode: the section follows the PAGE's mode, always. There used to be a second
540
+ case — a PINNED surface picked the accent's mode itself, through three
541
+ `.sf-section-surface-*.sf-section-accent` rules sitting last at (0,2,0). Those
542
+ went with the surface classes on 2026-09-04; nothing pins a mode now, so this
543
+ one rule is the whole story. */
544
+ /* The header joins this one too: a themed bar's buttons and links must come out
545
+ of the SAME preset as its background, or the bar is half on one theme. Pure
546
+ mapping, no paint, so there is nothing here to keep off the header. */
547
+ .sf-section-accent,
548
+ .sf-header-theme {
549
+ --accent: var(--sf-sec-accent, var(--accent-base));
550
+ --accent-2: var(--sf-sec-accent-2, var(--accent-2-base));
551
+ --accent-ink: var(--sf-sec-accent-ink, var(--accent-ink-base));
552
+ --btn: var(--theme-btn, var(--accent));
553
+ --btn-2: var(--theme-btn-2, var(--accent-2));
554
+ --btn-ink: var(--theme-btn-ink, var(--accent-ink));
555
+ }
556
+
557
+
558
+ /* ─── Corners ──────────────────────────────────────────────────────────
559
+ The Appearance panel's site-wide "Corners" control drives the site's radius
560
+ scale (cornerStyleTokens in lib/theme.ts sets --site-radius-* on the theme
561
+ carrier), which reaches every `rounded-sm|md|lg|xl|2xl` on the site because
562
+ the site's `inline` theme block (with the named colours, further down)
563
+ compiles those to `border-radius: var(--site-radius-<size>)`.
564
+
565
+ ⚠️ NOT TAILWIND'S OWN `--radius-*`. Until 2026-10-11 the setting overrode
566
+ Tailwind's scale directly, and efficient-shell's UI kit reads that same scale
567
+ for its chrome — so a site at 0 squared the kit's dialogs wherever the two
568
+ shared a page (see the note on the design tokens above). `inline` makes each
569
+ utility name the site's property itself, so nothing the site draws reads
570
+ Tailwind's `--radius-*` any more. The values on :root are Tailwind 4.3's own
571
+ scale, which is what the utilities drew before; the bare `rounded` keeps its
572
+ quarter-rem the same way.
573
+
574
+ The bare `rounded` utility does NOT follow the scale: it compiles to a literal
575
+ `0.25rem`, so it would sit out the setting and leave a site with square cards
576
+ and rounded inputs. Re-point it at a variable whose default is that same
577
+ 0.25rem — the utility keeps its exact value until the merchant chooses
578
+ otherwise.
579
+
580
+ `rounded-full` is deliberately NOT re-pointed. It compiles to
581
+ `calc(infinity * 1px)` and is what makes a thing CIRCULAR — toggle knobs,
582
+ status dots, finish swatches, the wheel placeholder's rings. A circle is a
583
+ shape, not a corner, and squaring those would be a regression with no upside;
584
+ the design that motivated this setting has no rounded panels but still has
585
+ round dots. */
586
+ :root {
587
+ --site-radius: 0.25rem;
588
+ --site-radius-xs: 0.125rem;
589
+ --site-radius-sm: 0.25rem;
590
+ --site-radius-md: 0.375rem;
591
+ --site-radius-lg: 0.5rem;
592
+ --site-radius-xl: 0.75rem;
593
+ --site-radius-2xl: 1rem;
594
+ --site-radius-3xl: 1.5rem;
595
+ --site-radius-4xl: 2rem;
596
+ }
597
+ @layer utilities {
598
+ .rounded {
599
+ border-radius: var(--site-radius);
600
+ }
601
+ }
602
+
603
+ /* The BUTTON's corners, and the one place the `Corners` setting can reach them.
604
+ Card `[FIX] The Corners Setting Says Buttons And Skips Them`.
605
+
606
+ The same hole as `.sf-card` further down, found from the other end. `Corners`
607
+ reaches an element by re-pointing the `--site-radius-*` scale above, which only
608
+ works if that element writes a `rounded-*` utility — and `components/Btn.tsx`,
609
+ the shared button every section draws, writes NONE. So the row's hint said
610
+ "Applies to panels, cards, inputs and buttons" while a button was square at 0,
611
+ at 9 and at 40. `--sf-button-radius` is the property `cornerRadiusTokens`
612
+ publishes for buttons, at half the card's ratio — the amount the inputs and
613
+ panels beside it already take — and it is UNSET at the identity, so the `0`
614
+ here is exactly what a button draws today: square, on every site that never
615
+ opened the panel. No migration, and the hint becomes true.
616
+
617
+ ⚠️ IT LIVES HERE, WITH `Corners`, AND NOT IN THE CARD-LOOK BLOCK BELOW. It was
618
+ written there first and `scripts/test-card-style-settings.tsx` §1a caught it:
619
+ every rule between those two markers must be scoped to `.sf-card` /
620
+ `.sf-card-grid`, because a card-look rule that is not would reach a colour
621
+ swatch or a toggle knob (Lewis, 2026-09-01). This is not a card-look rule —
622
+ it is a corner rule — and the guard reading its neighbourhood by position is
623
+ the reason position matters.
624
+
625
+ ⚠️ NO `overflow: clip` HERE, unlike `.sf-card`. A button's only children are
626
+ its label and its arrow glyph, and the arrow TRANSLATES 3px right on hover —
627
+ clipping the box would shave that arrow at the moment it moves. Nothing in a
628
+ button bleeds past its border, so there is nothing to clip. */
629
+ .sf-button { border-radius: var(--sf-button-radius, 0); }
630
+
631
+ /* ─── Stacking layers ──────────────────────────────────────────────────
632
+ The site's layers above the page, by NAME (SSOT-12), in steps of 100 (the
633
+ owner's layer rule, 2026-10-12). Markup takes a layer as `z-(--sf-layer-…)`.
634
+
635
+ `--sf-layer-lightbox` is every lightbox (components/Lightbox.tsx). Its value
636
+ is the `z-[100]` the three lightboxes wrote by hand, so nothing re-orders
637
+ against them. It is above the fixed header (`z-50`), the drawers (60 / 61)
638
+ and the dialogs (90) — but only because the lightbox is portalled out of its
639
+ section: a layer orders things inside ONE stacking context, and the owner
640
+ photos' dialog, drawn inside a `relative z-[2]` section, sat under the header
641
+ at any number (hartesmetal.com, 2026-10-15).
642
+
643
+ ⏳ The other site layers are still numbers in their components (the header
644
+ and its menu 40 / 50, drawers and menus 60 / 61, dialogs 90, the select list
645
+ 200). They move here, onto steps of 100, as their own change. */
646
+ :root {
647
+ --sf-layer-lightbox: 100;
648
+ }
649
+
650
+ /* ─── Floating launchers step aside for an overlay ─────────────────────
651
+ A floating launcher (`data-sf-floating`: the chat bubble and its panel, the
652
+ classic one and the assistant) is fixed to the bottom-right corner above
653
+ the page. A blocking overlay — the phone menu, the cart drawer, search, the
654
+ footer menu, a popup — marks <html> `data-sf-overlay` while it is open
655
+ (components/useModalDismiss.ts), and under that mark the launcher is hidden:
656
+ in the phone menu the bubble sat on the Region button and on "Your account"
657
+ (inovit.com.au at 390, 2026-10-17, card [FIX] Region Picker On Phone).
658
+ A rule rather than a layer number: it holds whatever the overlay's own
659
+ z-index, and the chat comes back, as it was, when the overlay closes.
660
+ Guard: scripts/test-phone-menu-region-picker.tsx §3-§4. */
661
+ :root[data-sf-overlay] [data-sf-floating] { visibility: hidden; }
662
+
663
+ /* ─── Card look (card 66332) ───────────────────────────────────────────
664
+ Site styles ▸ Basics ▸ Card shadow / Shadow colour / Card gap / Hover —
665
+ three site-level settings beside `Corners`, for the reason Corners is one
666
+ decision: a card appears in a product grid and a filter cell,
667
+ and one set per section is one idea with seven answers.
668
+
669
+ ⚠️ EVERY RULE HERE IS SCOPED TO `.sf-card` OR `.sf-card-grid`, AND THAT IS
670
+ THE EXEMPTION. Lewis, 2026-09-01: corners, shadow and hover must not leak
671
+ onto swatches, switches and status dots. `Corners` exempts them structurally
672
+ — `rounded-full` compiles to a literal that reads no token, so the radius
673
+ override cannot reach it (see the Corners block above) — and this is that
674
+ same rule in the only vocabulary a shadow can use: a thing that does not
675
+ carry the class is out of reach by construction, not by a list of exceptions
676
+ somebody has to maintain. `scripts/test-card-style-settings.tsx` §1 holds
677
+ both halves — the rules stay scoped, and no round control wears the class.
678
+
679
+ ⚠️ EVERY `:hover` CARRIES THE SAME `:active`. Lewis, 2026-09-01: a phone has
680
+ no hover, so the hover effect IS the press feedback. A rule with no `:active`
681
+ is a setting a shopper on a phone can never see, and it looks correct on
682
+ every desktop it is tested on. §2 keeps it that way. (The selector lists are
683
+ written on ONE line each because that guard reads a rule's whole selector;
684
+ splitting them over two lines would let a `:hover` sneak past it.)
685
+
686
+ ⚠️ NO SELECTOR NAMES A PAGE, A SECTION OR A FILTER. A filter cell is a card
687
+ and takes these values automatically — it gets no settings of its own
688
+ (settled). That is only true while these rules key off the site carrier and
689
+ the class alone; a page or filter-bar ancestor here would produce a card that
690
+ follows the site in one place and not in another. §3.
691
+
692
+ UNLAYERED, like the heading-compression rules below: a card writes its hover
693
+ as a Tailwind `hover:bg-[…]` utility inside `@layer utilities`, and a custom
694
+ property cannot override a declaration that does not read it. An unlayered
695
+ rule can.
696
+
697
+ An untouched site carries NO `data-sf-card-*` attribute (the identity value
698
+ of each setting emits none — cardShadowAttr / cardGapAttr / hoverEffectAttr
699
+ in lib/theme.ts), so not one rule below can match and the site renders
700
+ byte-identically. */
701
+
702
+ /* The shadow colour reaches the shadows through `--sf-shadow-color`, whose
703
+ BUILT-IN VALUE LIVES HERE rather than in the carrier: a site that never
704
+ touched the colour then publishes no declaration at all, the way `small`
705
+ corners publish none. Lewis's default, 2026-09-01. */
706
+ [data-sf-card-shadow="soft"] { --sf-card-shadow: 0 1px 2px color-mix(in srgb, var(--sf-shadow-color, #141416) 7%, transparent), 0 4px 12px -4px color-mix(in srgb, var(--sf-shadow-color, #141416) 10%, transparent); }
707
+ [data-sf-card-shadow="lifted"] { --sf-card-shadow: 0 2px 4px color-mix(in srgb, var(--sf-shadow-color, #141416) 6%, transparent), 0 14px 30px -10px color-mix(in srgb, var(--sf-shadow-color, #141416) 22%, transparent); }
708
+
709
+ /* The raised state a `lift` hover moves to, and the distance it moves. Both
710
+ read the merchant's shadow colour for the reason the resting shadow does —
711
+ a black lift on a near-black page is a setting that visibly does nothing. */
712
+ [data-sf-card-hover="lift"] { --sf-card-shadow-hover: 0 6px 10px color-mix(in srgb, var(--sf-shadow-color, #141416) 8%, transparent), 0 20px 40px -14px color-mix(in srgb, var(--sf-shadow-color, #141416) 26%, transparent); --sf-card-lift: translateY(-3px); }
713
+
714
+ /* The gutter between cards. A LENGTH rather than an attribute, because the
715
+ wheels listing SUBTRACTS its gutter when working out how wide one of N
716
+ columns would be (lib/wheels/plp-grid) — a visual gap and an arithmetic gap
717
+ that were two values would drift, which is the one thing that file exists to
718
+ prevent. Root-relative, like the 1rem it replaces: the gutter has always
719
+ scaled with the visitor's own browser text size, and a px figure here would
720
+ shrink it for anyone who raised theirs. */
721
+ [data-sf-card-gap="tight"] { --sf-card-gap: 0.5rem; }
722
+ [data-sf-card-gap="roomy"] { --sf-card-gap: 1.625rem; }
723
+
724
+ /* The resting card, and the state it moves to. Both read a custom property that
725
+ is UNSET on an untouched site, so both fall back to what a card draws today —
726
+ no shadow, and no transform. */
727
+ /* The card's CORNERS, and the one place the `Corners` setting can reach them.
728
+ A card writes no `rounded-*` utility — not one of the four card components
729
+ does — so overriding the `--site-radius-*` scale moved buttons and panels
730
+ and left every card square, whatever the panel said. `--sf-card-radius` is
731
+ the property `cornerStyleTokens` publishes for cards alone, and it is UNSET
732
+ at the identity, so the `0` here is what an untouched site keeps: square,
733
+ which is what a card draws today. No migration, and `Round` works.
734
+
735
+ `overflow: clip` rather than `hidden`: a rounded card whose photo runs to its
736
+ own edge (Latest Posts, the journal index) would otherwise wear a square
737
+ image over a round corner. `clip` does that without making a scroll
738
+ container, so the catalog card keeps its `sticky` behaviour. Nothing bleeds
739
+ outside a card today — no negative offset in any of the four — so at the
740
+ identity this clips nothing and the site renders byte-identically. */
741
+ /* ⚠️ THE RADIUS IS A CHAIN, READ CARD FIRST (`[ADD] A Card Sets Its Own
742
+ Corners`): this card's own answer, then the section's, then the site's, then
743
+ square. `--sf-card-box-radius` is published ONLY by a card that has been given
744
+ a number of its own, on a `display: contents` carrier the injection pass in
745
+ lib/puck/config.tsx wraps it in — so a card nobody has touched declares
746
+ nothing, falls straight through to `--sf-card-radius` and renders exactly as
747
+ it did before this rule grew a level. lib/content/card-corners.ts says why the
748
+ card tier needs a name of its own rather than overriding the inherited token.
749
+ The final `0` is unchanged and is still today's corner for a site that has set
750
+ none. */
751
+ .sf-card { box-shadow: var(--sf-card-shadow, none); border-radius: var(--sf-card-box-radius, var(--sf-card-radius, 0)); overflow: clip; }
752
+
753
+ /* The carrier that publishes it. `display: contents` generates no box at all, so
754
+ a card inside a grid stays the grid item and the markup gains nothing that can
755
+ be laid out — the same reason `.sf-section-corners` is `display: contents` one
756
+ tier up. Declared here rather than left to the inline style alone so no class
757
+ is rendered that the stylesheet never styles. */
758
+ .sf-card-corners { display: contents; }
759
+
760
+ /* The PICTURE inside a card, and the one place a section's `Card image` setting
761
+ can reach it. The card's own corner (`--sf-card-radius`, above) rounds the
762
+ card's OUTER edge and, through `overflow: clip`, any picture that runs all the
763
+ way to that edge — Latest Posts and the journal index both do. It cannot reach
764
+ an INSET picture, and that is the card Lewis actually saw on 2026-09-04: every
765
+ wheel card pads its image well by 5–30px, so the photo sits inside the card
766
+ and stays square whatever `Corners` says.
767
+
768
+ Hence a second property, NOT a second setting: `--sf-card-image-radius` is
769
+ published per SECTION by `cardImageShapeTokens` (lib/content/card-image-shape.ts)
770
+ and inherits down to the pictures in that section alone. Site-wide was the
771
+ first proposal and Lewis rejected it — 「有些是要分开的, 有些是要文字在图片上的」
772
+ — because one value cannot let the Collections card keep its overlay caption
773
+ while a product grid rounds its photos.
774
+
775
+ `0` is the fallback, so a section that has chosen nothing declares nothing and
776
+ the picture keeps the square cut it draws today. No migration, and the whole
777
+ setting is provably inert until used.
778
+
779
+ `overflow: clip` for the same reason `.sf-card` uses it rather than `hidden`:
780
+ these wells hold absolutely-positioned siblings (the badge, the gradient, the
781
+ hover crossfade) and `clip` rounds them off without making a scroll container,
782
+ so the catalog card keeps its `sticky` behaviour. */
783
+ .sf-card-image { border-radius: var(--sf-card-image-radius, 0); overflow: clip; }
784
+ .sf-card:hover, .sf-card:active { box-shadow: var(--sf-card-shadow-hover, var(--sf-card-shadow, none)); transform: var(--sf-card-lift, none); }
785
+
786
+ /* Only a site that has chosen gets the extra transitions. `transition-property`
787
+ ALONE — the duration and the easing stay with the card's own
788
+ `transition-colors duration-200|300` utility, so a card keeps the timing it
789
+ has always had and only gains two more properties to animate. */
790
+ [data-sf-card-shadow] .sf-card, [data-sf-card-hover] .sf-card { transition-property: color, background-color, border-color, box-shadow, transform; }
791
+
792
+ /* `None` and `Lift` must also STOP the fill lightening every card already does
793
+ — a row that says the hover is None while the card still changes colour is a
794
+ row that lies. `Highlight` is the name of that existing effect, so it emits
795
+ no attribute and matches nothing here. `var(--card-bg)` resolves on the CARD,
796
+ which is what lets a card inside a themed section band keep that band's own
797
+ surface instead of the page's. */
798
+ /* ⚠️ THIS ONE RULE IS KEYED ON `.sf-card-fill`, NOT `.sf-card`, AND THAT IS WHAT
799
+ LETS EVERY SECTION BE A CARD. Lewis, 2026-09-03: "所有的 section 也都可以加入
800
+ corner. 但是默认是直角的." — every section takes Corners, and square stays the
801
+ default.
802
+
803
+ The blocker was this rule. It cancels a card's own fill-lightening by setting
804
+ the card back to `var(--card-bg)`, which is right for a card that lightens and
805
+ WRONG for one that does not: a section drawing `bg-surface` would jump to a
806
+ different colour the moment a merchant chose "Hover: None", the one option
807
+ whose whole job is to hold the colour still. Measured before this change: of
808
+ the 36 unmarked card sections, exactly ZERO lightened their own card fill —
809
+ every `hover:bg-*` in them was on a BUTTON inside the section.
810
+
811
+ So the vocabulary splits, one grammar with one modifier rather than two
812
+ grammars:
813
+ `.sf-card` a card SURFACE — corners, shadow, lift, gap. Colour-free,
814
+ so wearing it is safe for any section and is one line.
815
+ `.sf-card-fill` …and this card lightens its fill on hover, so the None and
816
+ Lift options have something to cancel. Worn only by the
817
+ cards that actually do it.
818
+ `scripts/test-card-style-settings.tsx` §8 holds both halves. */
819
+ [data-sf-card-hover="none"] .sf-card-fill:hover, [data-sf-card-hover="none"] .sf-card-fill:active, [data-sf-card-hover="lift"] .sf-card-fill:hover, [data-sf-card-hover="lift"] .sf-card-fill:active { background-color: var(--card-bg); }
820
+
821
+ /* ⚠️ THE RULE THAT USED TO LIVE HERE IS GONE — [ADD] Gap Inherits From Site.
822
+ It read `[data-sf-card-gap] .sf-card-grid { gap: var(--sf-card-gap); }`, and it
823
+ is removed rather than left beside the new one because it would WIN: its
824
+ specificity is (0,2,0) against `.sf-gap-zone`'s (0,1,0), so on precisely the
825
+ sites that stored `tight` or `roomy` it would have pinned the old gutter and
826
+ the site's new Block gap would have done nothing — on those sites only, which
827
+ is the hardest kind of bug to see.
828
+
829
+ The two words themselves are NOT retired: `--sf-card-gap` is still defined
830
+ above, and every card grid now reads it as the MIDDLE rung of
831
+ `var(--sf-block-gap, var(--sf-card-gap, <own built-in>))`. So a site that
832
+ stored one keeps drawing it, and keeps drawing it ONLY on the card grids it
833
+ ever governed — see cardGridGapProps for why that middle rung has to exist. */
834
+
835
+ /* The brand plate is not a card — it is the strip laid over the lower quarter of
836
+ one — but it follows the card's fill, so `None` and `Lift` have to stop its
837
+ lightening too or the card stops changing colour and the plate inside it does
838
+ not. Same pair of states, same reason. */
839
+ [data-sf-card-hover="none"] .sf-card:hover .sf-card-plate, [data-sf-card-hover="none"] .sf-card:active .sf-card-plate, [data-sf-card-hover="lift"] .sf-card:hover .sf-card-plate, [data-sf-card-hover="lift"] .sf-card:active .sf-card-plate { background-color: var(--card-bg); }
840
+
841
+ /* ─── End card look (card 66332) ─────────────────────────────────────── */
842
+
843
+ /* ─── Heading compression ──────────────────────────────────────────────
844
+ The Appearance panel's "Headings" compression rung, applied to real headings
845
+ (h1–h4) under the theme carrier. Delivered as a data attribute rather than a
846
+ custom property because every heading in the codebase writes its own
847
+ `leading-*` / `tracking-*` utility, and a variable cannot override a
848
+ declaration that does not read it. These rules are UNLAYERED, so they win
849
+ over Tailwind's `@layer utilities` regardless of specificity.
850
+
851
+ `normal` emits no attribute at all, so neither rule below can match and the
852
+ default is byte-identical to today — the reason this is an attribute the
853
+ carrier omits rather than one it always sets.
854
+
855
+ Metrics are the display-type values from the Apex design system: closed
856
+ leading with slightly negative tracking.
857
+
858
+ ⚠️ THE TOP RUNG USED TO ADD `text-transform: uppercase`, AND THAT WAS THE BUG
859
+ CARD 66162 REMOVED. The Appearance panel also has a `Capitals` setting, which
860
+ emits `--sf-heading-case` and is read by a Tailwind arbitrary utility inside
861
+ `@layer utilities`. Unlayered beats layered whatever the specificity, so these
862
+ rules won every time: a merchant who chose "As typed" was shown their choice
863
+ selected and served capitals anyway. Measured on production before the fix
864
+ (inovit.com.au/wheels, the real `div.sf-theme-accent` carrier, a real <h2>):
865
+
866
+ Spacing=Normal + Capitals=As typed -> text-transform: none obeyed
867
+ Spacing=Compressed + Capitals=As typed -> text-transform: uppercase IGNORED
868
+ Spacing=Tight + Capitals=As typed -> text-transform: none obeyed
869
+
870
+ Capitalisation is not a spacing metric. `Capitals` is now the only thing on
871
+ the platform that writes a heading's case, which is what lets these two rungs
872
+ be split into a line gap and a text gap without either of them lying. DO NOT
873
+ ADD IT BACK: scripts/test-text-styles-panel.tsx §2 reads this very block and
874
+ fails on any `text-transform` in it, and it fails on a case in
875
+ lib/theme's `compressionMetrics` too, because the panel can tell the same lie
876
+ from the other end. A design that wants capitals has `Capitals: As designed`,
877
+ which is the default and is what every site is already on. */
878
+ [data-sf-headings="tight"] :is(h1, h2, h3, h4) {
879
+ line-height: 0.95;
880
+ letter-spacing: -0.01em;
881
+ }
882
+ [data-sf-headings="compressed"] :is(h1, h2, h3, h4) {
883
+ line-height: 0.86;
884
+ letter-spacing: -0.01em;
885
+ }
886
+
887
+ /* A part's own PARAGRAPH spacing on the editor canvas (plan S2). The cell
888
+ writes `--sf-part-para` on the part and flags it with `data-sf-part-para`; an
889
+ inline style cannot reach the part's line breaks, so these rules do. The gap
890
+ goes after each line except the last, list rows too; a blank line keeps its
891
+ height. The published page gets the same three rules from `paragraphRules`
892
+ in lib/puck/part-styles.ts — keep the two the same. A part without the flag
893
+ is untouched. */
894
+ [data-sf-part-para] br:not(br + br) {
895
+ display: block;
896
+ content: "";
897
+ margin-top: var(--sf-part-para);
898
+ }
899
+ [data-sf-part-para] :is([data-sf-bullet], [data-sf-number]):not(:first-child) {
900
+ margin-top: var(--sf-part-para);
901
+ }
902
+ [data-sf-part-para] :is([data-sf-bullet], [data-sf-number]):not(:last-child) {
903
+ margin-bottom: var(--sf-part-para);
904
+ }
905
+
906
+ /* ─── Header at-top (transparent) text colour ─────────────────────────
907
+ Before it goes solid on scroll, the fixed site Header paints its nav in
908
+ --sf-header-overlay-fg. Default = the PALETTE's ink, in both modes.
909
+
910
+ ⚠️ IT USED TO BE THE MODE'S INK — cream `--overlay-fg` in dark, `--fg` in
911
+ light — and that split is the one the solid bar lost on 2026-09-08 (see
912
+ `.sf-theme-surfaces`). A theme is one flat palette (66238): its background
913
+ reaches the page in every mode, so a light palette in dark mode put cream
914
+ nav on a white page. Found 2026-10-07 on INOVIT /journal, and on SHARPEN's
915
+ 404, where Next's error shell never runs the theme boot script, so even a
916
+ light-mode site has no `data-theme="light"` there. A page with no marker
917
+ leaves the body canvas under the bar, and the palette's ink is what reads on
918
+ it. `--fg` on <body> IS that ink; an unthemed site resolves it to the mode
919
+ base exactly as before (dark `--fg-base` is the same #f5f1e8 as
920
+ `--overlay-fg`). `scripts/test-chrome-theme-pair.ts` §6 holds it.
921
+
922
+ AUTO. A page publishes the ink its FIRST section DECLARES it needs, as
923
+ `data-lead-ink` on the page body (lib/puck/header-ink, read by both
924
+ StorefrontPageView and the editor preview). Cream over a band the section
925
+ itself paints cream on — a `dark` or `accent` surface, or a full-bleed hero
926
+ with its own dark legibility gradient — and nothing at all otherwise, so a
927
+ light content page keeps its near-black ink and cream-on-cream stays
928
+ impossible. A DECLARATION, never a sampled colour: see the module.
929
+
930
+ This replaced a hand-maintained list of route names plus a "does this page
931
+ open with a hero?" marker. Neither asked whether the thing under the header
932
+ was dark, so a dark section that was not a hero got ink-on-ink and no control
933
+ could fix it (card 66216).
934
+
935
+ `:has()` lets the header — a body child rendered ABOVE the page content —
936
+ react to a marker set DEEP inside the page body, so it stays pure CSS:
937
+ resolved at first paint, re-resolved on client navigation, with no boot
938
+ script and no React state driving the colour.
939
+
940
+ EVERY INK HAS ITS GROUND. Each rule that sets --sf-header-overlay-fg also
941
+ sets --sf-header-overlay-ground: the colour that ink was picked to sit ON
942
+ (cream ink -> near-black ground, near-black ink -> cream, --fg -> --bg). The
943
+ see-through bar has no background of its own, so this pair is the only
944
+ place its "background colour" exists. The cart badge draws its number in
945
+ it (components/HeaderTools, via --sf-header-ground, which Header and
946
+ BoxHeaderBar set inline). Add an ink rule without its ground and
947
+ scripts/test-cart-count-badge.tsx fails. */
948
+ .sf-site-header {
949
+ --sf-header-overlay-fg: var(--fg);
950
+ --sf-header-overlay-ground: var(--bg);
951
+ }
952
+ body:has([data-lead-ink="light"]) .sf-site-header {
953
+ --sf-header-overlay-fg: var(--overlay-fg);
954
+ --sf-header-overlay-ground: var(--overlay-fg-on-light);
955
+ }
956
+ body:has([data-lead-ink="dark"]) .sf-site-header {
957
+ --sf-header-overlay-fg: var(--overlay-fg-on-light);
958
+ --sf-header-overlay-ground: var(--overlay-fg);
959
+ }
960
+ /* THE MERCHANT'S OVERRIDE — Header → Look → "Text colour at the top". Auto
961
+ reads 99% of pages; this is the merchant taking over on the one it reads
962
+ wrong, so it has to beat Auto in BOTH directions, including the direction
963
+ Auto is confident about.
964
+
965
+ The leading `body ` is there for SPECIFICITY alone, and it is load-bearing:
966
+ the Auto rules above score (0,2,1) — one attribute inside :has(), one class,
967
+ one type — so a bare `.sf-site-header[data-header-ink]` at (0,2,0) would lose
968
+ to them and the control would silently do nothing.
969
+ `scripts/test-header-lead-ink.ts` §3b computes both and pins the winner. */
970
+ body .sf-site-header[data-header-ink="light"] {
971
+ --sf-header-overlay-fg: var(--overlay-fg);
972
+ --sf-header-overlay-ground: var(--overlay-fg-on-light);
973
+ }
974
+ body .sf-site-header[data-header-ink="dark"] {
975
+ --sf-header-overlay-fg: var(--overlay-fg-on-light);
976
+ --sf-header-overlay-ground: var(--overlay-fg);
977
+ }
978
+
979
+ /* ─── THE BOX HEADER ON A PHONE ───────────────────────────────────────
980
+ Blueprint step H.5, as the approved Box header mockup draws it. Under 801px
981
+ the row holding the site header's one-row Menu shows the logo at the left
982
+ and the rest together at the right, the menu button last; Search and the
983
+ region picker move into the menu's panel (components/PhoneMenuTools).
984
+
985
+ Only a row in the site header (`[data-section-group="header"]`) that holds a
986
+ one-row Menu (`[data-menu-row]`, components/FooterMenu). A page's Boxes, and
987
+ a header without such a Menu, keep their own `Columns on phone`.
988
+
989
+ ⚠️ UNLAYERED ON PURPOSE. A Box's phone cells are Tailwind utilities
990
+ (lib/content/box-layout `PHONE_ROW_CLASS`) in `@layer utilities`, and
991
+ unlayered CSS beats layered whatever the specificity. */
992
+ @media (max-width: 800px) {
993
+ /* The row, and the rows inside it: one line, each block its own width. A
994
+ block is the row's own child on the site and sits inside the editor's
995
+ frame (`[data-puck-component]`) in the editor, so each rule reads both. */
996
+ [data-section-group="header"] .flex-row:has([data-menu-row]),
997
+ [data-section-group="header"] .flex-row:has([data-menu-row]) .flex-row {
998
+ flex-direction: row;
999
+ flex-wrap: nowrap;
1000
+ }
1001
+ [data-section-group="header"] .flex-row:has([data-menu-row]) > *,
1002
+ [data-section-group="header"] .flex-row:has([data-menu-row]) .flex-row > * {
1003
+ flex: 0 0 auto;
1004
+ /* A Box's frame is `w-full`; here it is as wide as what it holds. */
1005
+ width: auto;
1006
+ min-width: 0;
1007
+ }
1008
+ /* The menu button last, beside the cart. */
1009
+ [data-section-group="header"] .flex-row:has([data-menu-row]) > :is([data-menu-row], :has([data-menu-row])) {
1010
+ order: 99;
1011
+ }
1012
+ /* The first block (the logo) takes the room left over, and is the query
1013
+ container a long store name shrinks to fit: components/Logo
1014
+ `fitToContainer`, which the Logo block sets in the site header. */
1015
+ [data-section-group="header"] .flex-row:has([data-menu-row]) > :first-child:not([data-menu-row], :has([data-menu-row])) {
1016
+ flex: 1 1 0;
1017
+ container-type: inline-size;
1018
+ overflow-x: clip;
1019
+ }
1020
+ /* Search and the region picker are in the menu's panel instead: the block,
1021
+ and in the editor the frame around it, so no gap is left. Only the frame
1022
+ whose block IS the tool, never a Box frame that merely holds one. */
1023
+ [data-section-group="header"]:has([data-menu-row]) [data-phone-menu-tool],
1024
+ [data-section-group="header"]:has([data-menu-row]) [data-puck-component]:has(> * > [data-phone-menu-tool]) {
1025
+ display: none;
1026
+ }
1027
+ }
1028
+
1029
+ /* ─── THE BOX HEADER AT MID WIDTHS ────────────────────────────────────
1030
+ 801px to 1500px: TWO ROWS, as the old header draws them from 801px
1031
+ (components/Header, its medium top bar). The logo and the tools on the top
1032
+ line, the one-row Menu's links on a second line under a hairline. Ruled by
1033
+ Victor Mau 2026-10-01: the approved mockup draws only 1440px (one row) and
1034
+ 390px (the phone bar above), and a row that ran out of room folded the
1035
+ links onto two lines inside it. Same scope as the phone rules: only the
1036
+ site header's row that holds a one-row Menu. Unlayered for the same reason.
1037
+ The gap and the hairline are today's header's own: `gap-3`, `border-line/60`
1038
+ and `pt-2.5`.
1039
+ NOT for a group on `Same row` (the Site Header's Menu row, the old header's
1040
+ `menu_row: inline`, lib/content/header-bar): its links stay on the logo's
1041
+ row at every width above a phone.
1042
+ UP TO 1500px, NOT 1100px: Victor's rule ended at 1100px (the approved
1043
+ mockup is one row at 1440px). Lewis, 2026-10-12, card [ADD] Every Old
1044
+ Header Setting To The Box Header: `Own row` stacks exactly as the old
1045
+ header did, to 1500px, so a switched site looks the same at every width. */
1046
+ @media (min-width: 801px) and (max-width: 1500px) {
1047
+ [data-section-group="header"]:not([data-header-menu-row="inline"]) .flex-row:has([data-menu-row]) {
1048
+ flex-wrap: wrap;
1049
+ row-gap: 12px;
1050
+ }
1051
+ /* The links: their own line, last, under the hairline. */
1052
+ [data-section-group="header"]:not([data-header-menu-row="inline"]) .flex-row:has([data-menu-row]) > :is([data-menu-row], :has([data-menu-row])) {
1053
+ order: 99;
1054
+ flex: 1 0 100%;
1055
+ border-top: 1px solid color-mix(in oklab, var(--site-line) 60%, transparent);
1056
+ padding-top: 10px;
1057
+ }
1058
+ /* The logo takes the top line's room, so the tools sit at its end. */
1059
+ [data-section-group="header"]:not([data-header-menu-row="inline"]) .flex-row:has([data-menu-row]) > :first-child:not([data-menu-row], :has([data-menu-row])) {
1060
+ flex: 1 1 0;
1061
+ container-type: inline-size;
1062
+ overflow-x: clip;
1063
+ }
1064
+ }
1065
+
1066
+ /* ─── THE BOX HEADER'S LETTERS ────────────────────────────────────────
1067
+ The Site Header's `Letters` (the old header's `nav_text_case`,
1068
+ lib/content/header-bar): ONE value for the menu's links AND the tool words,
1069
+ which sit in one bar — Lewis asked for them controlled together
1070
+ (2026-09-03). Uppercase, the default, is the menu's own capitals and 0.18em
1071
+ tracking, worn by the tool words too, so a bar is never half capitalised.
1072
+ Capitalised takes both off and lets the words read as they are written —
1073
+ a removed transform, never `capitalize`, which would rewrite a merchant's
1074
+ `FusionForge`. The phone panel's links are inside the menu, so they follow.
1075
+ Unlayered: the menu's capitals are Tailwind utilities in `@layer
1076
+ utilities`, and unlayered CSS beats layered whatever the specificity. */
1077
+ [data-section-group="header"] .mono-eyebrow-ctl {
1078
+ /* The menu's own type (components/FooterMenu, its row: `font-mono`
1079
+ `text-[13px]`), so a word tool reads as part of the same strip, as the
1080
+ old bar's tools did. An icon tool draws no text, so nothing moves. */
1081
+ font-family: var(--font-role-label), var(--font-mono-fallback);
1082
+ font-size: 13px;
1083
+ text-transform: uppercase;
1084
+ letter-spacing: 0.18em;
1085
+ }
1086
+ [data-section-group="header"][data-header-nav-case="capitalised"] [data-menu-row] ul,
1087
+ [data-section-group="header"][data-header-nav-case="capitalised"] .mono-eyebrow-ctl {
1088
+ text-transform: none;
1089
+ letter-spacing: normal;
1090
+ }
1091
+
1092
+ /* ─── THE BOX HEADER'S MENU ALIGNMENT ─────────────────────────────────
1093
+ Where the old header put its menu, as the page Menu's `Link alignment` and
1094
+ `Show links` (lib/content/menu-row; Lewis 2026-10-12, Q2 and Q4 option B).
1095
+ The bar stays ONE FLAT ROW — Logo, Menu, tools — so the phone and mid-width
1096
+ rules above keep working; these only place the menu where the bar is one
1097
+ row, and step aside where it stacks.
1098
+
1099
+ One row (801px up): a Menu with `Link alignment` takes the bar's free space
1100
+ and puts its links at its start, centre or end. `Center` gives the cells
1101
+ either side equal shares, so the links sit on the bar's centre (the old
1102
+ 1fr auto 1fr). A link outside a Menu's `Show links` range is hidden.
1103
+
1104
+ A LEAD part — a ranged Menu that stops before the last link, left of a
1105
+ centred logo — makes the bar three cells: the lead and the cell after the
1106
+ logo share the bar equally, the logo keeps its width. Where the bar stacks
1107
+ (a phone, and a laptop on Own row) the lead steps aside and the Menu after
1108
+ the logo shows EVERY link, its Box opened (`display: contents`) so its Menu
1109
+ and tools are the bar's own: one menu, one button, as the old header.
1110
+
1111
+ Unlayered, like the rules above, and after them where they meet: the
1112
+ mid-width rules are more specific and win the cells they place. */
1113
+ @media (min-width: 801px) {
1114
+ [data-section-group="header"] .flex-row > :is([data-menu-align], :has([data-menu-align])) {
1115
+ flex: 1 1 0%;
1116
+ min-width: 0;
1117
+ }
1118
+ /* Start and End fill the cell they grow into; a centred menu is as wide as
1119
+ its links, so the links sit on the bar's centre. */
1120
+ [data-section-group="header"] [data-menu-row]:is([data-menu-align="start"], [data-menu-align="end"]) {
1121
+ width: 100%;
1122
+ }
1123
+ [data-section-group="header"] [data-menu-row][data-menu-align="start"] > ul {
1124
+ justify-content: flex-start;
1125
+ }
1126
+ [data-section-group="header"] [data-menu-row][data-menu-align="center"] > ul {
1127
+ justify-content: center;
1128
+ }
1129
+ [data-section-group="header"] [data-menu-row][data-menu-align="end"] > ul {
1130
+ justify-content: flex-end;
1131
+ }
1132
+ /* Centred links: the cells either side share the rest equally. */
1133
+ /* NOT `min-width: 0`: a side cell never shrinks below what it holds, as
1134
+ the old grid's `1fr` columns did not, so a wide logo pushes the links
1135
+ over rather than running under them. */
1136
+ [data-section-group="header"] .flex-row:has([data-menu-align="center"]) > :not([data-menu-align]):not(:has([data-menu-align])) {
1137
+ flex: 1 1 0%;
1138
+ }
1139
+ [data-section-group="header"] .flex-row > :is([data-menu-align="center"], :has([data-menu-align="center"])) {
1140
+ flex: 0 1 auto;
1141
+ }
1142
+ [data-section-group="header"] [data-menu-row] li[data-menu-out] {
1143
+ display: none;
1144
+ }
1145
+ /* A centred logo: the cell after the lead part keeps its own width. */
1146
+ [data-section-group="header"] .flex-row > :is([data-menu-part="lead"], :has([data-menu-part="lead"])) + * {
1147
+ flex: 0 0 auto;
1148
+ }
1149
+ }
1150
+ @media (max-width: 800px) {
1151
+ [data-section-group="header"] .flex-row > :is([data-menu-part="lead"], :has([data-menu-part="lead"])) {
1152
+ display: none;
1153
+ }
1154
+ [data-section-group="header"] .flex-row > :is([data-menu-part="lead"], :has([data-menu-part="lead"])) + * {
1155
+ flex: 1 1 0;
1156
+ container-type: inline-size;
1157
+ overflow-x: clip;
1158
+ }
1159
+ }
1160
+ @media (min-width: 801px) and (max-width: 1500px) {
1161
+ /* Own row stacks: the links start their own line, as the old second row. */
1162
+ [data-section-group="header"]:not([data-header-menu-row="inline"]) [data-menu-row][data-menu-align] > ul {
1163
+ justify-content: flex-start;
1164
+ }
1165
+ [data-section-group="header"]:not([data-header-menu-row="inline"]) [data-menu-row] li[data-menu-out] {
1166
+ display: list-item;
1167
+ }
1168
+ [data-section-group="header"]:not([data-header-menu-row="inline"]) .flex-row > :is([data-menu-part="lead"], :has([data-menu-part="lead"])) {
1169
+ display: none;
1170
+ }
1171
+ [data-section-group="header"]:not([data-header-menu-row="inline"]) .flex-row > :is([data-menu-part="lead"], :has([data-menu-part="lead"])) + * {
1172
+ flex: 1 1 0;
1173
+ container-type: inline-size;
1174
+ overflow-x: clip;
1175
+ }
1176
+ /* The old stacked bar drew Search FIRST among its tools, whatever its
1177
+ Search position (components/Header, the medium band); the position only
1178
+ ever applied to the one-row bar. The logo stays ahead of it where a
1179
+ centred logo's Box is opened below. */
1180
+ [data-section-group="header"]:not([data-header-menu-row="inline"]) [data-phone-menu-tool="search"] > *,
1181
+ [data-section-group="header"]:not([data-header-menu-row="inline"]) [data-puck-component]:has(> * > [data-phone-menu-tool="search"]) {
1182
+ order: -1;
1183
+ }
1184
+ [data-section-group="header"]:not([data-header-menu-row="inline"]) .flex-row > :is([data-menu-part="lead"], :has([data-menu-part="lead"])) + * {
1185
+ order: -2;
1186
+ }
1187
+ [data-section-group="header"]:not([data-header-menu-row="inline"]) .flex-row:has([data-menu-part="lead"]) > :has([data-menu-row]):not([data-menu-row]):not(:has([data-menu-part="lead"])),
1188
+ [data-section-group="header"]:not([data-header-menu-row="inline"]) .flex-row:has([data-menu-part="lead"]) > :has([data-menu-row]):not([data-menu-row]):not(:has([data-menu-part="lead"])) :has([data-menu-row]):not([data-menu-row]) {
1189
+ display: contents;
1190
+ }
1191
+ }
1192
+
1193
+ /* ─── Header TOP SPACE ────────────────────────────────────────────────
1194
+ The page's own "Top space" (Page settings → `Top space`; the resolver is
1195
+ lib/puck/header-clearance). A page that takes its own space publishes
1196
+ `data-top-space="own"` on its body, and the bar above it paints a real
1197
+ background from the first pixel instead of being see-through.
1198
+
1199
+ ⚠️ THIS IS THE MAP/PAINT SPLIT THIS CARD OWED AN ANSWER FOR, and the answer
1200
+ is that the header does NOT wear `.sf-section-accent`. That class both MAPS
1201
+ the theme tokens and PAINTS with them (`background: var(--bg)` at its end),
1202
+ and a bar that must stay see-through at the top of most pages cannot wear a
1203
+ rule that paints unconditionally. So the MAPPING stays where it already is —
1204
+ the theme tokens the header inherits from the carrier — and the PAINT is
1205
+ these three declarations, which apply only to a page that asked for them.
1206
+
1207
+ WHY THE INK GOES THROUGH THE VARIABLE. <Header/> sets `color:
1208
+ var(--sf-header-overlay-fg)` INLINE while it is not solid, and an inline
1209
+ style cannot be beaten from a stylesheet. Re-pointing the variable — the same
1210
+ indirection the Auto/override rules above already use — lands the theme ink
1211
+ without a specificity fight and without touching the component.
1212
+
1213
+ The border is the one declaration that IS a fight: the bar carries Tailwind's
1214
+ `border-transparent` at (0,1,0) while not solid, and this selector scores
1215
+ (0,2,1) — `body` type + the attribute inside `:has()` + the class. That is
1216
+ the same arithmetic `scripts/test-header-lead-ink.ts` §3b pins for the ink
1217
+ override, and `scripts/test-header-top-space.ts` pins it for this rule.
1218
+
1219
+ ON SCROLL NOTHING HERE APPLIES ANY MORE and that is correct: <Header/> goes
1220
+ solid and sets background and colour inline, which beat this block. Solid is
1221
+ solid either way, so the bar does not change appearance as the reader
1222
+ scrolls — it was already painted. */
1223
+ body:has([data-top-space="own"]) .sf-site-header {
1224
+ --sf-header-overlay-fg: var(--fg);
1225
+ --sf-header-overlay-ground: var(--bg);
1226
+ background: var(--bg);
1227
+ border-bottom-color: var(--site-line);
1228
+ backdrop-filter: blur(12px);
1229
+ }
1230
+
1231
+ /* ─── The page's WIDTH reaching the chrome ────────────────────────────────
1232
+ A page set to `Full width` publishes `data-page-width="full"` on its own
1233
+ wrapper, and the bar above it plus the footer below it stop capping — so
1234
+ the three sit on one column, or run edge to edge, together.
1235
+
1236
+ ⚠️ WHY `:has()` AND NOT A VARIABLE INHERITED DOWN THE TREE. The header is a
1237
+ SIBLING of the page body, not an ancestor of it — measured in the browser on
1238
+ 2026-09-09: a custom property published on the page's wrapper resolves to
1239
+ `none` inside the header, every time. The carrier that DOES wrap all three
1240
+ (`.sf-theme-accent`) is rendered in app/layout.tsx, and a Next layout cannot
1241
+ read the pathname (see the note above `isEditorShellRequest`), so it cannot
1242
+ know which page it is wrapping. `:has()` is the one thing that reaches back
1243
+ up and across, and card 66514 already proved it here for the header's ink.
1244
+
1245
+ ⚠️ AND WHY IT RE-POINTS A VARIABLE RATHER THAN SETTING `max-width`. The cap is
1246
+ an INLINE style (`siteWidthCapStyle`), which no stylesheet can beat. Setting
1247
+ the variable it reads is not a specificity trick, it is the only way in.
1248
+
1249
+ `none` rather than a wider number: `Full width` means no cap at all, and a
1250
+ number would be a second opinion about how wide "no cap" is.
1251
+
1252
+ ⛔ A BAR THAT ANSWERED FOR ITSELF IS NOT LISTENING. Header and Footer each
1253
+ have their own Width row; when one is set to `Content width` the component
1254
+ writes the cap literally instead of through the variable, so this rule
1255
+ cannot move it. That is the three-level rule made mechanical: the level that
1256
+ spoke wins, and silence means follow. */
1257
+ body:has([data-page-width="full"]) .sf-site-header,
1258
+ body:has([data-page-width="full"]) footer {
1259
+ --sf-chrome-cap: none;
1260
+ }
1261
+
1262
+ @theme inline {
1263
+ --color-bg: var(--bg);
1264
+ --color-surface: var(--site-surface);
1265
+ --color-surface-2: var(--site-surface-2);
1266
+ --color-fg: var(--fg);
1267
+ --color-fg-dim: var(--fg-dim);
1268
+ --color-line: var(--site-line);
1269
+ --color-accent: var(--accent);
1270
+ --color-accent-2: var(--accent-2);
1271
+ --color-accent-ink: var(--accent-ink);
1272
+ /* The button's own fill/ink trio (card 66504), so `bg-btn` / `text-btn-ink`
1273
+ compile. Each resolves to the accent unless a theme carries a button
1274
+ colour — the chain is in `.sf-theme-accent` and its siblings above. */
1275
+ --color-btn: var(--btn);
1276
+ --color-btn-2: var(--btn-2);
1277
+ --color-btn-ink: var(--btn-ink);
1278
+ --color-overlay-fg: var(--overlay-fg);
1279
+ --color-green: var(--green);
1280
+ --color-alert: var(--alert);
1281
+ --color-overlay-scrim: var(--overlay-scrim);
1282
+ --color-overlay-chrome: var(--overlay-chrome);
1283
+ --color-plate-band: var(--plate-band-bg);
1284
+ --color-plate-band-fg: var(--plate-band-fg);
1285
+ --color-plate-band-fg-dim: var(--plate-band-fg-dim);
1286
+
1287
+ /* font families — resolve through the swappable --font-role-* tokens
1288
+ (defined on :root above, default to the built-in faces) so the
1289
+ editor's Fonts control can remap them per site. */
1290
+ --font-display: var(--font-role-heading), "Inter", system-ui, sans-serif;
1291
+ /* Heading 2 and Heading 3 each get a utility of their own, so a section can
1292
+ DRAW the face those rows set. Until this existed they could not: every
1293
+ heading on the site wrote `font-display`, which resolves
1294
+ `--font-role-heading` — Heading 1's face — while its size token said
1295
+ `--size-headingMedium`. Measured on main before this change, 43 lines took
1296
+ their size from Heading 2 and their face from Heading 1, and 30 did the
1297
+ same for Heading 3, so BOTH Font controls in Site styles saved a value and
1298
+ moved zero pixels.
1299
+
1300
+ ⚠️ THE FALLBACK TAIL IS THE SAME AS `--font-display`'s ON PURPOSE. These
1301
+ are the same typographic slot at a different rank, not a different family,
1302
+ and a heading that fell back to a different stack while the webfont loaded
1303
+ would flash a face nobody chose.
1304
+
1305
+ ⚠️ SWAPPING `font-display` FOR THESE MOVES NO PIXELS TODAY.
1306
+ `--font-role-heading2` and `--font-role-heading3` are declared on :root as
1307
+ `var(--font-role-heading)`, and `normalizeFonts` resolves an unset
1308
+ heading2/heading3 back to Heading 1's key, so a site that has never opened
1309
+ those two rows renders exactly as it did. What changes is that opening
1310
+ them now does something. */
1311
+ --font-heading2: var(--font-role-heading2), "Inter", system-ui, sans-serif;
1312
+ --font-heading3: var(--font-role-heading3), "Inter", system-ui, sans-serif;
1313
+ --font-body: var(--font-role-body), system-ui, sans-serif;
1314
+ /* Fraunces (default) — editorial italic serif, used by the Journal
1315
+ page for gold-italic emphasis in headlines + pull-quotes. Falls
1316
+ back to Georgia so the cascade never bare-metals to Times New
1317
+ Roman. The accent role is swappable via the Fonts control. */
1318
+ --font-serif: var(--font-role-accent), Georgia, "Iowan Old Style", serif;
1319
+ /* Space Mono web font (loaded by next/font in layout.tsx) is the
1320
+ primary face — same family lineage as Space Grotesk, identical
1321
+ look on every OS. The OS-mono cascade sits behind it as graceful
1322
+ fallback while the webfont is loading or if it fails to load. */
1323
+ --font-mono: var(--font-role-label), var(--font-mono-fallback);
1324
+
1325
+ /* Tailwind's radius scale, pointed at the SITE'S corners — the `Corners`
1326
+ block above says why it is `--site-radius-*` and where the defaults live. */
1327
+ --radius-xs: var(--site-radius-xs);
1328
+ --radius-sm: var(--site-radius-sm);
1329
+ --radius-md: var(--site-radius-md);
1330
+ --radius-lg: var(--site-radius-lg);
1331
+ --radius-xl: var(--site-radius-xl);
1332
+ --radius-2xl: var(--site-radius-2xl);
1333
+ --radius-3xl: var(--site-radius-3xl);
1334
+ --radius-4xl: var(--site-radius-4xl);
1335
+ }
1336
+
1337
+ * {
1338
+ box-sizing: border-box;
1339
+ }
1340
+
1341
+ /* Layered — see the element-reset block below for why every bare-element
1342
+ rule in this file must sit inside `@layer base`. */
1343
+ @layer base {
1344
+ html,
1345
+ body {
1346
+ background: var(--bg);
1347
+ color: var(--fg);
1348
+ /* Body copy face — via the swappable body role (defaults to Inter). The
1349
+ editor's Fonts control re-declares font-family on the page wrapper so
1350
+ content re-resolves to the chosen face; this keeps chrome outside the
1351
+ wrapper (e.g. the header) on the default body face. */
1352
+ font-family: var(--font-role-body);
1353
+ -webkit-font-smoothing: antialiased;
1354
+ /* Theme switch should feel instant — the long 0.4s fade read as lag. Keep a
1355
+ hair of easing so it's not a hard cut. */
1356
+ transition: background 0.15s ease, color 0.15s ease;
1357
+ }
1358
+
1359
+ /* Smooth-scroll for in-page hash anchors (header nav: /wheels#fusionforge etc.) */
1360
+ html {
1361
+ scroll-behavior: smooth;
1362
+ }
1363
+ }
1364
+
1365
+ /* The header's DRAWN height in px — what the bar actually measures on screen,
1366
+ which is the merchant's "Header height" OR the bar's own content, whichever is
1367
+ taller. <Header/> publishes it from a ResizeObserver on the bar. The
1368
+ body-clearance, the section-anchor offset, the bars that pin under the header
1369
+ and every `calc(100svh - header)` all read it, so page content never slides
1370
+ under a taller — or floats below a shorter — bar.
1371
+
1372
+ ⚠️ NOT the slider value. That is `--sf-header-set-h` below, and only the bar's
1373
+ own min-height may read it. The two were one var until 2026-09-16, and a bar
1374
+ that had grown past the slider (a tall logo, or the stacked two-row layout
1375
+ between 801px and 1500px) hid the top of the page under itself by the
1376
+ difference. See lib/puck/header-clearance.ts.
1377
+
1378
+ Default = the historical fixed header height, so the first paint of a site
1379
+ whose content fits is byte-identical to what it has always been. Keep it in
1380
+ step with HEADER_HEIGHT_PX.default (lib/puck/header-clearance.ts). */
1381
+ :root {
1382
+ --sf-header-h: 88px;
1383
+ }
1384
+
1385
+ /* The merchant's CHOSEN header height — the Appearance → "Header height" slider,
1386
+ published per site by app/layout.tsx (on <body>) and by <Header/> (on <html>).
1387
+ Read by the bar's own `min-height` and by NOTHING else: a taller bar must still
1388
+ be allowed to grow, or the stacked two-row header would clip its own menu.
1389
+ Keep this default in step with HEADER_HEIGHT_PX.default. */
1390
+ :root {
1391
+ --sf-header-set-h: 88px;
1392
+ }
1393
+
1394
+ /* The site's content column, in px — how wide this whole website is. Default =
1395
+ the width every storefront rendered at while it was hard-coded; app/layout.tsx
1396
+ overrides it per site (on the page content wrapper) from the site's theme row,
1397
+ and every page-width cap reads it as `max-w-[var(--sf-site-width,1600px)]`. A
1398
+ site that has set no width publishes nothing, so the cap takes its own
1399
+ fallback and lands right back on this number. Keep this default in step with
1400
+ PAGE_WIDTH.default (lib/content/page-width.ts) — scripts/test-site-width.ts
1401
+ reads it out of this file and fails if the two drift. NOT the same value as
1402
+ the per-PAGE `--sf-page-width`, which the page builder's own Width control
1403
+ publishes lower down for the page it is on. */
1404
+ :root {
1405
+ --sf-site-width: 1600px;
1406
+ }
1407
+
1408
+ /* Offset a page body below the fixed Header (HEADER_CLEARANCE_CLASS). Reads the
1409
+ live header height so the gap tracks the merchant's chosen "Header height"
1410
+ instead of a fixed 88px; the fallback keeps SSR's first paint correct before
1411
+ <Header/>'s effect publishes the real value. */
1412
+ .sf-header-clearance {
1413
+ padding-top: var(--sf-header-h, 88px);
1414
+ }
1415
+
1416
+ /* Bars that stick directly under the site header (STICKY_UNDERHEADER_CLASS — the
1417
+ Product Grid section header, the Journal category bar, the document-tabs bar).
1418
+ Each pins at the live header height on its own (`top-[var(--sf-header-h,88px)]`);
1419
+ this rule handles WHEN it should pin. By default they pin while the page scrolls,
1420
+ which only reads right when the site header itself stays pinned. <Header/>
1421
+ publishes its sticky mode as `data-header-sticky` on <html>; when the header is
1422
+ set to scroll away ("none") or to hide on scroll-down ("on_scroll_up"), a bar
1423
+ pinned at the top would hang over the gap the departed/hidden header left — so
1424
+ un-pin it and let it scroll away with the content. Absent attribute (first paint,
1425
+ or the default "always") keeps the pin, so the common case is unchanged. */
1426
+ :root[data-header-sticky="none"] .sf-sticky-underheader,
1427
+ :root[data-header-sticky="on_scroll_up"] .sf-sticky-underheader {
1428
+ position: static;
1429
+ }
1430
+
1431
+ /* Stick to top — the pin any section or card can be given (efficient-web-builder
1432
+ lib/content/section-sticky.ts, card [ADD] Every Section Can Stick To The Top).
1433
+ The pass wraps a node that sticks in `.sf-section-stick` and publishes the
1434
+ distance below the header as `--sf-stick-distance` (and `-phone`); nothing
1435
+ is emitted for a node that does not stick, so these rules match nothing on
1436
+ a page that never used it.
1437
+
1438
+ It stops under the live header height, the same `--sf-header-h` the
1439
+ under-header bars use. When the header does not stay pinned — the header
1440
+ publishes `data-header-sticky` "none" or "on_scroll_up" on <html> — there
1441
+ is no header to stop under, so the stop is the screen top. */
1442
+ .sf-section-stick {
1443
+ position: sticky;
1444
+ top: calc(var(--sf-header-h, 88px) + var(--sf-stick-distance, 0px));
1445
+ }
1446
+ .sf-section-stick.sf-section-stick-desktop-off {
1447
+ position: static;
1448
+ }
1449
+ :root[data-header-sticky="none"] .sf-section-stick,
1450
+ :root[data-header-sticky="on_scroll_up"] .sf-section-stick {
1451
+ top: var(--sf-stick-distance, 0px);
1452
+ }
1453
+ /* What comes after a pinned node slides OVER it. A sticky box is positioned,
1454
+ and a positioned box paints above the plain boxes that follow it, so the
1455
+ next section would slide UNDER the pinned one. Giving the followers a
1456
+ position of their own puts them back in page order. Only siblings of a pin
1457
+ are touched, so a page with no pin is unchanged. `:where()` makes the rule
1458
+ weigh nothing: a follower that sets its own position (the Category bar's
1459
+ own `sticky`, an `absolute` layer) keeps it. */
1460
+ :where(.sf-section-stick ~ :not(.sf-section-stick)) {
1461
+ position: relative;
1462
+ }
1463
+ /* The phone's own answer (below 720px, VIEW_TABLET_MIN, written as in the
1464
+ phone padding block below). */
1465
+ @media not all and (min-width: 720px) {
1466
+ .sf-section-stick.sf-section-stick-phone-on {
1467
+ position: sticky;
1468
+ top: calc(var(--sf-header-h, 88px) + var(--sf-stick-distance-phone, 0px));
1469
+ }
1470
+ .sf-section-stick.sf-section-stick-phone-off {
1471
+ position: static;
1472
+ }
1473
+ :root[data-header-sticky="none"] .sf-section-stick.sf-section-stick-phone-on,
1474
+ :root[data-header-sticky="on_scroll_up"] .sf-section-stick.sf-section-stick-phone-on {
1475
+ top: var(--sf-stick-distance-phone, 0px);
1476
+ }
1477
+ }
1478
+
1479
+ /* Section height — the "Height" field on any Puck section (card 66215). The
1480
+ injected pass wraps a section that sets one in this class and publishes the
1481
+ length as a custom property; the property is what varies per section, so the
1482
+ rule itself lives here once.
1483
+
1484
+ MIN-height, never height, and no overflow anywhere in this block: content
1485
+ taller than the number the merchant chose makes the section GROW (the owner's
1486
+ ruling — clipping would hide a merchant's own words with no sign of it in the
1487
+ editor). `display:grid` on the wrapper (set inline, beside the property) is
1488
+ what makes the section STRETCH into the height rather than sit at the top of
1489
+ an empty box; a grid row still grows past its minimum when the content is
1490
+ taller, so stretching costs nothing at the clip end.
1491
+
1492
+ TWO DECLARATIONS, because one React style object cannot carry `min-height`
1493
+ twice. The first is in `vh` and every browser understands it. The second is in
1494
+ `svh` — the SMALL viewport height, which accounts for the mobile address bar
1495
+ the old `vh` ignores — and only browsers that have the unit enter the
1496
+ @supports block to take it. A section whose length carries no viewport unit at
1497
+ all (Fixed in px) publishes only the base property, so the inner var() names
1498
+ the base as its own fallback rather than resolving to nothing. */
1499
+ .sf-section-minh {
1500
+ min-height: var(--sf-section-minh);
1501
+ }
1502
+ @supports (min-height: 1svh) {
1503
+ .sf-section-minh {
1504
+ min-height: var(--sf-section-minh-s, var(--sf-section-minh));
1505
+ }
1506
+ }
1507
+
1508
+ /* Section anchors — the "Anchor" field on any Puck section. When set,
1509
+ the section renders an invisible, zero-height <span id> as its first child
1510
+ (see SectionAnchor in lib/puck/config.tsx). `display:block; height:0` makes it
1511
+ a real scroll target that adds no layout box; `scroll-margin-top` clears the
1512
+ fixed header — now the live --sf-header-h (fallback 88px) — so a
1513
+ /<page>#<anchor> deep-link lands with the section below the header, not under
1514
+ it — the same JS-free path as the /wheels collection anchors above. */
1515
+ .sf-section-anchor {
1516
+ display: block;
1517
+ height: 0;
1518
+ scroll-margin-top: var(--sf-header-h, 88px);
1519
+ }
1520
+
1521
+ /* Hero slider — never show the horizontal scroll-snap scrollbar. Covers the
1522
+ track itself AND any descendant, because in the page-builder Puck wraps the
1523
+ slot children, so the actual scroll container can be a nested element the
1524
+ utility classes on `.hero-track` don't reach. */
1525
+ .hero-track,
1526
+ .hero-track * {
1527
+ scrollbar-width: none; /* Firefox */
1528
+ -ms-overflow-style: none; /* legacy Edge */
1529
+ }
1530
+ .hero-track::-webkit-scrollbar,
1531
+ .hero-track *::-webkit-scrollbar {
1532
+ display: none; /* Chrome / Safari */
1533
+ width: 0;
1534
+ height: 0;
1535
+ }
1536
+
1537
+ /* ─── Element resets ─────────────────────────────────────────────────
1538
+ EVERY bare-element reset in this file lives inside `@layer base`, and
1539
+ new ones must too. An UNLAYERED rule beats every layered one no matter
1540
+ how specific, and Tailwind's utilities live in `@layer utilities` — so
1541
+ a top-level `a { color: inherit }` silently outranks `text-accent`,
1542
+ `underline`, `hover:text-bg`… on that element. The class is still in
1543
+ the markup, still in the generated CSS, and still does nothing: the
1544
+ failure is invisible until someone measures a computed colour.
1545
+
1546
+ That is not hypothetical. It cost <button> its font/background/border
1547
+ once (fixed by layering it, below), and it had swallowed the colour on
1548
+ every <a> in the storefront — 74 links across 44 files carrying a
1549
+ `text-*` / `underline` utility that never applied.
1550
+
1551
+ `scripts/test-css-layers.ts` fails the build if a bare-element reset
1552
+ reappears at top level. */
1553
+ @layer base {
1554
+ a {
1555
+ color: inherit;
1556
+ text-decoration: none;
1557
+ }
1558
+
1559
+ img {
1560
+ display: block;
1561
+ max-width: 100%;
1562
+ }
1563
+
1564
+ button {
1565
+ font: inherit;
1566
+ color: inherit;
1567
+ background: none;
1568
+ border: 0;
1569
+ cursor: pointer;
1570
+ }
1571
+
1572
+ /* A HEADING MAY ALWAYS SHRINK TO THE BOX IT IS IN, AND BREAK A WORD THAT IS
1573
+ WIDER THAN THAT BOX. (Card [FIX] Big Text Spills Out Of A Section.)
1574
+
1575
+ A flex item defaults to `min-width: auto`: it refuses to be narrower than
1576
+ its own longest WORD. That is invisible while a section draws a heading at
1577
+ the size it was designed at, and it is the whole defect the moment a
1578
+ merchant raises that size — which they can, from Site styles, from a part's
1579
+ own Text style, and (since the Booking Form's four text kinds) from a
1580
+ section's own control. Measured on the Booking Form, 2026-09-10: its step
1581
+ label is an `<h3>` beside a numbered circle in `flex items-center gap-3`,
1582
+ and pointed at Heading 1 it laid out 470px wide in a 384px row and pushed
1583
+ the page's scroll width from 992 to 1081. Nothing clipped it with
1584
+ `overflow` — the page simply grew wider than the viewport and the tail was
1585
+ off-screen, which reads to a merchant as "cut off at the card's edge".
1586
+
1587
+ ⚠️ WRAP AND GROW, NEVER CAP. Capping how far a run may grow would undo the
1588
+ owner's 2026-09-05 ruling ("following a style brings the size too") one
1589
+ section at a time. A cap already exists and is the merchant's to set —
1590
+ `PartStyle.width`, "how far the part may run, in px". This makes the
1591
+ DEFAULT honest: the words wrap, the section gets taller, nothing is lost.
1592
+
1593
+ ⚠️ BOTH DECLARATIONS OR NEITHER. Once the item may shrink, a single word
1594
+ wider than the box still runs out of it; `break-word` breaks a word only
1595
+ when it cannot fit a line of its own, so every heading that fits today is
1596
+ drawn exactly as it is today.
1597
+
1598
+ ⚠️ HEADINGS ONLY, AND THAT IS A SCOPE RATHER THAN AN OVERSIGHT. A blanket
1599
+ `.flex > * { min-width: 0 }` would reach 346 flex rows and let icons and
1600
+ fixed-size boxes squash under pressure they currently resist. A run that is
1601
+ NOT a heading is reached by the other half of this fix, which is keyed to
1602
+ the thing that actually makes it big: `partStyleDeclarations` in
1603
+ `lib/puck/part-styles.ts` writes the same two properties whenever it states
1604
+ a size. Together they cover both ways a run can outgrow its box.
1605
+
1606
+ Layered, like every bare-element rule in this file — so a section that
1607
+ really does want `min-w-*` or `whitespace-nowrap` on a heading still wins
1608
+ with a utility. `scripts/test-css-layers.ts` fails the build if it is not. */
1609
+ h1,
1610
+ h2,
1611
+ h3,
1612
+ h4,
1613
+ h5,
1614
+ h6 {
1615
+ min-width: 0;
1616
+ overflow-wrap: break-word;
1617
+ }
1618
+ }
1619
+
1620
+ /* Visible focus ring on dark + light themes */
1621
+ :focus-visible {
1622
+ outline: 2px solid var(--accent);
1623
+ outline-offset: 2px;
1624
+ }
1625
+
1626
+ /* Native <select> dropdown options + popup panel.
1627
+ Theme-aware:
1628
+ dark theme → option panel = black bg, white text
1629
+ light theme → option panel = white bg, black text
1630
+ `color-scheme` on the select itself tells the browser which OS
1631
+ chrome to render (Chrome/Edge/Safari respect this for the popup
1632
+ panel background). The explicit option {bg,color} pair covers
1633
+ Firefox + the option rows themselves so contrast stays correct in
1634
+ both themes. */
1635
+ /* Layered — see the element-reset block above. The `option:hover/:checked`
1636
+ and `[data-theme="light"]` rules below stay unlayered on purpose: they are
1637
+ state/theme OVERRIDES of these defaults, not resets. */
1638
+ @layer base {
1639
+ select {
1640
+ color-scheme: dark;
1641
+ accent-color: var(--accent);
1642
+ }
1643
+ option {
1644
+ background-color: #0a0a0a;
1645
+ color: #f5f1e8;
1646
+ }
1647
+ }
1648
+ option:hover,
1649
+ option:focus,
1650
+ option:checked,
1651
+ option:active {
1652
+ background: var(--accent) linear-gradient(0deg, var(--accent), var(--accent));
1653
+ color: #0a0a0a;
1654
+ }
1655
+ [data-theme="light"] select {
1656
+ color-scheme: light;
1657
+ }
1658
+ [data-theme="light"] option {
1659
+ background-color: #ffffff;
1660
+ color: #0a0a0a;
1661
+ }
1662
+ [data-theme="light"] option:hover,
1663
+ [data-theme="light"] option:focus,
1664
+ [data-theme="light"] option:checked,
1665
+ [data-theme="light"] option:active {
1666
+ background: var(--accent) linear-gradient(0deg, var(--accent), var(--accent));
1667
+ color: #0a0a0a;
1668
+ }
1669
+
1670
+ /* Mono caps eyebrow utility — the header's nav, wordmark strip and utility
1671
+ links (components/Header.tsx is its only user).
1672
+
1673
+ The face reads --font-role-label DIRECTLY, never `var(--font-mono)`. A custom
1674
+ property's var() is substituted on the element that DECLARES it, and
1675
+ --font-mono is declared on :root — so it computes to the house JetBrains Mono
1676
+ there and inherits that literal everywhere, whatever a site publishes further
1677
+ down. Measured on one element with the site's Labels font set to Outfit:
1678
+ `var(--font-mono)` -> JetBrains Mono, `var(--font-role-label)` -> Outfit. That
1679
+ is why the header nav stayed monospace on both demo sites even once the roles
1680
+ reached it (card 8824). The Tailwind `font-mono` utility is unaffected — it
1681
+ inlines the theme value, so its var() resolves per element already.
1682
+
1683
+ The size and tracking below stay hard-coded: making them per-site is a
1684
+ separate card. */
1685
+ .mono-eyebrow {
1686
+ font-family: var(--font-role-label), var(--font-mono-fallback);
1687
+ font-size: 14px;
1688
+ letter-spacing: 0.18em;
1689
+ text-transform: uppercase;
1690
+ }
1691
+
1692
+ /* ── Site styles ▸ Accent ▸ Size ────────────────────────────────────────────
1693
+ The size a gold `==phrase==` takes. The row was added on 2026-09-05 and read
1694
+ by NOTHING until 2026-09-08: `typeSizesStyle` published `--size-accent` and
1695
+ zero lines in `components/` or `app/` looked at it, so the control saved a
1696
+ number and moved not one pixel.
1697
+
1698
+ ⚠️ A RULE, NOT A CLASS ON THE ELEMENT, and the reason is measured rather than
1699
+ stylistic. `goldPhrases` renders every accent run, and its output is
1700
+ byte-frozen: `scripts/test-markup-byte-identity.tsx` compares it against the
1701
+ parser that shipped, and `scripts/test-word-styles.tsx` compares 900 PUBLISHED
1702
+ strings against a named commit. Adding one utility class there moved all 900 —
1703
+ a real diff on every live page, to deliver a size the cascade delivers with no
1704
+ markup at all.
1705
+
1706
+ It also reaches the accent runs written by hand in CartDrawer, GalleryItem,
1707
+ checkout and the PDPs, which `goldPhrases` never sees and which are the same
1708
+ style. Scoped to `em` so it cannot catch the many other elements that wear
1709
+ `text-accent` for its COLOUR alone.
1710
+
1711
+ ⚠️ THE FALLBACK IS `inherit`, NOT A NUMBER. An accent is a phrase inside a
1712
+ line, so with no size stated it must take that line's size — which is what an
1713
+ `<em>` already did. A site that has never opened the row publishes no
1714
+ `--size-accent` and renders byte for byte as it did. The tokens are published
1715
+ on the page content wrapper, so chrome outside `<main>` keeps inheriting too.
1716
+ `scripts/test-style-face-matches-size.tsx` §4 pins both halves. */
1717
+ em.text-accent {
1718
+ font-size: var(--size-accent, inherit);
1719
+ }
1720
+
1721
+ /* Letters: Capitalised — the header's "Letters" control turning the caps OFF
1722
+ (card 66518).
1723
+
1724
+ IT HAS TO LIVE HERE, beside `.mono-eyebrow`, and it cannot be a Tailwind
1725
+ utility. `.mono-eyebrow` above is declared OUTSIDE every `@layer`, and
1726
+ unlayered CSS beats layered CSS whatever the specificity — measured in the
1727
+ built stylesheet: `.mono-eyebrow` at byte 143878 unlayered, `.normal-case`
1728
+ at 77347 inside `@layer utilities`. So `normal-case` landed in the DOM and
1729
+ changed nothing on screen, which is exactly how it reached Lewis.
1730
+
1731
+ Same specificity as `.mono-eyebrow`, same (un)layer, declared AFTER it — so
1732
+ the cascade picks this one, and it keeps working if either rule moves as
1733
+ long as this one stays below. The 0.18em tracking goes with the caps: that
1734
+ spacing is drawn for capitals and reads as a gap between lowercase letters. */
1735
+ .mono-eyebrow-plain {
1736
+ letter-spacing: normal;
1737
+ text-transform: none;
1738
+ }
1739
+
1740
+ /* The header's three tool controls — search, account, cart — taking the case
1741
+ from the strip they sit in (card 66518).
1742
+
1743
+ A <button> does NOT inherit `text-transform`: every UA stylesheet sets
1744
+ `text-transform: none` on form controls, and that beats the inherited value.
1745
+ So the nav strip said `uppercase`, the account <a> obeyed, and the cart and
1746
+ search <button>s did not. Measured in Chrome on the real storefront:
1747
+
1748
+ nav text-transform: uppercase
1749
+ a Account uppercase <- inherits
1750
+ button Bag none <- does not
1751
+ button Search none <- does not
1752
+
1753
+ Lewis saw it as "ACCOUNT Bag Search" in one bar. It was always true; the
1754
+ glyphs just had no letters to show it with.
1755
+
1756
+ SCOPED to the three controls rather than to `.mono-eyebrow button`, so the
1757
+ region picker and the theme toggle in the same strip keep the case they draw
1758
+ today. Unlayered, like the two rules above, for the same cascade reason. */
1759
+ .mono-eyebrow-ctl {
1760
+ letter-spacing: inherit;
1761
+ text-transform: inherit;
1762
+ }
1763
+
1764
+ /* Finish-swatch border — a neutral mid-grey perimeter. Each swatch
1765
+ already carries its own hex fill; this border just provides a clean
1766
+ edge line so the colour disc reads as a deliberately-bordered chip.
1767
+ Theme-independent: the mid-grey is quiet enough on the dark page bg
1768
+ and visible enough on the light card surface, so it is not flipped
1769
+ in the [data-theme="light"] block. Consumed by ProductCard and the
1770
+ wheels CatalogCard swatches. */
1771
+ :root {
1772
+ --swatch-border: #888;
1773
+ }
1774
+
1775
+ /* Ticker keyframes — track is duplicated and translates -50% to loop seamlessly.
1776
+ FOUR of them, one per answer of the Scrolling Bar's `Scroll direction` row.
1777
+
1778
+ ⚠️ `storefront-ticker` KEEPS ITS BARE NAME AND IS THE LEFTWARD ONE. It is the
1779
+ travel every published marquee already has, and the section-style baseline
1780
+ pins the ticker's default markup down to this exact animation string — so
1781
+ renaming it would move a fixture for a strip whose pixels had not changed,
1782
+ which costs a normaliser or a CHANGED_SECTIONS row to say nothing at all.
1783
+ The three new directions are the ones that get new names. */
1784
+ @keyframes storefront-ticker {
1785
+ from {
1786
+ transform: translateX(0);
1787
+ }
1788
+ to {
1789
+ transform: translateX(-50%);
1790
+ }
1791
+ }
1792
+ /* Rightward is the same travel read backwards, NOT a negative duration or an
1793
+ `animation-direction: reverse` on the shared name: both would need a second
1794
+ property on the element, and the element already says everything it says in
1795
+ one `animation` shorthand. */
1796
+ @keyframes storefront-ticker-right {
1797
+ from {
1798
+ transform: translateX(-50%);
1799
+ }
1800
+ to {
1801
+ transform: translateX(0);
1802
+ }
1803
+ }
1804
+ /* The vertical pair. Same -50% loop on the other axis — the track is stacked
1805
+ rather than laid across, so the duplicated half tiles downwards instead. */
1806
+ @keyframes storefront-ticker-up {
1807
+ from {
1808
+ transform: translateY(0);
1809
+ }
1810
+ to {
1811
+ transform: translateY(-50%);
1812
+ }
1813
+ }
1814
+ @keyframes storefront-ticker-down {
1815
+ from {
1816
+ transform: translateY(-50%);
1817
+ }
1818
+ to {
1819
+ transform: translateY(0);
1820
+ }
1821
+ }
1822
+
1823
+ /* `.hero-poster` (two radial gradients over a dark ramp, plus a 45° crosshatch
1824
+ on ::before) was deleted by card 66237. The Hero's z-1 layer now paints one
1825
+ flat `var(--site-surface)` inline, like every other picture slot. Nothing else
1826
+ used the class, and the hardcoded #2a2218 / #1a1410 / #0a0a0a in it were
1827
+ dark-theme literals that never followed a merchant's palette. Do not
1828
+ reintroduce it: `scripts/test-placeholder-copy.ts` §3b fails on the name. */
1829
+
1830
+ /* Studio-map editorial overrides moved inline to ContactPageClient.tsx
1831
+ so they reliably ship with the component (Turbopack dev mode doesn't
1832
+ always hot-reload mid-session edits to globals.css). */
1833
+
1834
+ /* Footer — the first block (the brand logo) fills its whole column so no menu
1835
+ ever wraps beneath it. --footer-logo-rows is measured from the live block and
1836
+ column counts in FooterSection and set on <footer>; the logo cell spans that
1837
+ many rows and pins to column 1, pushing every other block into the columns to
1838
+ its right. The per-tier toggles (logoFill{Desktop,Tablet,Mobile}) live in that
1839
+ measurement: a disabled tier — or a single-column layout — resolves the var to
1840
+ 1, so this same rule collapses to a normal cell and the logo just stacks.
1841
+
1842
+ The logo can instead fill its whole ROW: --footer-logo-col (also set on
1843
+ <footer> per tier) is `1 / -1` when that view's row-fill is on, making the
1844
+ logo span every column as its own top row while the other blocks flow beneath.
1845
+ Row-fill and column-fill can't both apply, so when row-fill is on FooterSection
1846
+ forces --footer-logo-rows back to 1; off, the var defaults to `1` (a normal
1847
+ single-column cell) and the row span above takes over. */
1848
+ .footer-block-grid > :first-child {
1849
+ grid-column: var(--footer-logo-col, 1);
1850
+ grid-row: 1 / span var(--footer-logo-rows, 1);
1851
+ }
1852
+
1853
+ /* Alloy Wheels Listing — how many wheel cards sit in one row, authored PER VIEW
1854
+ ("Wheels per row" in the section's Results settings). The three track lists
1855
+ are computed in lib/wheels/plp-grid and set on the grid element as custom
1856
+ properties; this rule is only the media-query switch that picks one, because a
1857
+ published page has no JS device to ask. The editor sets all three to the
1858
+ previewed view's list instead — its iframe width is not the device being
1859
+ previewed — so the same rule serves both without branching.
1860
+
1861
+ No fallback values on purpose: an element that carries the class always
1862
+ carries the vars, and a silent fallback here would be a second, invisible
1863
+ source of truth for the default the grid module already owns.
1864
+
1865
+ The breakpoints ARE lib/content/view-tiers' VIEW_TABLET_MIN / VIEW_DESKTOP_MIN
1866
+ — CSS cannot read those constants, so scripts/test-view-tier-breakpoints.ts
1867
+ fails if this file and that module ever disagree. */
1868
+ .plp-card-grid {
1869
+ display: grid;
1870
+ gap: var(--plp-grid-gap);
1871
+ grid-template-columns: var(--plp-grid-cols-mobile);
1872
+ }
1873
+ @media (min-width: 720px) {
1874
+ .plp-card-grid {
1875
+ grid-template-columns: var(--plp-grid-cols-tablet);
1876
+ }
1877
+ }
1878
+ @media (min-width: 1100px) {
1879
+ .plp-card-grid {
1880
+ grid-template-columns: var(--plp-grid-cols-desktop);
1881
+ }
1882
+ }
1883
+
1884
+ /* Integrations wall marquee (components/IntegrationsSection.tsx): the chip
1885
+ track is rendered twice, so -50% is exactly one full list — the loop is
1886
+ seamless at any width. Reduced-motion users get a static wall (the
1887
+ component sets [animation:none] via motion-reduce). */
1888
+ @keyframes sf-marquee {
1889
+ from { transform: translateX(0); }
1890
+ to { transform: translateX(-50%); }
1891
+ }
1892
+
1893
+ /* ── Word styles — Highlight · Bold · Quiet (card 66119) ────────────────────
1894
+ A selected word wears a NAMED style, defined once for the whole site: change
1895
+ the definition and every word wearing it follows. Three NEW marks, one each:
1896
+
1897
+ @@phrase@@ Highlight ++phrase++ Bold ^^phrase^^ Quiet
1898
+
1899
+ ⚠️ `**` AND `==` ARE NOT INVOLVED AND MUST NOT BE. `**` is bold and `==` is
1900
+ the brand-colour highlight, on this site and in the email campaign designer,
1901
+ which parses the same authored string through the same shared grammar. See
1902
+ lib/word-styles.ts for why re-pointing them was considered and rejected.
1903
+
1904
+ EACH MARK'S BUILT-IN LOOK LIVES HERE, ONCE. `wordStyleTokens` emits a
1905
+ `--sf-ws-*` custom property only for a facet the merchant has actually set,
1906
+ so the fallback in each `var()` below IS the default — there is no second
1907
+ copy of these values in TypeScript to drift from. A `var(--x, inherit)` reads
1908
+ as "the merchant said nothing about this facet, so leave it alone".
1909
+
1910
+ NOT in @layer utilities, so these beat any Tailwind utility on the same
1911
+ element. Nothing sets one today — a word-style span carries no class at all —
1912
+ but a section that later wrapped one would otherwise silently win over a
1913
+ merchant's explicit choice.
1914
+
1915
+ The three differ only in their DEFAULT colour and weight; every other facet
1916
+ starts unset, because a word style should change as little as it must. */
1917
+ [data-sf-ws] {
1918
+ font-family: var(--sf-ws-font, inherit);
1919
+ font-size: var(--sf-ws-size, inherit);
1920
+ font-style: var(--sf-ws-style, inherit);
1921
+ line-height: var(--sf-ws-line, inherit);
1922
+ letter-spacing: var(--sf-ws-track, inherit);
1923
+ }
1924
+
1925
+ /* Highlight — the site's brand colour, the same ink `==` paints in. They are
1926
+ two different marks on purpose: `==` is a fixed brand-colour highlight shared
1927
+ with email, this one is a style a merchant can redefine.
1928
+
1929
+ THE SELECTOR AND EVERY TOKEN BELOW SPELL THE KEY, NOT THE LABEL. A merchant
1930
+ read Emphasis from card 66172 until 2026-10-04 and reads Highlight again now;
1931
+ `highlight` is what every published page and the backend column already
1932
+ store, and it does not move for a copy change. */
1933
+ [data-sf-ws="highlight"] {
1934
+ --sf-ws-font: var(--sf-ws-highlight-font, inherit);
1935
+ --sf-ws-size: var(--sf-ws-highlight-size, inherit);
1936
+ --sf-ws-style: var(--sf-ws-highlight-style, inherit);
1937
+ --sf-ws-line: var(--sf-ws-highlight-line, inherit);
1938
+ --sf-ws-track: var(--sf-ws-highlight-track, inherit);
1939
+ font-weight: var(--sf-ws-highlight-weight, var(--sf-ws-highlight-weight-bold, inherit));
1940
+ color: var(--sf-ws-highlight-color, var(--accent));
1941
+ }
1942
+
1943
+ /* Strong — heavier than its sentence, in the sentence's own ink. 600 rather
1944
+ than 700 so it matches the weight `**` already renders at: a merchant reaching
1945
+ for "the strong one" should not find it louder than the bold beside it. */
1946
+ [data-sf-ws="strong"] {
1947
+ --sf-ws-font: var(--sf-ws-strong-font, inherit);
1948
+ --sf-ws-size: var(--sf-ws-strong-size, inherit);
1949
+ --sf-ws-style: var(--sf-ws-strong-style, inherit);
1950
+ --sf-ws-line: var(--sf-ws-strong-line, inherit);
1951
+ --sf-ws-track: var(--sf-ws-strong-track, inherit);
1952
+ font-weight: var(--sf-ws-strong-weight, var(--sf-ws-strong-weight-bold, 600));
1953
+ color: var(--sf-ws-strong-color, inherit);
1954
+ }
1955
+
1956
+ /* Quiet — the site's Labels ink, the dim tone eyebrows and captions already
1957
+ use, so a quiet word recedes by the same amount everywhere. */
1958
+ [data-sf-ws="quiet"] {
1959
+ --sf-ws-font: var(--sf-ws-quiet-font, inherit);
1960
+ --sf-ws-size: var(--sf-ws-quiet-size, inherit);
1961
+ --sf-ws-style: var(--sf-ws-quiet-style, inherit);
1962
+ --sf-ws-line: var(--sf-ws-quiet-line, inherit);
1963
+ --sf-ws-track: var(--sf-ws-quiet-track, inherit);
1964
+ font-weight: var(--sf-ws-quiet-weight, var(--sf-ws-quiet-weight-bold, inherit));
1965
+ color: var(--sf-ws-quiet-color, var(--fg-dim));
1966
+ }
1967
+
1968
+ /* ── A Hero slide's GROUPS, and the anchor each one sits on (card 66189) ───────
1969
+
1970
+ A group carries an ANCHOR, never a pair of coordinates. The owner's reason is
1971
+ the design: the hero's height is the merchant's, a phone breaks every stored
1972
+ coordinate, and "bottom left" means the same thing at any size.
1973
+
1974
+ ⚠️ WHY THIS IS A GRID AND NOT NINE ABSOLUTE CORNERS. Puck 0.20 hands a slot's
1975
+ children to ONE container as a flat list — there is no per-child wrapper — so
1976
+ `HeroSlide` cannot sort its groups into nine corner boxes, and nine absolute
1977
+ siblings cannot stack by content height without measuring in JS (which means
1978
+ the published page moving after it paints). One grid solves both: three bands
1979
+ of four rows with a `1fr` spacer between them, the vertical anchor picking the
1980
+ band's first row and the horizontal anchor riding `justify-self`.
1981
+
1982
+ That last part is load-bearing and easy to lose: three anchors in ONE band
1983
+ share a row, so `bottom-left` and `bottom-right` sit SIDE BY SIDE — which is
1984
+ what today's hero is (heading left, body right, one line). A design that gave
1985
+ each band a flex column would stack them and quietly break it.
1986
+
1987
+ Empty `auto` rows collapse to nothing, so a band holding one group hugs its
1988
+ edge exactly as if the other three rows were not there.
1989
+
1990
+ ⚠️ THE STACK RULES BELOW WIN ON SPECIFICITY, NOT ON SOURCE ORDER, AND THAT IS
1991
+ DELIBERATE. `~` is the GENERAL sibling combinator, so `.g[a] ~ .g[a]` matches
1992
+ the 2nd group on that anchor AND the 3rd AND the 4th — every one of them. The
1993
+ right row still wins because each longer chain is a more specific selector
1994
+ (three compound selectors beat two), and specificity is settled before source
1995
+ order ever comes into it. So these rules may be written in any order and may
1996
+ be reordered safely — but do NOT "tidy" them by collapsing the chains or by
1997
+ assuming the last one wins, because the 3rd and 4th rows would silently fall
1998
+ back to the 2nd row's rule and two groups would draw on top of each other.
1999
+
2000
+ THE STACK IS CAPPED AT FOUR, and the cap is enforced in the EDITOR as well as
2001
+ here — a fifth group would land on the fourth's row, and a guard is not in the
2002
+ room when a merchant presses Add. See `heroGroupAddBlocked` and
2003
+ `placementPoints` in lib/hero-group-placement.ts: the Add row and the full
2004
+ anchor's square both go unavailable, with a sentence saying why.
2005
+
2006
+ The whole model — the row maths, the depth, the nudge limit — lives in
2007
+ lib/hero-group-placement.ts, and scripts/test-hero-group-placement.tsx pins
2008
+ this file against it (a tenth anchor with no rule here fails there).
2009
+
2010
+ NOT in @layer utilities, like the word-style rules above and for the same
2011
+ reason: a group's row must beat any utility a later section puts on it. */
2012
+ .sf-hero-group-layer {
2013
+ display: grid;
2014
+ grid-template-columns: minmax(0, 1fr);
2015
+ /* FIVE rows per band, not four: the first row of each band belongs to the
2016
+ slide's OWN copy (card 66213) and the four after it are the groups' stack.
2017
+ Keep in step with HERO_GROUP_BAND_ROWS — lib/hero-group-placement carries
2018
+ the whole reason, and a band that disagrees with this line puts a group on
2019
+ a row the grid does not have, which is an implicit row BELOW the slide
2020
+ rather than an error anybody sees. */
2021
+ grid-template-rows: repeat(5, auto) 1fr repeat(5, auto) 1fr repeat(5, auto);
2022
+ /* The layer covers the whole slide, so it would swallow every click meant for
2023
+ what is underneath it. Only the copy and the groups are hit targets. */
2024
+ pointer-events: none;
2025
+ }
2026
+ /* …except in the editor, where the empty drop zone IS the drop target. Card
2027
+ 66213 moved this off the LAYER and onto the zone: the layer now also holds the
2028
+ slide's own copy, and making the whole layer a target would put the drop
2029
+ surface on top of the merchant's own heading. */
2030
+ .sf-hero-group-layer-edit .sf-hero-group-zone { pointer-events: auto; }
2031
+
2032
+ /* Puck's own drop zone, laid over the SAME rows as the copy beside it (card
2033
+ 66213). This is the one layer, and this element is what makes it one:
2034
+ `subgrid` means a group's `--sf-hg-row` counts the layer's tracks, not a
2035
+ private set of its own, so a group and the copy on one square STACK instead of
2036
+ overlapping — which two stacked layers can never do.
2037
+
2038
+ ⚠️ A REAL BOX, DELIBERATELY, AND NOT `display: contents`. Puck measures this
2039
+ element to decide where a drag lands; `display: contents` removes the box and
2040
+ takes the measurement with it. Spanning every row costs nothing because the
2041
+ zone is not a hit target (above) and its rows are the layer's own.
2042
+
2043
+ Measured in a real browser before it was written (900×500 slide, 40px inset):
2044
+ copy 406→424 at the left edge, group one 424→442 directly beneath it, group
2045
+ two 442→460, a top-right group at 40→58 against the right edge, and ZERO
2046
+ overlapping pairs. scripts/harness/hero-copy-grid-probe.html is that probe. */
2047
+ .sf-hero-group-zone {
2048
+ grid-column: 1;
2049
+ grid-row: 1 / -1;
2050
+ display: grid;
2051
+ grid-template-columns: minmax(0, 1fr);
2052
+ grid-template-rows: subgrid;
2053
+ pointer-events: none;
2054
+ }
2055
+
2056
+ .sf-hero-group {
2057
+ /* The BAND's own row, stated by the component as a custom property so the
2058
+ sibling rules below can add to it. An inline `grid-row` would beat them. */
2059
+ grid-row: var(--sf-hg-row);
2060
+ /* EXPLICIT, and it is not decoration: the zone above spans every row of
2061
+ column 1, so auto-placement has no free cell left and would flow the copy
2062
+ into an implicit SECOND column — which is exactly what the browser probe
2063
+ did before this line existed, parking the copy off to the right of a
2064
+ narrowed zone. */
2065
+ grid-column: 1;
2066
+ min-width: 0;
2067
+ pointer-events: auto;
2068
+ }
2069
+
2070
+ /* A group's own two numbers, applied from CUSTOM PROPERTIES the component
2071
+ renders rather than from the inline `max-width` / `translate` it used to
2072
+ write directly (card 66255).
2073
+
2074
+ ⚠️ THIS IS NOT A TIDY-UP, AND PUTTING THEM BACK INLINE BREAKS THE PHONE. An
2075
+ inline style beats every stylesheet rule that is not `!important`, so the
2076
+ phone block below could not reach either of them — and both have to be
2077
+ reached: a `width` is a % of the SLIDE (60% of 375px is 201px, a number no
2078
+ merchant chose) and a nudge is a ±40px fine adjustment against an ANCHOR,
2079
+ which a stack does not have. Free to change, because no stored document holds
2080
+ a HeroGroup at all: measured 2026-08-29 across every schema, in both `layout`
2081
+ and `published_layout`, soft-deleted rows included.
2082
+
2083
+ An UNTOUCHED group still renders nothing for the nudge — the component omits
2084
+ the property entirely — so the fallback here is what carries the "no nudge"
2085
+ case, exactly as the inline style's absence used to. */
2086
+ .sf-hero-group {
2087
+ max-width: var(--sf-hg-w, 100%);
2088
+ translate: var(--sf-hg-nudge, none);
2089
+ }
2090
+
2091
+ /* A box's WIDTH BUILT-IN, and it is two different numbers because the two
2092
+ positions are two different questions (owner 2026-09-04: "是需要在那个百分之
2093
+ 60 的盒子里, 100% 的出现").
2094
+
2095
+ A box parked on the SLIDE is a column of copy over a photograph, and 60% of
2096
+ the slide is the shape that reads — so the zone's own children carry 60%
2097
+ here. A box parked INSIDE another box was handed the same 60% and it was
2098
+ simply wrong: it is not measuring the slide, it is measuring the box its
2099
+ merchant just drew, and 60% of that is a second unasked-for indent. It takes
2100
+ the `var()` fallback one rule up — 100%, fill the box you are in.
2101
+
2102
+ ⚠️ THE BUILT-IN LIVES HERE AND NOT IN `defaultProps` — doc/editor-ui.md E11,
2103
+ the same call `lib/content/container-gap.ts` and `container-direction.ts`
2104
+ already made for this very box. A seeded default is a second copy of a
2105
+ number, free to drift from the one the render uses; worse here than for
2106
+ either of those, because this number depends on WHERE the node sits and
2107
+ `defaultProps` is stamped before the node has a position.
2108
+
2109
+ ⚠️ AND IT IS A CUSTOM PROPERTY ON A SELECTOR, NOT A `max-width`. The rule
2110
+ above is the one place a box's width is applied, so a merchant's stored
2111
+ width still beats this (it arrives inline) and the phone block below still
2112
+ overrides both. A `max-width` here would be a second application of one
2113
+ idea, which is the defect the inline-vs-stylesheet note above is about. */
2114
+ .sf-hero-group-zone > .sf-hero-group {
2115
+ --sf-hg-w: 60%;
2116
+ }
2117
+
2118
+ /* The second group on ONE anchor drops 1 row, so a stack never overlaps.
2119
+ Document order is Layer-panel order, so the tree order IS the stack order. */
2120
+ .sf-hero-group[data-hg="top-left"] ~ .sf-hero-group[data-hg="top-left"],
2121
+ .sf-hero-group[data-hg="top-center"] ~ .sf-hero-group[data-hg="top-center"],
2122
+ .sf-hero-group[data-hg="top-right"] ~ .sf-hero-group[data-hg="top-right"],
2123
+ .sf-hero-group[data-hg="left"] ~ .sf-hero-group[data-hg="left"],
2124
+ .sf-hero-group[data-hg="center"] ~ .sf-hero-group[data-hg="center"],
2125
+ .sf-hero-group[data-hg="right"] ~ .sf-hero-group[data-hg="right"],
2126
+ .sf-hero-group[data-hg="bottom-left"] ~ .sf-hero-group[data-hg="bottom-left"],
2127
+ .sf-hero-group[data-hg="bottom-center"] ~ .sf-hero-group[data-hg="bottom-center"],
2128
+ .sf-hero-group[data-hg="bottom-right"] ~ .sf-hero-group[data-hg="bottom-right"] {
2129
+ grid-row: calc(var(--sf-hg-row) + 1);
2130
+ }
2131
+
2132
+ /* The third group on ONE anchor drops 2 rows, so a stack never overlaps.
2133
+ Document order is Layer-panel order, so the tree order IS the stack order. */
2134
+ .sf-hero-group[data-hg="top-left"] ~ .sf-hero-group[data-hg="top-left"] ~ .sf-hero-group[data-hg="top-left"],
2135
+ .sf-hero-group[data-hg="top-center"] ~ .sf-hero-group[data-hg="top-center"] ~ .sf-hero-group[data-hg="top-center"],
2136
+ .sf-hero-group[data-hg="top-right"] ~ .sf-hero-group[data-hg="top-right"] ~ .sf-hero-group[data-hg="top-right"],
2137
+ .sf-hero-group[data-hg="left"] ~ .sf-hero-group[data-hg="left"] ~ .sf-hero-group[data-hg="left"],
2138
+ .sf-hero-group[data-hg="center"] ~ .sf-hero-group[data-hg="center"] ~ .sf-hero-group[data-hg="center"],
2139
+ .sf-hero-group[data-hg="right"] ~ .sf-hero-group[data-hg="right"] ~ .sf-hero-group[data-hg="right"],
2140
+ .sf-hero-group[data-hg="bottom-left"] ~ .sf-hero-group[data-hg="bottom-left"] ~ .sf-hero-group[data-hg="bottom-left"],
2141
+ .sf-hero-group[data-hg="bottom-center"] ~ .sf-hero-group[data-hg="bottom-center"] ~ .sf-hero-group[data-hg="bottom-center"],
2142
+ .sf-hero-group[data-hg="bottom-right"] ~ .sf-hero-group[data-hg="bottom-right"] ~ .sf-hero-group[data-hg="bottom-right"] {
2143
+ grid-row: calc(var(--sf-hg-row) + 2);
2144
+ }
2145
+
2146
+ /* The fourth group on ONE anchor drops 3 rows, so a stack never overlaps.
2147
+ Document order is Layer-panel order, so the tree order IS the stack order. */
2148
+ .sf-hero-group[data-hg="top-left"] ~ .sf-hero-group[data-hg="top-left"] ~ .sf-hero-group[data-hg="top-left"] ~ .sf-hero-group[data-hg="top-left"],
2149
+ .sf-hero-group[data-hg="top-center"] ~ .sf-hero-group[data-hg="top-center"] ~ .sf-hero-group[data-hg="top-center"] ~ .sf-hero-group[data-hg="top-center"],
2150
+ .sf-hero-group[data-hg="top-right"] ~ .sf-hero-group[data-hg="top-right"] ~ .sf-hero-group[data-hg="top-right"] ~ .sf-hero-group[data-hg="top-right"],
2151
+ .sf-hero-group[data-hg="left"] ~ .sf-hero-group[data-hg="left"] ~ .sf-hero-group[data-hg="left"] ~ .sf-hero-group[data-hg="left"],
2152
+ .sf-hero-group[data-hg="center"] ~ .sf-hero-group[data-hg="center"] ~ .sf-hero-group[data-hg="center"] ~ .sf-hero-group[data-hg="center"],
2153
+ .sf-hero-group[data-hg="right"] ~ .sf-hero-group[data-hg="right"] ~ .sf-hero-group[data-hg="right"] ~ .sf-hero-group[data-hg="right"],
2154
+ .sf-hero-group[data-hg="bottom-left"] ~ .sf-hero-group[data-hg="bottom-left"] ~ .sf-hero-group[data-hg="bottom-left"] ~ .sf-hero-group[data-hg="bottom-left"],
2155
+ .sf-hero-group[data-hg="bottom-center"] ~ .sf-hero-group[data-hg="bottom-center"] ~ .sf-hero-group[data-hg="bottom-center"] ~ .sf-hero-group[data-hg="bottom-center"],
2156
+ .sf-hero-group[data-hg="bottom-right"] ~ .sf-hero-group[data-hg="bottom-right"] ~ .sf-hero-group[data-hg="bottom-right"] ~ .sf-hero-group[data-hg="bottom-right"] {
2157
+ grid-row: calc(var(--sf-hg-row) + 3);
2158
+ }
2159
+
2160
+ /* ── A BUTTON IN A SLIDE BOX SET TO `One row` DROPS ITS FULL-WIDTH FRAME ─────
2161
+ Card `[FIX] Two Buttons Side By Side In A Hero`.
2162
+
2163
+ THE DEFECT. `components/ButtonBlock.tsx` draws a frame that asks for the
2164
+ whole line — `w-full`, or the site frame, which is `w-full` inside a cap.
2165
+ That is right wherever a Button block owns its own row, and it is exactly
2166
+ wrong in the one place a merchant puts TWO of them on one line. Measured in a
2167
+ real browser on this stylesheet before this rule was written, two Buttons in
2168
+ a 1200px Slide box set to `One row`:
2169
+
2170
+ each block 1200px wide, stacked one under the other — two lines, not one.
2171
+
2172
+ Card 66262 had already drawn the right answer and called it THE HERO LOOK:
2173
+ "the button alone, with no frame and no row of its own, because the hero puts
2174
+ BOTH its buttons in one row". It reached the component as a `hero` prop set
2175
+ by the slide's own built-in button — and that button was deleted when the
2176
+ hero moved onto Boxes (#1280), so the prop had no caller left and the look
2177
+ was unreachable. This is that look, restored where it can actually be reached.
2178
+
2179
+ ⚠️ IT IS A RULE AND NOT A PROP, AND THAT IS THE WHOLE MECHANISM. The question
2180
+ is "what is holding this block?", and the block cannot answer it: it renders
2181
+ on the published site, where there is no document to walk, and every block in
2182
+ a hero is a server component, so there is no React context to read either.
2183
+ `components/HeroGroup.tsx` makes the identical call one tier up for
2184
+ `--sf-hg-w` and states the reason there in full — CSS already knows, so the
2185
+ answer is asked where it is free. `lib/puck/config.tsx`'s cell-width pass is
2186
+ the same shape for the EDITOR half of a position question.
2187
+
2188
+ ⚠️ TWO SELECTORS, BECAUSE THE EDITOR ADDS A LAYER. Puck 0.20 wraps every slot
2189
+ child in a drag element unless the component declares `inline`; the published
2190
+ page has no wrapper (HeroGroup's own docblock carries that measurement, card
2191
+ 66290). So the first line is the page and the second is the canvas.
2192
+
2193
+ ⛔ `:not(.sf-hero-group)` IS LOAD-BEARING. A Slide box may hold a Slide box,
2194
+ and that inner box answers this question FOR ITSELF — it carries its own
2195
+ `Direction`, so it matches this rule on its own when it is a row and must not
2196
+ be reached through its parent when it is `Stacked`. Without it, a button in a
2197
+ stacked box inside a one-row box would silently lose its width.
2198
+
2199
+ ⚠️ NOT GATED ON THE DESKTOP. Below 800px the block above turns this container
2200
+ into a column while the `flex-row` class stays on it, so this rule still
2201
+ applies — and it must: `align-items` is unset on a Slide box, so a flex item
2202
+ with `width: auto` is STRETCHED to the column, which is the full-width button
2203
+ a phone already draws. A `min-width: 801px` gate here would give the phone
2204
+ two shrink-wrapped buttons instead, which nobody asked for.
2205
+
2206
+ UNLAYERED, like every rule in this section, and here that is load-bearing
2207
+ rather than incidental: `w-full`, `max-w-*`, `mx-auto` and `px-*` are all
2208
+ Tailwind utilities inside `@layer utilities`, and an unlayered rule beats a
2209
+ layered one whatever their specificity. */
2210
+ .sf-hero-group-blocks.flex-row > .sf-button-block,
2211
+ .sf-hero-group-blocks.flex-row > *:not(.sf-hero-group) > .sf-button-block {
2212
+ width: auto;
2213
+ max-width: none;
2214
+ margin-inline: 0;
2215
+ padding-inline: 0;
2216
+ }
2217
+
2218
+ /* ── ON A PHONE THE NINE SQUARES BECOME ONE COLUMN (card 66255) ──────────────
2219
+
2220
+ THE DEFECT THIS FIXES IS A COLLISION, NOT A MISSING FEATURE. The layer above
2221
+ is ONE grid column and an anchor resolves to a grid ROW, so a band's three
2222
+ anchors deliberately SHARE a row and are pulled apart by `justify-self` —
2223
+ correct on a wide screen, and what makes today's hero expressible. The
2224
+ sibling chains above only drop a group that shares an ANCHOR with the one
2225
+ before it, so a `left` group and a `right` group sit in ONE cell at every
2226
+ width. At the default 60% each, that is 120% of the slide.
2227
+
2228
+ Measured in a real browser on this stylesheet before the block was written,
2229
+ two groups anchored left and right with a 28px heading in each:
2230
+
2231
+ 375px left 20 → 221 right 154 → 355 both top 242 → 355
2232
+ OVERLAPPING BY 67px, on identical vertical bounds.
2233
+ 1280px left 40 → 259 right 989 → 1240 both top 298 → 372
2234
+ side by side, 730px of clear air. Correct, and untouched below.
2235
+
2236
+ ⚠️ THE CONTAINER CHANGES, THE CHILDREN DO NOT. `grid-row` is INERT on a flex
2237
+ item, so nothing here has to out-specify the ~60 lines of sibling chains
2238
+ above — and a `grid-row: auto` override could not have: those chains are four
2239
+ classes and four attributes deep and a media query adds no specificity. Turn
2240
+ the container into a single-line column and every box gets a line of its own
2241
+ by the flexbox model itself. The chains stay exactly as they are and come
2242
+ back untouched at 801px.
2243
+
2244
+ Both boxes are laid out here: `.sf-hero-group-layer` holds the slide's own
2245
+ copy and the zone, `.sf-hero-group-zone` holds the groups. Two containers,
2246
+ one column — the copy first because it is first in the document, and the
2247
+ document order is the reading order. There is deliberately no `order:` here:
2248
+ sorting the column by band would move the box without moving the markup, so
2249
+ a screen reader and the tab key would disagree with the screen.
2250
+
2251
+ THE PHONE LINE IS 800px, WHICH IS THE HERO'S OWN. The picker hides below it,
2252
+ the arrows hide below it, the slideshow's bottom dots appear below it and the
2253
+ slide's type and padding step down at it — see lib/hero-phone-stack.ts, which
2254
+ carries the number for the guard to read (a media query cannot take a custom
2255
+ property, so it is spelled out here too). */
2256
+ @media (max-width: 800px) {
2257
+ .sf-hero-group-layer {
2258
+ display: flex;
2259
+ flex-direction: column;
2260
+ flex-wrap: nowrap;
2261
+ /* The slide's own padding steps down HERE rather than on the element (card
2262
+ 66265). It used to be `max-[800px]:px-5 max-[800px]:pt-[100px]
2263
+ max-[800px]:pb-[90px]`, which Tailwind compiles to
2264
+ `@media (width < 800px)` — one pixel short of this block's own line — so
2265
+ at exactly 800px THIS DIV was a phone column with desktop padding on it.
2266
+ The values are the ones those three utilities emitted, `--spacing` and
2267
+ all, so a phone renders the pixels it rendered before the move. */
2268
+ padding-inline: calc(var(--spacing) * 5);
2269
+ padding-top: 100px;
2270
+ padding-bottom: 90px;
2271
+ /* The two rows the Hero asks, as the merchant's own answer or the default
2272
+ every published hero already draws: bottom, left. The FALLBACKS carry the
2273
+ default rather than the component writing it, so an unasked hero renders
2274
+ the same style attribute it rendered before this card — see
2275
+ `heroPhoneStackStyle`, and keep the two in step. */
2276
+ justify-content: var(--sf-hero-phone-y, flex-end);
2277
+ align-items: var(--sf-hero-phone-x, flex-start);
2278
+ }
2279
+ .sf-hero-group-zone {
2280
+ display: flex;
2281
+ flex-direction: column;
2282
+ flex-wrap: nowrap;
2283
+ /* The zone is the column's full width even when the column hugs a side, so
2284
+ a group is aligned against the SLIDE and not against the widest group
2285
+ beside it. Without this the layer's `align-items` shrink-wraps the zone
2286
+ and `Sideways` would move the whole block instead of each box. */
2287
+ align-self: stretch;
2288
+ align-items: var(--sf-hero-phone-x, flex-start);
2289
+ }
2290
+ .sf-hero-group {
2291
+ /* A group's stored width is a % of the SLIDE. 60% of 375px is 201px — a
2292
+ number nobody chose — so in the stack a group may use the whole column
2293
+ and its box is as wide as its own words, which is what leaves `Sideways`
2294
+ something to move. */
2295
+ max-width: 100%;
2296
+ /* ±40px is a fine adjustment against an ANCHOR. There is no anchor in a
2297
+ stack, and 40px of a 375px screen is 11% of the picture — enough to push
2298
+ a box under the slide's own `overflow: hidden`. */
2299
+ translate: none;
2300
+ }
2301
+ /* …and the blocks INSIDE a group stack too (card 66264). A group may be set to
2302
+ `One row`, which is a wide-screen answer: on a phone a heading beside a
2303
+ button is the same collision this whole block exists to prevent, one tier
2304
+ down. The stored answer is untouched and comes straight back at 801px.
2305
+
2306
+ IT LIVES HERE, IN THIS BLOCK, RATHER THAN AS A `max-[800px]:flex-col` ON THE
2307
+ ELEMENT — which is how components/Flex.tsx spells the same idea, and how the
2308
+ rest of the shop spells every phone rule it has. Tailwind compiles that
2309
+ variant to `@media (width < 800px)`, which stops at 799; this block is
2310
+ `max-width: 800px`, which does not. A group's blocks would therefore have
2311
+ flipped one pixel away from the group's own stack. One line in the block
2312
+ that already owns the hero's phone line keeps the two together, and keeps
2313
+ the number in exactly one place.
2314
+
2315
+ UNLAYERED, like every rule in this section, and here that is load-bearing
2316
+ rather than incidental: `flex-row` is a Tailwind utility inside
2317
+ `@layer utilities`, and an unlayered rule beats a layered one whatever their
2318
+ specificity. */
2319
+ .sf-hero-group-blocks {
2320
+ flex-direction: column;
2321
+ }
2322
+ /* ── AND THE REST OF THE HERO, MOVED HERE BY CARD 66265 ──────────────────
2323
+ Everything below drew its phone answer from a `max-[800px]:` variant on the
2324
+ element until that card. Tailwind v4 compiles that variant to
2325
+ `@media (width < 800px)`, which STOPS AT 799, while this block is
2326
+ `@media (max-width: 800px)`, which does not — so at a viewport of exactly
2327
+ 800px the hero was in two modes at once: the layer, the zone and the groups
2328
+ above had already collapsed to the phone column while these four still drew
2329
+ the wide layout. Card 66264 found that and stepped around it; this is the
2330
+ same move it made for `.sf-hero-group-blocks`, applied to the rest.
2331
+
2332
+ `max-[801px]:` would also have worked and is refused: it leaves a number in
2333
+ the source that reads wrong to everyone who meets it afterwards. The line is
2334
+ 800 on both sides, declared once, above.
2335
+
2336
+ UNLAYERED, like every rule in this section, and here that is load-bearing
2337
+ rather than incidental: each of these elements still carries the Tailwind
2338
+ utility for its WIDE value (`py-3.5`, `left-6`), those live in
2339
+ `@layer utilities`, and an unlayered rule beats a layered one whatever their
2340
+ specificity. (Card 66288 took the copy grid and the copy button row out of
2341
+ this run with the rest of the slide's built-in copy; the two that remain are
2342
+ the ticker and the editor's video control.) The values are the ones the removed
2343
+ utilities emitted, so a phone renders exactly what it rendered before.
2344
+ `scripts/test-hero-800-line.tsx` resolves all four at 799, 800 and 801. */
2345
+ .sf-hero-ticker {
2346
+ padding-block: calc(var(--spacing) * 3);
2347
+ }
2348
+ /* Editor-only chrome, and it moves for the same reason as the rest: a control
2349
+ that stepped in at 799 while the slide under it stacked at 800 is the same
2350
+ disagreement, just one a shopper never sees. */
2351
+ .sf-hero-video-control {
2352
+ left: calc(var(--spacing) * 4);
2353
+ }
2354
+ /* ── AND THE HERO'S OVER-MEDIA CHROME, MOVED HERE BY CARD 66270 ──────────
2355
+ The MIRROR HALF of the block above. Those declarations arrived because
2356
+ `max-[800px]:` compiles to `@media (width < 800px)`, which stops one pixel
2357
+ SHORT of this block's line. These two arrive because `min-[800px]:`
2358
+ compiles to `@media (width >= 800px)`, which INCLUDES 800 — exactly as this
2359
+ block does. So at a viewport of exactly 800px the slide had already
2360
+ collapsed into its phone column, above, while the slideshow still drew its
2361
+ DESKTOP chrome over the top of it: the slide picker and the prev / next
2362
+ arrows shown, and the phone's own dot row — the control that is meant to
2363
+ REPLACE them down here — hidden. No width had both.
2364
+
2365
+ `min-[801px]:` would also have worked and is refused, for the same reason
2366
+ `max-[801px]:` was: it leaves a number in the source that reads wrong to
2367
+ everyone who meets it afterwards. The line is 800 on both sides, declared
2368
+ once, at the top of this block.
2369
+
2370
+ UNLAYERED, and load-bearing rather than incidental: each element still
2371
+ carries a Tailwind utility for its OTHER answer (`flex` on the rail,
2372
+ `hidden` on the dots), those live in `@layer utilities`, and an unlayered
2373
+ rule beats a layered one whatever their specificity. Same fact card 66265
2374
+ leans on. `scripts/test-hero-800-chrome.tsx` resolves all three elements at
2375
+ 799, 800 and 801, and proves these two rules are unlayered from the source.
2376
+
2377
+ THE RAIL IS BOTH FEATURES AT ONCE. `heroOverlayRailClass` in
2378
+ lib/hero-overlay-anchor.ts is the one desktop gate, written once, and the
2379
+ picker and the arrows both read it — so this single selector is why "the
2380
+ arrows follow whatever the picker does on a phone" stays true by
2381
+ construction. Do not give either of them a rule of its own. */
2382
+ .sf-hero-overlay-rail {
2383
+ display: none;
2384
+ }
2385
+ /* The dots are NOT a caller of that gate — they are the phone's own control,
2386
+ drawn inline by components/HeroSection.tsx, and they had to move with it:
2387
+ hiding the picker and the arrows at 800 while these stayed hidden too would
2388
+ leave an 800px visitor no way to change slide at all. */
2389
+ .sf-hero-phone-dots {
2390
+ display: flex;
2391
+ }
2392
+ /* The air between stacked boxes, put on the GROUPS alone and never as a `gap`
2393
+ on the layer. An empty groups zone is still a flex item, so a `gap` would
2394
+ push the copy of every published slide — none of which has a group — 28px
2395
+ off the bottom for no reason anyone asked for. A margin on a box that does
2396
+ not exist costs nothing. */
2397
+ .sf-hero-group-zone .sf-hero-group {
2398
+ margin-top: 28px;
2399
+ }
2400
+ /* ── ON A PHONE THE HERO GROWS TO FIT ITS WORDS (card [FIX] Hero Grows On
2401
+ Phone, GG 2026-10-16) ─────────────────────────────────────────────────
2402
+ The phone column above is bottom-aligned (`justify-content: flex-end`) in
2403
+ a layer that covered a hero of FIXED height (`absolute inset-0` on a
2404
+ `h-[…]` / `h-screen min-h-[680px]` section). Words taller than the hero
2405
+ therefore rose off the TOP of the page, under the site's header:
2406
+ hartesmetal.com/dealers' heading began 181px above the page at 390x844, and
2407
+ hartesmetal.com.au's home slideshow 100px above it at 375x667.
2408
+
2409
+ So on a phone the hero's height is a MINIMUM and its words size it:
2410
+ · the hero (HeroSingle's section, the slideshow's viewport) is a column
2411
+ at least as tall as the height that applies at this width: the set
2412
+ phone height (`--sf-hero-h-m`, which the builder writes on the
2413
+ element), from 720px the set desktop height (`--sf-hero-h-d`; 720 is
2414
+ the height control's own line and picks WHICH height, nothing else —
2415
+ the rule just below this block), or, with no set height,
2416
+ `max(100vh, 680px)`: the pixels `h-screen min-h-[680px]` drew;
2417
+ · the slideshow's track fills that column and is as tall as its tallest
2418
+ slide (a flex row stretches its slides), and each slide is a column;
2419
+ · the word layer joins the flow, fills its hero or slide, and is its
2420
+ width. `width: 100%` and not stretch: the layer keeps `mx-auto` for the
2421
+ wide screen, and in a flex column auto side margins switch stretching
2422
+ off, so it would shrink to its widest word and `lib/fit-words` would
2423
+ never see that word overflow.
2424
+ Words that already fit stay exactly where the bottom-aligned stack put
2425
+ them. 801px and up is untouched.
2426
+
2427
+ HERE, on the hero's one phone line, and not as `max-[720px]:` utilities on
2428
+ the components (17.0.1 did that, and drew the layout on a second line —
2429
+ between 720 and 800 the old overflow stayed). UNLAYERED, like the rest of
2430
+ this block: it must beat the height and position utilities the elements
2431
+ carry for their wide layout. Selected by the hooks the hero already has.
2432
+ `scripts/test-hero-grows-on-phone.ts` resolves every line at 799, 800 and
2433
+ 801. */
2434
+ section:not([data-slide-name]):has(> .sf-hero-group-layer),
2435
+ div:has(> .hero-track) {
2436
+ display: flex;
2437
+ flex-direction: column;
2438
+ height: auto;
2439
+ min-height: var(--sf-hero-h-m, max(100vh, 680px));
2440
+ }
2441
+ .hero-track {
2442
+ height: auto;
2443
+ flex: 1 1 auto;
2444
+ }
2445
+ .hero-track > * {
2446
+ height: auto;
2447
+ }
2448
+ section[data-slide-name] {
2449
+ display: flex;
2450
+ flex-direction: column;
2451
+ height: auto;
2452
+ }
2453
+ .sf-hero-group-layer {
2454
+ position: relative;
2455
+ flex: 1 1 auto;
2456
+ width: 100%;
2457
+ }
2458
+ }
2459
+ /* …and WHICH height is the minimum between 720 and 800: the desktop one, as
2460
+ the builder's own `min-[720px]:h-[var(--sf-hero-h-d)]` says (VIEW_TABLET_MIN,
2461
+ the line the Height control switches on). Only the height is chosen here;
2462
+ the layout above is on the hero's one line, 800. */
2463
+ @media (min-width: 720px) and (max-width: 800px) {
2464
+ section:not([data-slide-name]):has(> .sf-hero-group-layer),
2465
+ div:has(> .hero-track) {
2466
+ min-height: var(--sf-hero-h-d, max(100vh, 680px));
2467
+ }
2468
+ }
2469
+
2470
+ /* ── NEGATIVE GAP = THE BLOCKS OVERLAP (card ss.negative-gap) ────────────────
2471
+ The other half of `containerGapProps` in lib/content/container-gap.ts, which
2472
+ is where the whole argument for this shape is written down. In short: CSS
2473
+ `gap` rejects a negative length outright — `gap: -12px` is an invalid
2474
+ declaration, the browser drops it, and the page lays out exactly as it does at
2475
+ `gap: 0` (measured in Chromium 2026-09-03). So below zero the render stops
2476
+ asking `gap` and pulls the blocks together with a margin instead.
2477
+
2478
+ HALF THE OVERLAP ON EVERY CHILD, and the same half given back to the slot as
2479
+ padding. Two neighbours each move half way, so they meet the full overlap
2480
+ inside each other, and the group's own outer edge does not move at all. The
2481
+ half arrives as `--sf-gap-overlap`, because it is a runtime number and a class
2482
+ cannot carry one.
2483
+
2484
+ ⚠️ NOT `> * + *`, which is one rule shorter and wrong twice: on a wrapping row
2485
+ it indents the first block of every line after the first (CSS cannot see that
2486
+ the line broke), and between grid rows it does nothing at all, because grid
2487
+ rows are tracks rather than siblings. Both measured failing first.
2488
+
2489
+ ONLY REACHED BELOW ZERO. Nothing on this page is emitted for a gap of 0 or
2490
+ more, which is what makes this card unable to move a single live site: the
2491
+ floor was 0 until today, so no stored document holds a negative.
2492
+
2493
+ UNLAYERED, like the hero rules above, so these beat the Tailwind utilities the
2494
+ same elements carry. */
2495
+ /* ── The gap between the blocks inside a container ────────────────────────────
2496
+ The LENGTH arrives as `--sf-gap-len` from lib/content/container-gap.ts, which
2497
+ sets it to either a stored number (`24px`) or, when the container is
2498
+ inheriting, the site token with that container's own built-in as the fallback
2499
+ (`var(--sf-block-gap, 24px)`). So the cascade does the inheriting and no site
2500
+ value is ever plumbed through React to get here.
2501
+
2502
+ ⚠️ HALVED BELOW 800px, UNCONDITIONALLY — owner's ruling 2026-09-03, chosen
2503
+ over "only when the merchant set a value" with the cost stated. Every live
2504
+ page's PHONE spacing therefore changes the day this ships: Grid and Box 24→12,
2505
+ Footer box 16→8, Events 24→12, Product range 8→4, the wheels listing 1rem→0.5rem.
2506
+ DESKTOP does not move by a pixel. This is the ruling, not a bug — anyone
2507
+ reading a "the gaps got smaller on mobile" report should stop here.
2508
+
2509
+ 800px is the breakpoint the Gallery already shipped a 2:1 step at
2510
+ (`gap-2 max-[800px]:gap-1`), so the one number every container now halves at
2511
+ is the one number one of them already used, rather than a sixth opinion.
2512
+
2513
+ `calc(… / 2)` rather than a second property from the render: the halving is a
2514
+ RULE, and a rule belongs in the stylesheet where it is stated once, not in
2515
+ four call sites that could each forget it.
2516
+
2517
+ UNLAYERED, like the overlap rules below, so these beat the Tailwind gap
2518
+ utilities the same elements still carry. */
2519
+ .sf-gap-zone {
2520
+ gap: var(--sf-gap-len);
2521
+ }
2522
+ @media (max-width: 800px) {
2523
+ .sf-gap-zone {
2524
+ gap: calc(var(--sf-gap-len) / 2);
2525
+ }
2526
+ }
2527
+
2528
+ /* The overlap below zero halves on the same screen, for the same reason and by
2529
+ the same ruling. It needs its OWN derived property rather than redefining
2530
+ `--sf-gap-overlap`: a custom property cannot be defined in terms of itself,
2531
+ so `--sf-gap-overlap: calc(var(--sf-gap-overlap) / 2)` is a cycle and CSS
2532
+ throws the whole declaration away — silently, which is the shape of bug this
2533
+ repo keeps writing guards for. One extra name, and the four rules below read
2534
+ it instead. */
2535
+ .sf-gap-overlap {
2536
+ --sf-gap-overlap-eff: var(--sf-gap-overlap);
2537
+ }
2538
+ @media (max-width: 800px) {
2539
+ .sf-gap-overlap {
2540
+ --sf-gap-overlap-eff: calc(var(--sf-gap-overlap) / 2);
2541
+ }
2542
+ }
2543
+ .sf-gap-overlap > * {
2544
+ /* WHICH BLOCK SITS ON TOP, pinned rather than left to chance — owner's pick
2545
+ 2026-09-03: the block LOWER in the Layers list paints over the one above it,
2546
+ and a merchant changes it by dragging in Layers.
2547
+
2548
+ That is already what CSS does between plain siblings, but only while every
2549
+ one of them is unpositioned: measured 2026-09-03, a column of two where the
2550
+ FIRST block alone sets `position: relative` paints the first ON TOP, which
2551
+ would make the answer depend on which block a merchant happened to drop.
2552
+ Positioning them all puts every child in the same class, where tree order
2553
+ decides again — measured restoring later-on-top. A block that sets its own
2554
+ `z-index` still wins, and that is left alone deliberately: it is a block
2555
+ saying something louder than a container default. */
2556
+ position: relative;
2557
+ }
2558
+ .sf-gap-overlap-block,
2559
+ .sf-gap-overlap-both {
2560
+ padding-block: var(--sf-gap-overlap-eff);
2561
+ }
2562
+ .sf-gap-overlap-block > *,
2563
+ .sf-gap-overlap-both > * {
2564
+ margin-block: calc(-1 * var(--sf-gap-overlap-eff));
2565
+ }
2566
+ .sf-gap-overlap-inline,
2567
+ .sf-gap-overlap-both {
2568
+ padding-inline: var(--sf-gap-overlap-eff);
2569
+ }
2570
+ .sf-gap-overlap-inline > *,
2571
+ .sf-gap-overlap-both > * {
2572
+ margin-inline: calc(-1 * var(--sf-gap-overlap-eff));
2573
+ }
2574
+
2575
+ /* ── Grid cell width (the cell-width card) ──────────────────────────────────
2576
+ How many of a Grid row's positions ONE block takes.
2577
+
2578
+ ⚠️ THE RULE IS WRITTEN TWICE ON PURPOSE, AND DELETING EITHER HALF BREAKS
2579
+ EXACTLY ONE OF THE TWO PLACES THIS RUNS. Puck 0.20 wraps every slot child in
2580
+ a drag element of its own unless the component declares `inline`
2581
+ (components/HeroGroup.tsx carries the measurement, card 66290). So:
2582
+
2583
+ published page .sf-grid-zone > [data-sf-span] the block itself is
2584
+ the grid item
2585
+ editor canvas .sf-grid-zone > *:has(> [data-sf-span]) Puck's wrapper is
2586
+ the grid item, and
2587
+ the attribute is one
2588
+ level inside it
2589
+
2590
+ A rule written only the first way is the defect the hero anchor shipped with
2591
+ for weeks: right on the live site, silently ignored in the editor, and it
2592
+ reads as a styling question rather than a wrapper one. `:has()` is how the
2593
+ width reaches the wrapper — it is the only direction CSS travels upward, and
2594
+ it is already this stylesheet's own idiom (see the lead-ink rules above and
2595
+ the overlay rules in storefront-editor.css).
2596
+
2597
+ NOT `display: contents` on the wrapper, which is the other way to make the
2598
+ block the grid item: Puck MEASURES that element to place a drop and to draw
2599
+ its selection overlay, and `display: contents` removes the box and takes the
2600
+ measurement with it. The same warning is written out at .sf-hero-group-zone.
2601
+
2602
+ A block with NO span stamps no attribute and matches nothing here, so it
2603
+ auto-places into one position exactly as it did before this card. */
2604
+ .sf-grid-zone {
2605
+ grid-template-columns: repeat(var(--sf-grid-cols, 3), minmax(0, 1fr));
2606
+ }
2607
+
2608
+ .sf-grid-zone > [data-sf-span="1"],
2609
+ .sf-grid-zone > *:has(> [data-sf-span="1"]) { grid-column: span 1; }
2610
+ .sf-grid-zone > [data-sf-span="2"],
2611
+ .sf-grid-zone > *:has(> [data-sf-span="2"]) { grid-column: span 2; }
2612
+ .sf-grid-zone > [data-sf-span="3"],
2613
+ .sf-grid-zone > *:has(> [data-sf-span="3"]) { grid-column: span 3; }
2614
+ .sf-grid-zone > [data-sf-span="4"],
2615
+ .sf-grid-zone > *:has(> [data-sf-span="4"]) { grid-column: span 4; }
2616
+ .sf-grid-zone > [data-sf-span="5"],
2617
+ .sf-grid-zone > *:has(> [data-sf-span="5"]) { grid-column: span 5; }
2618
+ .sf-grid-zone > [data-sf-span="6"],
2619
+ .sf-grid-zone > *:has(> [data-sf-span="6"]) { grid-column: span 6; }
2620
+ .sf-grid-zone > [data-sf-span="7"],
2621
+ .sf-grid-zone > *:has(> [data-sf-span="7"]) { grid-column: span 7; }
2622
+ .sf-grid-zone > [data-sf-span="8"],
2623
+ .sf-grid-zone > *:has(> [data-sf-span="8"]) { grid-column: span 8; }
2624
+ .sf-grid-zone > [data-sf-span="9"],
2625
+ .sf-grid-zone > *:has(> [data-sf-span="9"]) { grid-column: span 9; }
2626
+ .sf-grid-zone > [data-sf-span="10"],
2627
+ .sf-grid-zone > *:has(> [data-sf-span="10"]) { grid-column: span 10; }
2628
+ .sf-grid-zone > [data-sf-span="11"],
2629
+ .sf-grid-zone > *:has(> [data-sf-span="11"]) { grid-column: span 11; }
2630
+ .sf-grid-zone > [data-sf-span="12"],
2631
+ .sf-grid-zone > *:has(> [data-sf-span="12"]) { grid-column: span 12; }
2632
+
2633
+ /* The phone. A span is a DESKTOP statement — twelve positions across a phone is
2634
+ unreadable — so below the breakpoint every cell takes one position of a
2635
+ 1–3 column row. `grid-column: auto` beats the span rules above on order, not
2636
+ on specificity, which is why this block sits after them. */
2637
+ @media (max-width: 800px) {
2638
+ .sf-grid-zone {
2639
+ grid-template-columns: repeat(var(--sf-grid-cols-phone, 1), minmax(0, 1fr));
2640
+ }
2641
+ .sf-grid-zone > [data-sf-span],
2642
+ .sf-grid-zone > *:has(> [data-sf-span]) { grid-column: auto; }
2643
+ }
2644
+
2645
+ /* A cell's HEIGHT — the same wrapper as the span rules above, and the same two
2646
+ halves ([FIX] Grid Cell Card Stretches In The Editor):
2647
+
2648
+ published page .sf-grid-zone > * the block is the grid
2649
+ item, and the grid's
2650
+ own default stretch
2651
+ makes it the row height.
2652
+ No rule: there is
2653
+ nothing to add.
2654
+ editor canvas .sf-grid-zone > [data-puck-component] the editor's drag
2655
+ wrapper is the grid
2656
+ item. IT stretches;
2657
+ the block inside it
2658
+ is a plain block, as
2659
+ tall as its words.
2660
+
2661
+ Measured on the Hartes homepage (first Grid, 1440): the white card was 774px
2662
+ live and 638px in the canvas, so the black page showed under it.
2663
+
2664
+ The editor half makes the wrapper a grid of ONE column. Its child — or, through
2665
+ a `display: contents` host the editor adds, its grandchild — is then a grid
2666
+ item, and a grid item stretches to its area, so the block is the cell's height
2667
+ exactly as it is live. `minmax(0, 1fr)` keeps the width what a block child had:
2668
+ the wrapper's own width, allowed to shrink below its content.
2669
+
2670
+ Matched on the attribute, not with `:has()` as the span rules are: a span is
2671
+ a property of the BLOCK, so the rule has to look inside; a stretch is a
2672
+ property of EVERY wrapper in this zone. The published page never carries the
2673
+ attribute, so this cannot touch a live layout. A block declared `inline`
2674
+ carries the attribute on its own root and would become a grid here — none is
2675
+ droppable into a Grid, and scripts/test-cell-stretch.ts holds that.
2676
+
2677
+ Still NOT `display: contents` on the wrapper — see the warning on the span
2678
+ rules above: the editor measures that element for drops and the outline. */
2679
+ .sf-grid-zone > [data-puck-component] {
2680
+ display: grid;
2681
+ grid-template-columns: minmax(0, 1fr);
2682
+ }
2683
+
2684
+ /* …and the same HEIGHT in a Box laid out in one ROW, and in a Slide box set to
2685
+ One row ([FIX] Box Row Children Stretch In The Editor). The same wrapper, the
2686
+ same two halves, one container over:
2687
+
2688
+ published page row > * the block is the flex
2689
+ item, and the row's
2690
+ stretch makes it the
2691
+ row height. No rule.
2692
+ editor canvas row > [data-puck-component] the drag wrapper is
2693
+ the flex item. IT
2694
+ stretches; the block
2695
+ inside it is as tall
2696
+ as its words.
2697
+
2698
+ Measured with scripts/probe-row-stretch.tsx on the real Box markup: a block
2699
+ beside a 774px block was 774px live and 638px in the canvas, for a Box row
2700
+ with Align unset or Stretch and for a Slide box row.
2701
+
2702
+ WHICH ROWS. A row stretches unless it was told where to sit, so the selector
2703
+ is "a row with no Start, Center or End on it". That covers Stretch, and an
2704
+ Align never stored (every older Box, every Slide box — the flex default is
2705
+ stretch). A Box with a picture always carries an anchor, so it never matches.
2706
+ The row class is a general one, so the reach was counted in the live config
2707
+ (every block with a slot, rendered): only three zones ever wear it — the Box,
2708
+ the Slide box, and a Footer group set to Horizontal, whose panel always
2709
+ stores Start, Center or End.
2710
+ A Stacked box has no row class, so a column zone never matches — and that is
2711
+ on purpose: there the wrapper already fills the width, and a grid would also
2712
+ stop the block's margins collapsing out of it.
2713
+
2714
+ ⚠️ UNDER 800px A BOX ROW BECOMES A COLUMN AND KEEPS ITS ROW CLASS, so the rule
2715
+ still applies there (and in a Slide box row at 800px and under). That is
2716
+ measured, not assumed: at 390 every height and width is unchanged except one
2717
+ — a block whose words carry a margin was 618px in the canvas and 638px live,
2718
+ because the margin collapsed out of the block into the wrapper. The grid
2719
+ wrapper keeps it inside, so the canvas now draws 638 as live does. A Stacked
2720
+ box still draws 618 in the canvas; that is the column case above, left alone.
2721
+
2722
+ The class names are the ones lib/content/container-direction and
2723
+ lib/content/box-layout emit; scripts/test-cell-stretch.ts §4 reads them from
2724
+ there, so a renamed class goes red there instead of silently matching nothing
2725
+ here.
2726
+
2727
+ `100%` AND NOT THE CELL'S `minmax(0, 1fr)`, and the reason is the width. A
2728
+ grid cell is sized by the grid, so its wrapper may shrink below its words. A
2729
+ flex item is not: its smallest size is its content's, unless the row says
2730
+ otherwise. A `minmax(0, 1fr)` column makes the wrapper's smallest size zero,
2731
+ so a word wider than the row was squeezed in the canvas where live it runs
2732
+ out. A `100%` column is the content's size while the row is measuring, and
2733
+ exactly the wrapper's width once it is laid out — which is what a plain
2734
+ block child was. The probe holds both: every width is unchanged.
2735
+
2736
+ `:not([class])` — ONLY EASEL'S WRAPPER, NEVER A BLOCK'S OWN ROOT. A block
2737
+ declared `inline` (the Slide box) has no wrapper: Easel puts the SAME two
2738
+ attributes on the block's own root, which is already the flex item and
2739
+ already stretched, and must not become a grid. No attribute tells the two
2740
+ apart. Their shape does: Easel draws its wrapper as a bare div with no class
2741
+ (efficient-easel/easel), and every block root draws one. §4 holds both.
2742
+
2743
+ ⚠️ THREE `:not()`s, NOT ONE WITH A LIST, and no comma anywhere in the
2744
+ selector. scripts/css-rules.ts — the reader the hero guards resolve real
2745
+ classes with — splits a rule on its commas, so `:not(.a, .b)` reached it as a
2746
+ bare `.b`, and it handed `display: grid` to the hero's arrows and dot row. A
2747
+ browser reads both spellings the same; §4 holds the reader too.
2748
+
2749
+ Still NOT `display: contents` on the wrapper, for the reason above. */
2750
+ .flex-row:not(.items-start):not(.items-center):not(.items-end) > [data-puck-component]:not([class]) {
2751
+ display: grid;
2752
+ grid-template-columns: 100%;
2753
+ }
2754
+
2755
+ /* ── THE PHOTO WALL'S LAYOUT ────────────────────────────────────────────────
2756
+ The Customer Photo Grid draws one of `lib/content/gallery-layout.ts`'s
2757
+ patterns. THE PER-LAYOUT RULES ARE NOT HERE: nineteen layouts over two tracks
2758
+ is four hundred-odd nth-child rules, and every page would carry all of them.
2759
+ `galleryLayoutCss()` emits the chosen one beside the wall, which is this
2760
+ repo's own inline-<style> idiom (ProductGridSection, ContactLocations, FAQ).
2761
+ What lives here is only what every layout shares.
2762
+
2763
+ ⚠️ NOTHING BELOW APPLIES UNTIL A LAYOUT IS CHOSEN. The class is stamped only
2764
+ by the chosen-layout branch, so every published gallery — all of them today —
2765
+ renders the three-step wall it always has, byte for byte.
2766
+
2767
+ ⚠️ THE ROW HEIGHT IS THE WHOLE REASON THIS BLOCK EXISTS. A tile sizes itself
2768
+ from its WIDTH (`galleryCardRatioClass`, square by default), so a wide photo
2769
+ is automatically a tall photo and "span two rows" would mean nothing. Inside a
2770
+ wall the CELL decides instead: the rows are a fixed height, a two-row photo is
2771
+ exactly twice a one-row photo, and the card stretches to fill whatever it was
2772
+ given. That is what makes a feature photo actually big rather than merely
2773
+ wide.
2774
+
2775
+ The card's own ratio is overridden by specificity, not by `!important` —
2776
+ `.sf-gallery-zone .sf-card` is two classes against Tailwind's one. */
2777
+ .sf-gallery-zone {
2778
+ display: grid;
2779
+ grid-auto-flow: row dense;
2780
+ /* One row of the wall. Scales with the viewport so a wall is not a strip of
2781
+ stamps on a wide screen nor a stack of billboards on a small one. */
2782
+ grid-auto-rows: clamp(140px, 15vw, 260px);
2783
+ }
2784
+
2785
+ /* A cell may be narrower than its content wants; without this a long caption
2786
+ pushes the column wider and the whole wall drifts off the pattern. */
2787
+ .sf-gallery-zone > * {
2788
+ min-width: 0;
2789
+ }
2790
+
2791
+ /* The tile fills the cell it was given, through Puck's wrapper in the editor and
2792
+ directly on the published page — the same "both are the grid item" fact the
2793
+ nth-child rules rely on. */
2794
+ .sf-gallery-zone > *,
2795
+ .sf-gallery-zone > * > * {
2796
+ height: 100%;
2797
+ }
2798
+
2799
+ /* …and the card stops deciding its own shape, because the layout decided it. */
2800
+ .sf-gallery-zone .sf-card {
2801
+ aspect-ratio: auto;
2802
+ height: 100%;
2803
+ }
2804
+
2805
+ @media (max-width: 800px) {
2806
+ .sf-gallery-zone {
2807
+ grid-auto-rows: clamp(90px, 26vw, 150px);
2808
+ }
2809
+ }
2810
+
2811
+ /* ── A SECTION'S OWN BOX, ROUNDED ───────────────────────────────────────────
2812
+ Lewis, 2026-09-04: the corner setting is for the BOX. Before this it was not:
2813
+ `sectionCornerStyle` publishes the radius as custom properties onto a carrier
2814
+ that is `display: contents`, so the value reached anything INSIDE the section
2815
+ that already read a radius — a card, a `rounded-*` utility — and never the
2816
+ section's own edge. Measured the same day on origin/main: of the 71 sections
2817
+ carrying the Shape group, only 20 wear `.sf-card` and had anything inside to
2818
+ catch the value at all. The other 51 include Hero Slideshow, Collections, Stats
2819
+ and Gallery, and he found it by setting 27px on a Hero and watching nothing
2820
+ happen. (Re-measured 2026-09-05; an earlier 22/49 was taken before main moved.)
2821
+
2822
+ ⚠️ THE CARRIER STAYS `display: contents`, AND THIS IS WHY IT CAN. The obvious
2823
+ fix — make the carrier a real box and put the radius on it — is the one thing
2824
+ `lib/puck/config.tsx` says not to do: a block box between a section and its
2825
+ parent breaks a full-viewport cover's `absolute inset-0` and any sticky child.
2826
+ A `display: contents` element generates no box but IS still in the DOM for
2827
+ selectors, and its child is the section's own root — measured 2026-09-05 by
2828
+ walking the rendered React tree of every one: ALL 71 carriers hold exactly one
2829
+ element, none holds more, and none failed to render. (An earlier count of 64
2830
+ was a limit of the measurement, not of the sections.) So this paints the box the section already had rather than adding one,
2831
+ and the layout is untouched at every value.
2832
+
2833
+ ⚠️ AND IT ONLY EXISTS WHEN THE SECTION HAS CHOSEN. The carrier is emitted only
2834
+ for a section with its own corner (an inheriting one returns the render
2835
+ untouched), so this selector matches nothing on an untouched page and the
2836
+ zero-regression claim `test-section-corners` makes is unchanged.
2837
+
2838
+ `overflow: clip` for the reason `.sf-card` uses it rather than `hidden`: a
2839
+ rounded box whose photo runs to its own edge would otherwise wear a square
2840
+ image over a round corner, and `clip` does that without making a scroll
2841
+ container — so a sticky child inside a section keeps working.
2842
+
2843
+ ⚠️ IT READS `--sf-section-radius`, NOT THE CARD PROPERTY, AND THAT IS THE
2844
+ WHOLE OF THE TWO-TIER SPLIT. It read `--sf-card-radius` for one day, which
2845
+ meant the section's own edge and every card inside it were one number and
2846
+ could never differ — Lewis asked for a Product Grid rounded one way with its
2847
+ product cards rounded another (2026-09-04). The two properties are named and
2848
+ explained in lib/content/section-corners.ts. That control now exists — a row on
2849
+ the CARD BLOCK, where Lewis ruled on 2026-09-05 it belongs — and it writes a
2850
+ THIRD property, `--sf-card-box-radius`, so this rule is still the section's own
2851
+ edge and nothing else. The fallback `0` costs nothing today and is what stops
2852
+ this rule painting a box on any future carrier that arrives without a section
2853
+ radius. */
2854
+ .sf-section-corners > * { border-radius: var(--sf-section-radius, 0); overflow: clip; }
2855
+
2856
+ /* ── A SECTION'S (AND A BOX'S) OWN BORDER ────────────────────────────────────
2857
+ Box Border, approved by Lewis 2026-10-15. efficient-web-builder 17.0.0's one
2858
+ resolver (`lib/content/borders.ts`) publishes the Border a section or a Box
2859
+ sets as custom properties on a `display: contents` carrier,
2860
+ `.sf-section-border`, emitted ONLY when the section has a border of its own —
2861
+ so this selector matches nothing on an untouched page. The line is drawn on
2862
+ the carrier's CHILD, the section's own box, as the radius above is.
2863
+
2864
+ Only what was set is published, so each `var()` falls back to "nothing set":
2865
+ an unset side is 0 (no line), an unset style is solid, an unset colour is the
2866
+ site's Borders colour (`--site-line`, the theme's `line`). A Box inside a
2867
+ section that has a border inherits the section's properties for whatever it
2868
+ did not set itself: the Box follows the tier above, as Corners does.
2869
+
2870
+ On a phone the `-phone` properties come first and the desktop ones are the
2871
+ fallback, so an unset phone value follows the desktop. The boundary is the
2872
+ phone padding's below (`not all and (min-width: 720px)`, lib/content/
2873
+ view-tiers' VIEW_TABLET_MIN). scripts/test-section-border-css.ts.
2874
+
2875
+ `.sf-section-border-self` is the same line drawn on the block's OWN root:
2876
+ efficient-web-builder 18.2.0 puts it there for a Box inside a row or another
2877
+ Box, where a carrier would take the row's place and break its layout. The
2878
+ properties sit on that root itself, so the rule reads them the same way. */
2879
+ .sf-section-border > *,
2880
+ .sf-section-border-self {
2881
+ border-style: var(--sf-border-style, solid);
2882
+ border-color: var(--sf-border-color, var(--site-line));
2883
+ border-width: var(--sf-border-top-width, 0) var(--sf-border-right-width, 0) var(--sf-border-bottom-width, 0) var(--sf-border-left-width, 0);
2884
+ }
2885
+ @media not all and (min-width: 720px) {
2886
+ .sf-section-border > *,
2887
+ .sf-section-border-self {
2888
+ border-style: var(--sf-border-style-phone, var(--sf-border-style, solid));
2889
+ border-color: var(--sf-border-color-phone, var(--sf-border-color, var(--site-line)));
2890
+ border-width: var(--sf-border-top-width-phone, var(--sf-border-top-width, 0)) var(--sf-border-right-width-phone, var(--sf-border-right-width, 0)) var(--sf-border-bottom-width-phone, var(--sf-border-bottom-width, 0)) var(--sf-border-left-width-phone, var(--sf-border-left-width, 0));
2891
+ }
2892
+ }
2893
+
2894
+ /* ── A SECTION THAT RUNS EDGE TO EDGE HANDS ITS CARDS BACK TO THE SITE ────────
2895
+ Lewis, 2026-09-10: a card stops following its section when the section
2896
+ ACTUALLY RENDERS EDGE TO EDGE. Such a section has no visible edges, so its
2897
+ corner is off the page and passing that number down would round the cards by
2898
+ a value whose source the merchant cannot see. The class is put on the section's
2899
+ own corners carrier by the injection pass in lib/puck/config.tsx, off
2900
+ `sectionRunsFullWidth` — NEVER off a bare `mode === "full"`, which reads every
2901
+ untouched section as full. lib/content/card-corners.ts carries the whole
2902
+ ruling, including why the site's Hero image switch does not enter it.
2903
+
2904
+ ⚠️ IT LEADS WITH THE CARD'S OWN PROPERTY, and that is what makes source order
2905
+ irrelevant: a card that answered for itself wins here exactly as it wins in
2906
+ `.sf-card` above, whichever rule the cascade happens to pick.
2907
+
2908
+ ⚠️ AND THE FALLBACK IS `--sf-site-card-radius`, NOT `--sf-card-radius`. Inside
2909
+ a section that has its own corner, `--sf-card-radius` IS the section's answer —
2910
+ that is the whole two-tier split — so reaching past it needs a name only the
2911
+ two roots publish. */
2912
+ .sf-section-bleeds .sf-card { border-radius: var(--sf-card-box-radius, var(--sf-site-card-radius, 0)); }
2913
+
2914
+ /* ── A Scrolling Marquee that sits INSIDE the section next to it ──────────────
2915
+ `lib/sections/ticker-join.ts` carries the why. The short of it: a full-screen
2916
+ section is 100vh, so a marquee under it lands below the fold; joined, the
2917
+ strip takes NO page height and is pinned to the boundary instead, which puts
2918
+ it inside that first screen. The Hero's own strip has always done this with
2919
+ `absolute inset-x-0 bottom-0` (components/HeroSection.tsx) — the box below is
2920
+ what lets a SIBLING section do the same, by putting a containing block on the
2921
+ seam for it to hang off.
2922
+
2923
+ ⚠️ THE `z-index` IS ON THE BOX, NOT ON THE STRIP, and moving it back breaks
2924
+ the thing it protects. The strip's own `z-[2]` (PR #515) lifts it over a
2925
+ pinned Scroll-Over Intro, but a z-index inside a positioned box only orders
2926
+ the box's own children — it cannot lift the pair past a SIBLING section. So
2927
+ the pair's stacking order has to be declared here, on the element that is
2928
+ actually the sibling, and it is the same 2 for the same reason.
2929
+
2930
+ ⚠️ `height: 0` MUST NOT BECOME `height: 0; overflow: hidden`. The strip is
2931
+ entirely outside its box on purpose — that is what "inside the section next
2932
+ to it" means — so any clipping on this element erases the marquee. See the
2933
+ corners rule directly below, which exists because the section-corners carrier
2934
+ would have done exactly that. */
2935
+ .sf-ticker-join { position: relative; height: 0; z-index: 2; }
2936
+ .sf-ticker-join-above { bottom: 0; }
2937
+ .sf-ticker-join-below { top: 0; }
2938
+
2939
+ /* ⛔ THE CORNERS CARRIER WOULD HAVE CLIPPED THE STRIP AWAY, and this is the two
2940
+ lines that stop it. `.sf-section-corners > *` (above) rounds and CLIPS a
2941
+ section's outermost box; for a joined marquee that box is the zero-height one,
2942
+ so `overflow: clip` on a 0px-tall element deleted the whole marquee from the
2943
+ page while the panel still reported a corner radius. Lewis asked on 2026-09-07
2944
+ that the border and the corners stay the marquee's own settings, so the fix is
2945
+ to move the rounding one level in — onto the strip, which is the box a
2946
+ merchant is actually looking at — rather than to exclude Ticker from the
2947
+ corners pass and take the control away.
2948
+
2949
+ The specificity is (0,2,0) against the (0,2,0) of the rule above, so these
2950
+ come LAST on purpose: source order is what decides, exactly as the
2951
+ `.sf-section-surface-*` pairs in this file are ordered. */
2952
+ .sf-section-corners > .sf-ticker-join { border-radius: 0; overflow: visible; }
2953
+ .sf-section-corners > .sf-ticker-join > * { border-radius: var(--sf-section-radius, 0); overflow: clip; }
2954
+
2955
+ /* ── A section's phone padding ────────────────────────────────────────────────
2956
+ The phone half of the override ruled by the owner on 2026-09-07: ONE value
2957
+ used on both screens, and a setting MAY also carry a phone value. Set, the
2958
+ phone uses it; unset, the phone follows the desktop value.
2959
+
2960
+ "Unset follows the desktop" is expressed by the ABSENCE of these classes, not
2961
+ by a fallback inside `var()`. lib/content/section-padding.ts emits a class
2962
+ only for the axis that actually has a phone value, so a section with no phone
2963
+ value carries no class, matches no rule here, and renders byte-identically to
2964
+ before this block existed.
2965
+
2966
+ ⚠️ `!important` IS LOAD-BEARING, and it is the reason for the class. The
2967
+ desktop padding is an INLINE style on the same element (sectionPadStyle), and
2968
+ an inline style beats any stylesheet declaration that is not `!important`.
2969
+
2970
+ ⚠️ NO FALLBACK IN THE `var()`, for the same reason the grid block above gives:
2971
+ the class and the property are emitted together, so a fallback here would be a
2972
+ second, invisible answer to a question the render already answered.
2973
+
2974
+ The boundary IS lib/content/view-tiers' VIEW_TABLET_MIN. Written as `not all
2975
+ and (min-width: 720px)` rather than a `max-width` so the literal is the SAME
2976
+ 720 the two blocks above use, with no fractional-pixel gap to get wrong. */
2977
+ @media not all and (min-width: 720px) {
2978
+ .sf-pad-top-phone {
2979
+ padding-top: var(--sf-pad-t-phone) !important;
2980
+ }
2981
+ .sf-pad-bottom-phone {
2982
+ padding-bottom: var(--sf-pad-b-phone) !important;
2983
+ }
2984
+ /* The sideways pair, and ONLY a Box standing on a picture ever wears these.
2985
+ Owner, 2026-09-09: the card's left and right inner padding has to be
2986
+ settable. No section has a sideways padding and these classes do not give
2987
+ it one — nothing emits them but `components/Flex` for a card. */
2988
+ .sf-pad-left-phone {
2989
+ padding-left: var(--sf-pad-l-phone) !important;
2990
+ }
2991
+ .sf-pad-right-phone {
2992
+ padding-right: var(--sf-pad-r-phone) !important;
2993
+ }
2994
+ /* A picture's PHONE focus (Lewis 2026-10-19: one picture, one dot; the view
2995
+ you drag in sets that view's point). The same split as the padding above:
2996
+ the desktop point is the picture's INLINE `object-position`, and the
2997
+ builder's `phoneFocus` (efficient-web-builder lib/content/phone-focus)
2998
+ adds this class and its variable only when a phone point is set, so a
2999
+ picture without one matches nothing here and draws the bytes it drew. */
3000
+ .sf-focus-phone {
3001
+ object-position: var(--sf-focus-phone) !important;
3002
+ }
3003
+ }
3004
+
3005
+ /* ── A card owns the room at its own sides ────────────────────────────────────
3006
+ Owner, 2026-09-09, choosing A over B when both were drawn: the card owns it,
3007
+ and the number a merchant sets is the only one.
3008
+
3009
+ ⛔ WHAT THIS UNDOES, AND WHY IT IS NOT A HACK. A block inside a Box still
3010
+ renders its own BlockFrame, and that frame draws the SITE's Side space — so
3011
+ the words on a card sat 40px in from the picture on a wide screen and 24px in
3012
+ on a phone, from a frame that belongs to the block rather than to the card.
3013
+ Stacked under the Box's own frame gutter it read as 80px at the default Width,
3014
+ and none of it was reachable from any panel.
3015
+
3016
+ It is the same defect the owner already ruled on for the axis next door
3017
+ (2026-09-04: a block inside a box does not answer width, the box does). That
3018
+ ruling took the CONTROL away and left the BEHAVIOUR, which is why the gutter
3019
+ was still being drawn.
3020
+
3021
+ ⚠️ SCOPED TO A CARD'S OWN CONTENT LAYER, not to every Box. The class is
3022
+ emitted only where there is a background picture, so an ordinary layout Box
3023
+ and every block on every page are untouched.
3024
+
3025
+ ⚠️ AND IT ZEROES ONLY THE SIDES. `padding-top`/`bottom` on a block are its own
3026
+ spacing control and are left alone; blocks have no sideways spacing control at
3027
+ all, so there is nothing here a merchant could have set. */
3028
+ .sf-box-sides > * {
3029
+ padding-left: 0;
3030
+ padding-right: 0;
3031
+ }
3032
+
3033
+ /* ── A section's height on a phone ────────────────────────────────────────────
3034
+ The phone half of the Height group. Lewis chose 甲 on 2026-09-08: the phone
3035
+ overrides the group AS ONE UNIT, so a section either follows the desktop
3036
+ completely or holds its own mode + number + unit.
3037
+
3038
+ "Follows the desktop" is the ABSENCE of this class, not a fallback inside
3039
+ `var()` — lib/content/section-min-height.ts emits the class only when the node
3040
+ stores a phone MODE, so a section without one matches no rule here and renders
3041
+ byte-identically to before this block existed.
3042
+
3043
+ ⚠️ `display: grid` LIVES HERE, NOT IN THE INLINE STYLE, whenever the phone is
3044
+ the only screen with a height. It is what makes a short section stretch to its
3045
+ minimum, and an inline copy would apply at every width — turning a section the
3046
+ merchant left on `Auto` for the desktop from a block into a grid there. Here it
3047
+ reaches the phone alone.
3048
+
3049
+ ⚠️ `!important` because the desktop's height is an INLINE style on the same
3050
+ element, and an inline style beats any stylesheet rule without it.
3051
+
3052
+ The boundary IS lib/content/view-tiers' VIEW_TABLET_MIN — written as `not all
3053
+ and (min-width: 720px)` so the literal is the same 720 the tier blocks above
3054
+ use. The `@supports` leg mirrors the desktop pair: `svh` where the browser has
3055
+ it, the plain unit as the fallback. */
3056
+ @media not all and (min-width: 720px) {
3057
+ .sf-section-minh-phone {
3058
+ display: grid !important;
3059
+ min-height: var(--sf-section-minh-p) !important;
3060
+ }
3061
+ @supports (min-height: 1svh) {
3062
+ .sf-section-minh-phone {
3063
+ min-height: var(--sf-section-minh-ps, var(--sf-section-minh-p)) !important;
3064
+ }
3065
+ }
3066
+ }
3067
+
3068
+ /* ── A BOX WHOSE GAP IS THE ONLY SPACING ────────────────────────────────────
3069
+ Owner, 2026-09-11: 「为什么 eyebrow 没有padding, 别的text有?」 Every block frames
3070
+ itself with its own vertical padding and no two agree (none / 16 / 24 / 48),
3071
+ so the space between two blocks in a Box has three sources — the block above,
3072
+ the Gap, and the block below. This is the Box saying its Gap is the only one.
3073
+
3074
+ ⚠️ UNLAYERED ON PURPOSE. Written as `[&>*]:py-0` it would be a Tailwind
3075
+ utility, landing in `@layer utilities` beside the `py-4` it must beat, at the
3076
+ same specificity — decided by emission order, with the larger value later. It
3077
+ would do nothing, silently. Unlayered CSS beats every layered rule whatever
3078
+ its specificity, which is why this is here and not a class in the markup.
3079
+
3080
+ `padding-block`, not `padding`: a block's own SIDE padding is its business and
3081
+ several blocks read the site gutter through it. */
3082
+ .sf-box-gap-only > * {
3083
+ padding-block: 0;
3084
+ }
3085
+
3086
+ /* …and a block with its OWN padding is still a block of the Box. Lewis
3087
+ 2026-10-16: a Heading or Text has its own padding (Left/Right/Top/Bottom),
3088
+ drawn by the builder's wrapper, `div.sf-section-pad`, AROUND the block's
3089
+ frame. That wrapper is then the Box's direct child, so the rule above zeroes
3090
+ the wrapper and the frame's own `py-4` came back: +32px on the Hartes US
3091
+ footer tagline given 20px on its right. This reaches the frame inside it. The
3092
+ wrapper's own padding is an INLINE style, which this never beats, so what the
3093
+ merchant typed still draws. Unlayered, like the rule above, for its reason. */
3094
+ .sf-box-gap-only > .sf-section-pad > * {
3095
+ padding-block: 0;
3096
+ }
3097
+
3098
+ /* ── A SECTION ANIMATES IN ─────────────────────────────────────────────────────
3099
+ card [ADD] A Section Can Animate In — step two of three. Step one taught the
3100
+ site to answer `prefers-reduced-motion` (#1364, the rule directly below this
3101
+ one); step three draws the Animation tab. This is the motion itself.
3102
+
3103
+ THREE MOTIONS, and the three ideas behind them are written in
3104
+ `lib/content/section-animation.ts` rather than repeated here. What belongs
3105
+ HERE, and nowhere else, is the timing: CSS is what runs it, so the duration,
3106
+ the curve and the distance live in exactly one place and no TypeScript
3107
+ constant restates them. Nothing times this animation from JS — the wrapper
3108
+ ends on the element's own `animationend`.
3109
+
3110
+ ⚠️ EVERY MOTION DECLARES A `from` AND NO `to`, AND THAT IS LOAD-BEARING.
3111
+ A keyframe list with no `to` animates to the element's OWN computed style, so
3112
+ the resting appearance of a section is never described twice. The alternative
3113
+ — `to { opacity: 1; transform: none }` — is a second statement of how the
3114
+ section looks, and it is the statement that would be wrong the first time a
3115
+ section arrives already carrying a transform of its own.
3116
+
3117
+ ⚠️ AND IT IS WHY A DROPPED `animationend` COSTS NOTHING. With
3118
+ `animation-fill-mode: both` the element rests on the last keyframe, and the
3119
+ last keyframe IS its natural appearance. The wrapper clears the state to drop
3120
+ the compositor layer, not to make the content visible.
3121
+
3122
+ ── HOW THE REDUCE-MOTION RULE BELOW REACHES THIS ────────────────────────────
3123
+ It is not asked about anywhere. The rule below sets
3124
+ `animation-duration: 1ms !important` on everything, so an armed section plays
3125
+ its one pass in a millisecond and lands at rest — i.e. it is simply there,
3126
+ which is what "reduce motion" means. `!important` is why order does not
3127
+ matter and why these rules may sit above it. `scripts/probe-section-animation.mjs`
3128
+ reads that back out of a real Chrome with the media feature emulated instead
3129
+ of trusting this paragraph.
3130
+
3131
+ ⛔ AND THAT IS ALSO WHY THE HIDDEN STATE IS NOT A KEYFRAME'S JOB. `armed` is
3132
+ a plain `opacity: 0` written by JS, never by the server, and only ever onto a
3133
+ section that is below the fold with an observer already watching it. See
3134
+ `SECTION_ANIM_STATE` in lib/content/section-animation.ts. */
3135
+ @keyframes sf-section-fade {
3136
+ from { opacity: 0; }
3137
+ }
3138
+ @keyframes sf-section-rise {
3139
+ from { opacity: 0; transform: translateY(24px); }
3140
+ }
3141
+ @keyframes sf-section-scale {
3142
+ from { opacity: 0; transform: scale(0.96); }
3143
+ }
3144
+
3145
+ [data-sf-anim][data-sf-anim-state="armed"] {
3146
+ opacity: 0;
3147
+ }
3148
+
3149
+ [data-sf-anim][data-sf-anim-state="play"] {
3150
+ animation-duration: 600ms;
3151
+ /* Ease-out. A section arrives quickly and settles — the shape every entrance
3152
+ in the house drawings has, and the inverse of the ease-in a thing leaving
3153
+ would take. */
3154
+ animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
3155
+ animation-fill-mode: both;
3156
+ }
3157
+
3158
+ [data-sf-anim="fade"][data-sf-anim-state="play"] {
3159
+ animation-name: sf-section-fade;
3160
+ }
3161
+ [data-sf-anim="rise"][data-sf-anim-state="play"] {
3162
+ animation-name: sf-section-rise;
3163
+ }
3164
+ [data-sf-anim="scale"][data-sf-anim-state="play"] {
3165
+ animation-name: sf-section-scale;
3166
+ }
3167
+
3168
+ /* ── THE VISITOR'S OWN "REDUCE MOTION" SETTING ──────────────────────────────
3169
+ [ADD] The Site Respects Reduce Motion.
3170
+
3171
+ "Reduce motion" is a switch on every phone and laptop. People turn it on
3172
+ because movement on a screen makes them ill — vestibular disorders, migraine,
3173
+ motion sickness — not because they dislike animation. The browser hands the
3174
+ answer to CSS for free and, until this rule, this site threw it away.
3175
+
3176
+ Measured on main, 2026-09-11: 95 files under app/ components/ lib/ carry a
3177
+ `transition-` utility and 3 of them guard it. So 92 files moved for everyone,
3178
+ always. `components/useReducedMotion.ts` is the JS half of this answer and
3179
+ has been here for a while; this is the CSS half it was always missing.
3180
+
3181
+ ── WHAT "REDUCE" MEANS HERE, AND WHY IT IS NOT "KILL EVERYTHING" ──────────
3182
+ The crude rule — every duration to zero — is the one most sites ship, and it
3183
+ is wrong in a way that is easy to miss: it also kills the fades. A focus ring
3184
+ that snaps into place, a menu whose background flicks from one colour to
3185
+ another, a dialog that appears with no fade at all — those transitions carry
3186
+ no movement, so they harm nobody, and removing them makes the interface
3187
+ harder to follow rather than kinder.
3188
+
3189
+ So this rule sorts transitions by WHAT THEY ANIMATE, not by how long they
3190
+ take. `transition-property` is replaced with a fixed list of properties that
3191
+ change only ink and light. Everything the author asked for that is not on the
3192
+ list — transform, translate, height, top, margin, background-position, the
3193
+ whole family of movement — is simply no longer a transitioned property, so it
3194
+ changes instantly. The author's own duration and easing survive on the fades.
3195
+
3196
+ A disclosure that animated its height now opens in one step. That is the
3197
+ intended result and not a regression: a growing box pushes every word below
3198
+ it down the screen, which is exactly the movement being asked about.
3199
+
3200
+ ── ANIMATIONS ────────────────────────────────────────────────────────────
3201
+ A keyframe list cannot be sorted this way — CSS cannot see inside one — so
3202
+ every animation is collapsed to a single 1ms pass. Two decisions in that:
3203
+
3204
+ · NOT `animation: none`. An element whose static styles are invisible and
3205
+ which becomes visible through a `forwards` fill would then never appear.
3206
+ Nothing in this repo is built that way today, but a one-line rule that
3207
+ can hide content later is the wrong default. Running the animation to its
3208
+ end instead always lands where the author meant it to land.
3209
+ · 1ms rather than 0s, so a JS handler waiting on `animationend` still gets
3210
+ its event and does not hang a component half-open.
3211
+
3212
+ The six looping marquees here (the four ticker directions, the integrations
3213
+ wall, the editor's thumbnail strip) each end one iteration exactly one list
3214
+ width along, which is the seam — so they stop where they started and read as
3215
+ a still image, which is the point.
3216
+
3217
+ ── ⚠️ UNLAYERED AND `!important`, BOTH LOAD-BEARING ───────────────────────
3218
+ The card that opened this work asked for the rule inside `@layer base`, on
3219
+ the reasoning that a component needing to keep a motion could then win with a
3220
+ utility. That is the right instinct about escape hatches and the wrong
3221
+ mechanism, and it would have shipped a rule that does nothing at all:
3222
+
3223
+ · `@import "tailwindcss"` orders the layers base → components → utilities,
3224
+ so EVERY Tailwind utility outranks everything in `@layer base` whatever
3225
+ its specificity. All 92 unguarded files move via utilities. Layered here,
3226
+ this rule would lose to every single one of them, silently, and the only
3227
+ way to find out would be to read a computed style.
3228
+ · `@layer utilities` is no better: `*` is specificity 0,0,0 and loses to the
3229
+ `.transition-all` beside it.
3230
+ · Unlayered, it beats all of them — the same cascade fact `.sf-box-gap-only`
3231
+ above relies on, and the inverse of the one `scripts/test-css-layers.ts`
3232
+ guards. That guard is about bare-element RESETS and does not reach `*`;
3233
+ it would not have caught this being layered. `test-reduced-motion.ts`
3234
+ does.
3235
+
3236
+ `!important` then covers the two things being unlayered does not reach: the
3237
+ unlayered class rules in this very file, and inline styles. Three components
3238
+ set an animation inline — the search dialog, the cart drawer, the subscribe
3239
+ popup — and an inline style beats any stylesheet rule that is not important.
3240
+
3241
+ ── THE ESCAPE HATCH ──────────────────────────────────────────────────────
3242
+ `data-motion-keep` on an element exempts it and its subtree. It is written
3243
+ into the selector rather than as a second rule that reverts, so there is no
3244
+ cascade puzzle to solve when reading it, and one grep finds every exception.
3245
+ Nothing carries it today. A motion that genuinely must survive this setting
3246
+ is rare enough to be argued for in review, which is what the attribute makes
3247
+ someone do. */
3248
+ @media (prefers-reduced-motion: reduce) {
3249
+ *:not([data-motion-keep], [data-motion-keep] *),
3250
+ *:not([data-motion-keep], [data-motion-keep] *)::before,
3251
+ *:not([data-motion-keep], [data-motion-keep] *)::after {
3252
+ /* Ink and light only. Nothing on this list moves anything on the screen. */
3253
+ transition-property:
3254
+ opacity, color, background-color, border-color, outline-color,
3255
+ text-decoration-color, caret-color, fill, stroke, box-shadow, filter,
3256
+ backdrop-filter !important;
3257
+ animation-duration: 1ms !important;
3258
+ animation-delay: 0ms !important;
3259
+ animation-iteration-count: 1 !important;
3260
+ /* Beats the `html { scroll-behavior: smooth }` reset in @layer base. A
3261
+ scroll the visitor did not ask for is movement like any other. */
3262
+ scroll-behavior: auto !important;
3263
+ }
3264
+ }