@repobit/dex-system-design 0.25.12 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (336) hide show
  1. package/CHANGELOG.md +187 -0
  2. package/README.md +131 -8
  3. package/package.json +95 -13
  4. package/schemas/component-manifest.json +3294 -405
  5. package/schemas/manifest-omissions.json +173 -0
  6. package/src/blog/src/breakpoints.js +6 -6
  7. package/src/blog/src/content/blog-atoms-showcase.js +1 -1
  8. package/src/blog/src/content/blog-page-layout.js +2 -2
  9. package/src/blog/src/lib/article-toc.js +7 -7
  10. package/src/blog/src/shell/blog-shell.js +1 -1
  11. package/src/blog/src/tokens/blog.js +2 -2
  12. package/src/blog/src/tokens/core.js +34 -35
  13. package/src/blog/src/tokens/legacy.js +0 -9
  14. package/src/blog/tokens/blog.js +2 -2
  15. package/src/blog/tokens/core.js +34 -35
  16. package/src/blog/tokens/legacy.js +0 -9
  17. package/src/blog/tokens.css +36 -46
  18. package/src/components/accordion/accordion-bg.css.js +83 -59
  19. package/src/components/accordion/accordion-bg.js +29 -10
  20. package/src/components/accordion/accordion-bg.stories.js +67 -67
  21. package/src/components/accordion/accordion-keyboard.js +80 -0
  22. package/src/components/accordion/accordion.css.js +142 -80
  23. package/src/components/accordion/accordion.js +37 -10
  24. package/src/components/accordion/accordion.stories.js +7 -7
  25. package/src/components/agent-endpoints/agent-endpoints.css.js +36 -39
  26. package/src/components/agent-endpoints/agent-endpoints.js +30 -20
  27. package/src/components/agent-endpoints/agent-endpoints.stories.js +1 -1
  28. package/src/components/ai-guardian-demo.stories.js +91 -91
  29. package/src/components/alert/alert.css.js +198 -0
  30. package/src/components/alert/alert.js +103 -0
  31. package/src/components/alert/alert.stories.js +206 -0
  32. package/src/components/anchor/anchor-nav.css.js +213 -101
  33. package/src/components/anchor/anchor-nav.js +26 -40
  34. package/src/components/anchor/anchor.stories.js +22 -12
  35. package/src/components/annotation/annotation.css.js +58 -0
  36. package/src/components/annotation/annotation.js +67 -0
  37. package/src/components/annotation/annotation.stories.js +63 -0
  38. package/src/components/architecture.stories.js +285 -0
  39. package/src/components/atoms.stories.js +342 -0
  40. package/src/components/avatar/avatar.css.js +67 -0
  41. package/src/components/avatar/avatar.js +42 -0
  42. package/src/components/avatar/avatar.stories.js +95 -0
  43. package/src/components/awards/awards-flex.css.js +209 -23
  44. package/src/components/awards/awards-flex.js +66 -7
  45. package/src/components/awards/awards-flex.stories.js +1 -1
  46. package/src/components/awards/{awards-light.stories.js → awards-summary.stories.js} +54 -64
  47. package/src/components/awards/awards.css.js +116 -58
  48. package/src/components/awards/awards.js +102 -28
  49. package/src/components/awards/awards.stories.js +1 -1
  50. package/src/components/back/back.css.js +51 -22
  51. package/src/components/back/back.js +28 -7
  52. package/src/components/back/back.stories.js +9 -3
  53. package/src/components/badge/badge.css.js +158 -46
  54. package/src/components/badge/badge.js +62 -74
  55. package/src/components/badge/badge.stories.js +176 -248
  56. package/src/components/bento-grid/bento-grid.css.js +21 -16
  57. package/src/components/bento-grid/bento-grid.js +16 -4
  58. package/src/components/bento-grid/bento-grid.stories.js +19 -18
  59. package/src/components/bento-grid/bento-item.css.js +52 -28
  60. package/src/components/bento-grid/bento-item.js +2 -2
  61. package/src/components/blockquote/blockquote.css.js +47 -0
  62. package/src/components/blockquote/blockquote.js +29 -0
  63. package/src/components/blockquote/blockquote.stories.js +62 -0
  64. package/src/components/breadcrumb/breadcrumb.css.js +92 -22
  65. package/src/components/breadcrumb/breadcrumb.js +56 -11
  66. package/src/components/button/button.css.js +531 -161
  67. package/src/components/button/button.js +206 -214
  68. package/src/components/button/button.stories.js +322 -174
  69. package/src/components/caption/caption.css.js +21 -0
  70. package/src/components/caption/caption.js +31 -0
  71. package/src/components/caption/caption.stories.js +62 -0
  72. package/src/components/card-features/card-features.css.js +14 -22
  73. package/src/components/card-features/card-features.js +68 -9
  74. package/src/components/card-features/card-features.stories.js +11 -11
  75. package/src/components/cards/card.css.js +60 -81
  76. package/src/components/cards/card.js +56 -25
  77. package/src/components/cards/card.stories.js +39 -39
  78. package/src/components/carousel/carousel.css.js +93 -99
  79. package/src/components/carousel/carousel.js +32 -4
  80. package/src/components/checkbox/checkbox.css.js +111 -93
  81. package/src/components/checkbox/checkbox.js +64 -90
  82. package/src/components/checkbox/checkbox.stories.js +96 -29
  83. package/src/components/code/code.css.js +70 -0
  84. package/src/components/code/code.js +40 -0
  85. package/src/components/code/code.stories.js +73 -0
  86. package/src/components/compare/compare.css.js +179 -43
  87. package/src/components/compare/compare.js +52 -25
  88. package/src/components/compare/compare.stories.js +61 -4
  89. package/src/components/comparison-panel/comparison-panel.css.js +38 -51
  90. package/src/components/comparison-panel/comparison-panel.js +5 -4
  91. package/src/components/comparison-panel/comparison-panel.stories.js +1 -1
  92. package/src/components/compatibility-table/compatibility-table.css.js +180 -80
  93. package/src/components/compatibility-table/compatibility-table.js +37 -9
  94. package/src/components/control-alignment.stories.js +393 -0
  95. package/src/components/cta-banner/cta-banner.css.js +41 -13
  96. package/src/components/cta-banner/cta-banner.js +6 -5
  97. package/src/components/cta-banner/cta-banner.stories.js +43 -98
  98. package/src/components/display/display.css.js +25 -1
  99. package/src/components/display/display.js +46 -13
  100. package/src/components/display/display.stories.js +270 -40
  101. package/src/components/divider/divider-horizontal.js +21 -5
  102. package/src/components/divider/divider-vertical.js +15 -7
  103. package/src/components/divider/divider.css.js +2 -2
  104. package/src/components/divider/divider.stories.js +38 -34
  105. package/src/components/dropdown/dropdown.css.js +93 -126
  106. package/src/components/dropdown/dropdown.js +114 -34
  107. package/src/components/dropdown/dropdown.stories.js +56 -24
  108. package/src/components/features/features.css.js +24 -11
  109. package/src/components/features/features.js +7 -3
  110. package/src/components/features/features.stories.js +37 -37
  111. package/src/components/features-tab/features-tab.css.js +19 -8
  112. package/src/components/features-tab/features-tab.js +6 -5
  113. package/src/components/features-tab/features-tab.stories.js +23 -23
  114. package/src/components/fieldset/fieldset.css.js +30 -0
  115. package/src/components/fieldset/fieldset.js +28 -0
  116. package/src/components/fieldset/fieldset.stories.js +84 -0
  117. package/src/components/footer/footer-links-group.css.js +10 -14
  118. package/src/components/footer/footer-links-group.js +4 -3
  119. package/src/components/footer/footer-lp.css.js +59 -30
  120. package/src/components/footer/footer-lp.js +17 -12
  121. package/src/components/footer/footer-lp.stories.js +63 -75
  122. package/src/components/footer/footer-nav-menu.css.js +26 -4
  123. package/src/components/footer/footer-nav-menu.js +3 -2
  124. package/src/components/footer/footer.css.js +146 -264
  125. package/src/components/footer/footer.js +176 -106
  126. package/src/components/footer/footer.stories.js +9 -9
  127. package/src/components/footer/localeMap.js +1 -1
  128. package/src/components/grid/grid.css.js +1 -1
  129. package/src/components/grid/grid.js +49 -7
  130. package/src/components/grid/grid.stories.js +111 -0
  131. package/src/components/header/header-code.css.js +61 -51
  132. package/src/components/header/header-code.js +28 -14
  133. package/src/components/header/header-code.stories.js +5 -5
  134. package/src/components/header/header.css.js +655 -176
  135. package/src/components/header/header.js +269 -36
  136. package/src/components/header/header.stories.js +841 -18
  137. package/src/components/heading/heading.css.js +14 -17
  138. package/src/components/heading/heading.js +48 -33
  139. package/src/components/heading/heading.stories.js +27 -25
  140. package/src/components/help-text/help-text.css.js +21 -0
  141. package/src/components/help-text/help-text.js +25 -0
  142. package/src/components/help-text/help-text.stories.js +66 -0
  143. package/src/components/highlight/highlight.css.js +86 -64
  144. package/src/components/highlight/highlight.js +18 -27
  145. package/src/components/highlight/highlight.stories.js +14 -3
  146. package/src/components/icons/analysis-icon.js +6 -16
  147. package/src/components/icons/arrow-down-icon.js +6 -19
  148. package/src/components/icons/arrow-icon.js +44 -0
  149. package/src/components/icons/arrow-right-icon.js +30 -23
  150. package/src/components/icons/arrow-up-icon.js +19 -18
  151. package/src/components/icons/bd-ai-icon.js +1 -1
  152. package/src/components/icons/brand-marks.stories.js +279 -0
  153. package/src/components/icons/brand-signature-icon.js +42 -0
  154. package/src/components/icons/check-icon.js +27 -0
  155. package/src/components/icons/circle-info-icon.js +33 -0
  156. package/src/components/icons/close-icon.js +6 -18
  157. package/src/components/icons/family-icon.js +8 -20
  158. package/src/components/icons/globe-icon.js +6 -18
  159. package/src/components/icons/guide-icon.js +6 -18
  160. package/src/components/icons/icon-element.js +99 -0
  161. package/src/components/icons/icons.js +36 -0
  162. package/src/components/icons/icons.stories.js +184 -52
  163. package/src/components/icons/individual-icon.js +6 -18
  164. package/src/components/icons/laurel-icon.js +9 -19
  165. package/src/components/icons/machine-learning-icon.js +6 -17
  166. package/src/components/icons/marks.js +41 -0
  167. package/src/components/icons/minus-icon.js +7 -21
  168. package/src/components/icons/missing-icon.js +52 -0
  169. package/src/components/icons/plus-icon.js +7 -21
  170. package/src/components/icons/quote-icon.js +19 -19
  171. package/src/components/image/image.css.js +72 -25
  172. package/src/components/image/image.js +11 -6
  173. package/src/components/image/image.stories.js +2 -2
  174. package/src/components/input/custom-form.stories.js +214 -62
  175. package/src/components/input/input-clipboard.css.js +51 -144
  176. package/src/components/input/input-clipboard.js +31 -15
  177. package/src/components/input/input.css.js +8 -146
  178. package/src/components/input/input.js +93 -38
  179. package/src/components/install-options/install-options.css.js +84 -77
  180. package/src/components/install-options/install-options.js +33 -30
  181. package/src/components/install-options/install-options.stories.js +3 -3
  182. package/src/components/label/label.css.js +35 -0
  183. package/src/components/label/label.js +38 -0
  184. package/src/components/label/label.stories.js +82 -0
  185. package/src/components/light-carousel/light-carousel.css.js +111 -63
  186. package/src/components/light-carousel/light-carousel.js +85 -11
  187. package/src/components/light-carousel/light-carousel.stories.js +126 -82
  188. package/src/components/link/link.css.js +150 -99
  189. package/src/components/link/link.js +86 -11
  190. package/src/components/link/link.stories.js +124 -30
  191. package/src/components/list/list-item/list-item.css.js +74 -22
  192. package/src/components/list/list-item/list-item.js +32 -10
  193. package/src/components/list/list-item/list-item.stories.js +23 -23
  194. package/src/components/list/list.css.js +8 -2
  195. package/src/components/list/list.js +3 -2
  196. package/src/components/list/list.stories.js +222 -0
  197. package/src/components/modal/modal.css.js +114 -24
  198. package/src/components/modal/modal.js +83 -7
  199. package/src/components/modal/modal.stories.js +9 -9
  200. package/src/components/overview/overview.css.js +19 -14
  201. package/src/components/overview/overview.js +6 -5
  202. package/src/components/overview/overview.stories.js +10 -10
  203. package/src/components/page-switcher/page-switcher.css.js +97 -40
  204. package/src/components/page-switcher/page-switcher.js +28 -5
  205. package/src/components/page-switcher/page-switcher.stories.js +95 -0
  206. package/src/components/paragraph/paragraph.css.js +15 -10
  207. package/src/components/paragraph/paragraph.js +41 -44
  208. package/src/components/paragraph/paragraph.stories.js +56 -21
  209. package/src/components/picture/picture.css.js +103 -44
  210. package/src/components/picture/picture.js +100 -30
  211. package/src/components/picture/picture.stories.js +1 -1
  212. package/src/components/pricing-cards/new-pricing-card.js +3 -3
  213. package/src/components/pricing-cards/new-pricing.css.js +24 -15
  214. package/src/components/pricing-cards/pricing-card-actions.css.js +1 -1
  215. package/src/components/pricing-cards/pricing-card-actions.js +3 -4
  216. package/src/components/pricing-cards/pricing-card-container.css.js +3 -3
  217. package/src/components/pricing-cards/pricing-card-container.js +3 -2
  218. package/src/components/pricing-cards/pricing-card-header.css.js +10 -8
  219. package/src/components/pricing-cards/pricing-card-header.js +6 -7
  220. package/src/components/pricing-cards/pricing-card-pricing.css.js +16 -9
  221. package/src/components/pricing-cards/pricing-card-pricing.js +8 -6
  222. package/src/components/pricing-cards/pricing-card-show-more.css.js +11 -3
  223. package/src/components/pricing-cards/pricing-card-show-more.js +8 -3
  224. package/src/components/pricing-cards/pricing-card.css.js +244 -190
  225. package/src/components/pricing-cards/pricing-card.js +54 -70
  226. package/src/components/pricing-cards/pricing-feature-item.css.js +25 -8
  227. package/src/components/pricing-cards/pricing-feature-item.js +8 -3
  228. package/src/components/process-steps/process-steps.css.js +26 -14
  229. package/src/components/process-steps/process-steps.js +23 -6
  230. package/src/components/process-steps/process-steps.stories.js +1 -1
  231. package/src/components/progress/progress.css.js +80 -0
  232. package/src/components/progress/progress.js +42 -0
  233. package/src/components/progress/progress.stories.js +79 -0
  234. package/src/components/quick-answer/quick-answer.css.js +75 -13
  235. package/src/components/quick-answer/quick-answer.js +8 -3
  236. package/src/components/radio/radio.css.js +92 -128
  237. package/src/components/radio/radio.js +106 -127
  238. package/src/components/radio/radio.stories.js +54 -19
  239. package/src/components/review/review-grid.css.js +5 -6
  240. package/src/components/review/review-grid.js +14 -11
  241. package/src/components/review/review.css.js +11 -6
  242. package/src/components/review/review.js +3 -8
  243. package/src/components/shared/ensemble.css.js +93 -0
  244. package/src/components/shared/form-field.css.js +206 -0
  245. package/src/components/shared/prop-vocabulary.js +48 -0
  246. package/src/components/shared/token-scales.js +306 -0
  247. package/src/components/skeleton/skeleton.css.js +34 -0
  248. package/src/components/skeleton/skeleton.js +32 -0
  249. package/src/components/skeleton/skeleton.stories.js +70 -0
  250. package/src/components/spinner/spinner.css.js +62 -0
  251. package/src/components/spinner/spinner.js +38 -0
  252. package/src/components/spinner/spinner.stories.js +92 -0
  253. package/src/components/spotlight-section/spotlight-section.css.js +14 -13
  254. package/src/components/spotlight-section/spotlight-section.js +42 -39
  255. package/src/components/switch/switch.css.js +103 -0
  256. package/src/components/switch/switch.js +44 -0
  257. package/src/components/switch/switch.stories.js +86 -0
  258. package/src/components/tab-list/content-switcher.stories.js +189 -0
  259. package/src/components/tab-list/tab-list.css.js +250 -0
  260. package/src/components/tab-list/tab-list.js +184 -0
  261. package/src/components/tab-list/tab-list.stories.js +221 -0
  262. package/src/components/tabs/tabs.css.js +14 -123
  263. package/src/components/tabs/tabs.js +16 -19
  264. package/src/components/tabs/tabs.stories.js +95 -95
  265. package/src/components/tag/tag.css.js +54 -0
  266. package/src/components/tag/tag.js +32 -0
  267. package/src/components/tag/tag.stories.js +126 -0
  268. package/src/components/terminal/terminal-message.css.js +164 -60
  269. package/src/components/terminal/terminal-message.js +5 -5
  270. package/src/components/terminal/terminal-message.stories.js +70 -1
  271. package/src/components/termsOfUse/terms.css.js +25 -20
  272. package/src/components/termsOfUse/terms.js +13 -9
  273. package/src/components/termsOfUse/terms.stories.js +1 -1
  274. package/src/components/textarea/textarea.css.js +51 -0
  275. package/src/components/textarea/textarea.js +136 -0
  276. package/src/components/textarea/textarea.stories.js +191 -0
  277. package/src/components/tooltip/tooltip.css.js +75 -0
  278. package/src/components/tooltip/tooltip.js +156 -0
  279. package/src/components/tooltip/tooltip.stories.js +161 -0
  280. package/src/components/trust-panel/trust-panel.css.js +85 -33
  281. package/src/components/trust-panel/trust-panel.js +18 -10
  282. package/src/components/trust-panel/trust-panel.stories.js +1 -1
  283. package/src/components/ultimate-security-demo.stories.js +431 -0
  284. package/src/components/video/video.css.js +17 -0
  285. package/src/components/video/video.js +40 -0
  286. package/src/components/video/video.stories.js +106 -0
  287. package/src/components/video-player/video-player.css.js +67 -11
  288. package/src/components/video-player/video-player.js +35 -4
  289. package/src/components/video-player/video-player.stories.js +1 -1
  290. package/src/components/visual-alignment.stories.js +411 -0
  291. package/src/components/visually-hidden/visually-hidden.css.js +24 -0
  292. package/src/components/visually-hidden/visually-hidden.js +16 -0
  293. package/src/components/visually-hidden/visually-hidden.stories.js +56 -0
  294. package/src/stories/Button.stories.js +17 -17
  295. package/src/stories/Header.stories.js +10 -10
  296. package/src/stories/Page.js +1 -1
  297. package/src/stories/Page.stories.js +6 -6
  298. package/src/tokens/borders.stories.js +339 -0
  299. package/src/tokens/color-responsibilities.stories.js +227 -0
  300. package/src/tokens/dark-derivation.js +309 -0
  301. package/src/tokens/dark-findings.js +197 -0
  302. package/src/tokens/dark-mode.stories.js +207 -0
  303. package/src/tokens/dimension.stories.js +251 -0
  304. package/src/tokens/elevation.stories.js +143 -0
  305. package/src/tokens/focus.stories.js +276 -0
  306. package/src/tokens/fonts.css +20 -20
  307. package/src/tokens/fonts.stories.js +354 -374
  308. package/src/tokens/layout.css +25 -11
  309. package/src/tokens/motion.stories.js +231 -0
  310. package/src/tokens/shape.stories.js +272 -0
  311. package/src/tokens/spacing-rhythm.stories.js +359 -0
  312. package/src/tokens/spacing.stories.js +208 -703
  313. package/src/tokens/story-chrome.js +503 -0
  314. package/src/tokens/surfaces.stories.js +224 -0
  315. package/src/tokens/text-on-surface.stories.js +164 -0
  316. package/src/tokens/token-map.js +220 -0
  317. package/src/tokens/token-map.stories.js +257 -0
  318. package/src/tokens/token-purposes.js +266 -0
  319. package/src/tokens/tokens-grid.stories.js +388 -900
  320. package/src/tokens/tokens.css +1633 -1045
  321. package/src/tokens/tokens.js +1130 -492
  322. package/src/tokens/tokens.stories.js +137 -47
  323. package/src/tokens/type-ceiling-words.js +299 -0
  324. package/src/tokens/typography.stories.js +1003 -965
  325. package/src/tokens/vertical-rhythm.stories.js +224 -0
  326. package/src/components/awards/awards-icon.js +0 -56
  327. package/src/components/awards/awards-light.js +0 -240
  328. package/src/components/banner/banner.js +0 -60
  329. package/src/components/banner/banner.stories.js +0 -51
  330. package/src/components/button/icons.js +0 -24
  331. package/src/components/input/index.js +0 -0
  332. package/src/components/light-carousel/light-carousel-simple.css.js +0 -183
  333. package/src/components/light-carousel/light-carousel-simple.js +0 -74
  334. package/src/tokens/colors.js +0 -121
  335. package/src/tokens/typography.css.js +0 -207
  336. package/src/tokens/typography.js +0 -0
@@ -1,5 +1,15 @@
1
1
  import { html } from 'lit';
2
2
  import './header.js';
3
+ /* The reference story below composes the same atoms the component does, and
4
+ borrows the component's OWN stylesheet rather than restating it -- see the
5
+ note on IntendedDesign for what that buys and what it does not cover. */
6
+ import headerCSS from './header.css.js';
7
+ import { ensembleCSS } from '../shared/ensemble.css.js';
8
+ import '../badge/badge.js';
9
+ import '../button/button.js';
10
+ import '../heading/heading.js';
11
+ import '../icons/brand-signature-icon.js';
12
+ import '../paragraph/paragraph.js';
3
13
 
4
14
  export default {
5
15
  title : 'Components/Header',
@@ -20,7 +30,7 @@ export default {
20
30
  devices="5"
21
31
  subscription="1"
22
32
  >
23
- <img slot="header-picture" src="/assets/product.png" alt="Product" />
33
+ <img slot="header-picture" src="/images/bd-header-img.jpg" alt="Product" />
24
34
  <span slot="disclaimer-top">Protection for 5 devices.</span>
25
35
  </bd-header>
26
36
  \`\`\`
@@ -34,9 +44,376 @@ export default {
34
44
  | \`disclaimer-bottom-link\` | Link in the bottom disclaimer |
35
45
  | \`disclaimer-bottom-text-end\` | End of the bottom disclaimer line |
36
46
 
47
+ ### This is Header / v2's chosen configuration
48
+
49
+ Everything below was brought across from Figma \`Header / v2\` (node \`340:4\`),
50
+ which is the winning variant of the *Header height study* — variant **I**
51
+ (all type shrunk) at frame padding 16, with the brand signature and scrim. The
52
+ Figma component measures **1290 × 566**, under the 600px target the study was
53
+ run against; every value here was then measured in this Storybook at 1440.
54
+
55
+ **The type is all roles, and no new token was needed.** That is the headline
56
+ result: the as-built header needed a brand-new \`fontSize-64\` primitive and a
57
+ \`display-hero\` role for its 64px headline — **both since retired, because this
58
+ was their only reason to exist** — and the configuration that actually
59
+ won drops to 48 — where \`heading-h1\` already lives. Measured, in render order:
60
+
61
+ | | role | measured |
62
+ |---|---|---|
63
+ | product name | \`heading-h4\` role on a \`<p>\` | 24px / 600 / 31.2px, \`rgb(0 89 214)\` |
64
+ | headline | \`bd-h\` at \`visual-level="h1"\`, level from \`heading-level\` | 48px / 700 / 60px |
65
+ | description | \`bd-p variant="regular"\` | 16px / 400 / 24px |
66
+ | struck price | \`bd-p regular\` + \`font-weight="medium"\`, muted | 16px / 500 / 24px, \`rgb(86 94 102)\` |
67
+ | discount | \`bd-badge tone="success" variant="fill" size="lg"\` | 14px / 500, white on \`rgb(14 133 50)\` |
68
+ | final price | \`heading-h2\` | 36px / 700 / 45px |
69
+ | disclaimers | \`bd-p variant="xsmall"\`, muted | 12px / 400 / 17.4px |
70
+ | both CTAs | \`size="lg"\` | 16/32 padding, 18px label, 8px radius |
71
+
72
+ ### The heading structure: one h1, and the eyebrow is not a heading
73
+
74
+ This changed, and it changes the outline of every page rendering the header, so
75
+ it is worth reading rather than skipping.
76
+
77
+ **Before:** the product name was \`as="h2"\` and the headline \`as="h4"\`, in
78
+ that DOM order. So the eyebrow was announced as a landmark and the actual
79
+ promise was announced two levels below it — a heading list read *"Bitdefender
80
+ Total Security"* (h2) then *"Complete protection for all your devices"* (h4),
81
+ in the wrong order of importance.
82
+
83
+ **Now:** the headline is the page's \`<h1>\` and the product name is a \`<p>\`.
84
+
85
+ - The headline is the main heading of the page. This component is the product
86
+ hero and there is one per page, which is the condition an \`h1\` needs.
87
+ \`bd-h\`'s h1 role is 5xl / bold / tight — 48/700/1.25 — which is Header /
88
+ v2's value-prop exactly, so \`as="h1"\` needs no \`visual-level\` beside it.
89
+ The outline level and the type role agree here instead of having to be
90
+ separated.
91
+ - The product name is a **kicker**: a label naming the product, and in Figma a
92
+ plain text layer called \`product-name\` rather than a heading. A kicker in
93
+ the outline is worse than no kicker in the outline — it adds a rung nobody
94
+ navigates by and pushes the real heading down. It resolves through the
95
+ \`heading-h4\` **role** on a non-heading element, which is the right way
96
+ round: the role names the type job ("the most prominent label on the
97
+ section"), and what the element *is* is settled by the tag.
98
+
99
+ **The alternative, and why not.** The other accessible eyebrow pattern nests
100
+ both inside the heading —
101
+ \`<h1><span>Bitdefender…</span><span>Complete protection…</span></h1>\` —
102
+ giving the heading an accessible name that carries the product as well as the
103
+ promise. Rejected on two counts: every heading list, skip link and screen-reader
104
+ rotor entry would read the whole run, and it would mean hand-rendering the
105
+ \`h1\` here instead of going through \`bd-h\`, which takes one slot. The product
106
+ is already named by the document title, the price block and the CTA; the
107
+ heading does not have to carry it too.
108
+
109
+ **If your page needs a different level, say so — that is your call, not this
110
+ component's.** \`heading-level\` takes h1–h6 and defaults to \`h1\`:
111
+
112
+ \`\`\`html
113
+ <bd-header heading-level="h2" subtitle="Complete protection…"></bd-header>
114
+ \`\`\`
115
+
116
+ Two things about it worth knowing before you use it:
117
+
118
+ - **It moves the heading in the outline and changes nothing about how it
119
+ looks.** \`visual-level\` is pinned to \`h1\` in the component, so the hero
120
+ stays 48 / 700 / 60px at every level. The size is the design's; only the
121
+ level is the page's. Without the pin, \`bd-h\` follows \`as\` for both and
122
+ \`heading-level="h3"\` would have rendered the hero at the h3 role instead —
123
+ measured **32px at 1024 and 24px at 320**, against 48 and 30 — an SEO
124
+ decision repainting the design.
125
+ - **\`h1\` is the right answer far more often than not.** A hero that is not
126
+ the page's main heading is usually a sign the hero is in the wrong place, so
127
+ treat a non-\`h1\` here as something you decided rather than something you
128
+ reached for. The prop exists because the decision belongs to the page — the
129
+ dev team and the SEO team own the heading levels on this section — not
130
+ because the default is in doubt.
131
+
132
+ It was hardcoded until 2026-09-17, which is the part that was wrong: the rule
133
+ above says the composing page owns \`as=\`, and the component was keeping it.
134
+
135
+ **The spacing is variant I's 16/8/8.** \`:host\` pads the row 16 (was a bare
136
+ \`32px\`), the content column takes 24 back, the two columns are 64 apart (was
137
+ \`30px\`, on no step and 34 short of the design), and the gaps run
138
+ content 16 → body 16 → copy 8 → actions 8. Tightening copy and actions to 8 is
139
+ where most of the 94px the study saved came from.
140
+
141
+ **What this retired.** Not only additions — six things that were wrong went:
142
+
143
+ - a \`font-size: 44px\` on the final price, carrying a recorded \`no token:\`
144
+ exception because 44 is off the type scale. The design asks for 36, which
145
+ \`heading-h2\` is exactly, so the exception simply dissolved
146
+ - a hand-rolled discount badge — \`green-100\` behind \`text/success\` at 16px
147
+ semibold in a \`--radius-xl\` box, with two \`no role:\` markers saying it
148
+ should be a \`bd-badge\`. It is one now, and not the same one: that tint is
149
+ the atom's \`shade\`, and the design asks for \`fill\`
150
+ - three off-scale literals (\`30px\`, \`32px\` twice), all three previously
151
+ baselined in \`lint:tokens\`
152
+ - \`transform: scale(1.35) translateY(-7%)\` on the product image, a crop hack
153
+ that magnified the photo 135% so a wide image would fill a tall box. A real
154
+ \`object-fit: cover\` at \`height: 100%\` does that without throwing away a
155
+ third of the resolution — and it matters to the scrim, which is positioned
156
+ against the wrapper while a transformed image is somewhere else
157
+ - two dead declarations on each disclaimer. They set \`font-size\` and
158
+ \`font-weight\` on a \`bd-p\` host, which \`scripts/atom-inert-properties.json\`
159
+ records as INERT — the atom sets all five type axes on its inner element. So
160
+ the rendered size was 14 while the rule claimed otherwise; the size now comes
161
+ from the atom's own \`xsmall\` variant
162
+ - two \`.bd-header-image\` rules disagreeing about \`height\`
163
+
164
+ **One recorded divergence.** Figma binds \`fontSize\` to a variable on these
165
+ text nodes and leaves \`lineHeight\` and \`letterSpacing\` raw, so its \`-1px\`
166
+ product-name tracking, its \`-2.5%\` headline tracking and its \`150%\` disclaimer
167
+ leading were unbound numbers rather than tokens. CLAUDE.md's rule is to read
168
+ the intent and spell it with the role, so the code takes the roles' values —
169
+ and the Figma component was then set to match, so the two agree on one answer
170
+ rather than on two.
171
+
172
+ ### Responsive: three steps on the breakpoint scale
173
+
174
+ **There is no mobile frame in Figma.** The height study was run at 1290 only,
175
+ so every width below that was decided here, from the grid arithmetic and then
176
+ by measuring. The page column caps at \`--grid-container-max-*\` with
177
+ \`--grid-padding-*\` either side, so once the design's 64px gap comes out, the
178
+ two-column arithmetic is fixed:
179
+
180
+ | step | content width | per column |
181
+ |---|---|---|
182
+ | xxl | 1320 − 80 = 1240 | 588 |
183
+ | xl | 1140 − 80 = 1060 | 498 |
184
+ | lg | 960 − 80 = 880 | 408 |
185
+ | md | 720 − 64 = 656 | 296 |
186
+ | sm | 540 − 48 = 492 | 214 |
187
+
188
+ The CTA row is **446px**. It fits at xl and xxl, wraps at lg, and at md there
189
+ is less room than one button needs. So:
190
+
191
+ - **\`below('lg')\` — stack.** One column, 32px row gap.
192
+ - **\`below('md')\` — stack the CTA.** Separate from the step above on purpose:
193
+ at md the column is 656px, where a side-by-side CTA row still fits.
194
+ - **\`below('md')\` — re-proportion the scrim band** (same query, different
195
+ concern: it is about the media panel, not the copy).
196
+
197
+ Measured across ten widths afterwards — no horizontal overflow at any of them,
198
+ and the buttons hold 183/246 at every width:
199
+
200
+ | width | cols | column | headline | description | band | total |
201
+ |---|---|---|---|---|---|---|
202
+ | 1440 | 2 | 588 | 48 × 2 | 16 × 3 | 30% | **566** |
203
+ | 1280 | 2 | 498 | 48 × 2 | 16 × 3 | 30% | 566 |
204
+ | 1100 | 2 | 408 | 48 × 2 | 16 × 4 | 25% | 661 |
205
+ | 1024 | 2 | 408 | 48 × 2 | 16 × 4 | 25% | 661 |
206
+ | 900 | 1 | 656 | 48 × 2 | 16 × 2 | 37% | 1012 |
207
+ | 768 | 1 | 656 | 48 × 2 | 16 × 2 | 37% | 1012 |
208
+ | 600 | 1 | 492 | 48 × 2 | 16 × 3 | 27% | 997 |
209
+ | 375 | 1 | 311 | 48 × 3 | 16 × 5 | 42% | 1002 |
210
+ | 320 | 1 | 256 | 48 × 4 | 16 × 6 | 51% | 1066 |
211
+
212
+ #### Why lg keeps two columns even though it costs 661px
213
+
214
+ It is the one arguable step, and it is arguable rather than good. Two columns
215
+ at lg cost the description a fourth line, the CTA row a wrap, and the section
216
+ **661px** against the design's 566. No gap change rescues it: the row needs 445
217
+ and dropping 64 to the lg grid gutter's 40, or to 24, or to 16, gives 420, 428
218
+ and 432. The column is about 40px short however the gutter is spent.
219
+
220
+ It keeps them because stacking is worse **on the same screen**. Stacked at lg
221
+ the copy gets one 880px column — the headline collapses to one line, which the
222
+ study measured as needing 678 — and then an 880px-wide photograph beneath it at
223
+ its own 1.497 ratio: about 1100px of hero in a window 992–1199 wide. Bounding
224
+ the stacked panel to 16/9 would still land near 978. 661 wins.
225
+
226
+ #### The scrim band now scales, because it is built from lengths
227
+
228
+ The band was four percentage stops of a fixed 160px — read straight off Figma,
229
+ which made the invariant arithmetic: the 70% stop landed on the mark's top edge
230
+ only because 0.3 × 160 happens to equal 24 + 24. It is now **length stops
231
+ measured from the bottom**, so the plateau *is* the mark's box by construction
232
+ at any size, and the narrow variant is two smaller numbers rather than a second
233
+ set of percentages. The stops still resolve to Figma's exactly at desktop:
234
+ plateau 0–48, 0.22 at 88, zero at 160.
235
+
236
+ That mattered because the band was a constant and the panel is not. The panel
237
+ follows the copy beside it when the columns are side by side, and its own
238
+ 1.497 ratio once they stack — 311×208 at 375 and 256×171 at 320, which put a
239
+ 160px band at **77%** and **94%** of the picture. Below md the inset drops a
240
+ step and the fade drops to 48, giving 40 + 48 = 88: **42%** and **51%**.
241
+
242
+ One thing that is inherent rather than fixed — a constant band on a variable
243
+ panel is a variable proportion. *Default* has less copy than the design, so its
244
+ panel is 399 tall at 1440 and the band reads 40% there against the design's 30%.
245
+
246
+ #### What was removed, and one claim corrected
247
+
248
+ Two of the three old media blocks were largely inert. \`flex-direction: column\`
249
+ on \`.bd-container\` is inert — the container is \`display: grid\`, and the
250
+ identical mistake was already recorded further down the file. \`gap: 2em\` was an
251
+ em literal on no scale and the only live declaration in that block, which is
252
+ why the gap read 32px at exactly 1024 and 64px at 1025. \`width: 100%\` on two
253
+ grid items is redundant, and \`::slotted([slot="right"])\` is one of the four
254
+ slot names this stylesheet targets that \`header.js\` never renders.
255
+
256
+ So the header's real stacking point was \`below('md')\`, and measured at 768 that
257
+ was far too late: two 312px columns, the headline at three lines, the
258
+ description at five, the buttons crushed from 183/246 to **150/146** with both
259
+ labels broken over three lines inside a box padded for one, and the section
260
+ **737px** tall — against a design that exists to hold it under 600. Flex items
261
+ shrink by default, so "nowrap" never meant "stays on one line"; it meant
262
+ "squashes". \`.bd-cta\` now wraps, which happens at whatever width runs out
263
+ rather than at a breakpoint somebody picks.
264
+
265
+ **A correction.** The \`(max-width: 480px)\` block set \`max-width: 90%\` and
266
+ \`max-height: 200px\` on \`.bd-header-image\` — a class \`header.js\` does not
267
+ render. For the component it was **dead**: measured at 375 and 320, the slotted
268
+ image computes \`max-width: none\`, \`max-height: none\` and sits flush with the
269
+ wrapper on every edge. It was live only in *Intended design*, which draws a
270
+ real \`<img class="bd-header-image">\`, and that is where a 90%-wide centred
271
+ image left the scrim and the mark — both positioned against the **wrapper** —
272
+ hanging past the photo on each side.
273
+
274
+ #### Three things only the render showed
275
+
276
+ The ten-width table above was all measured, and it missed all three of these.
277
+ Numbers answer "does it fit"; they do not answer "does it look deliberate".
278
+
279
+ 1. **The price row was not centred** while everything around it was.
280
+ \`text-align\` centres a block's *inline content*, and
281
+ \`.bd-price-zone-top\` is \`display: flex\` — so the property reached its
282
+ children's text and moved the row itself not at all. The struck price and
283
+ the badge sat hard left while the final price, a block span, centred
284
+ underneath. It was patched with \`justify-content: center\`, and then the
285
+ patch became the argument for dropping the centring altogether — see below.
286
+ Both are gone.
287
+ 2. **The stacked CTAs were ragged** — 183 above 246, both centred. The first
288
+ attempt made it *worse*: \`align-items: stretch\` took the hosts full width,
289
+ but each anchor is \`inline-flex\` and hugs its label, so both buttons ended
290
+ up hard against the left edge of a centred layout. The atom's own answer is
291
+ its \`fullwidth\` attribute, which a media query cannot set, so the host's
292
+ display does it instead: a lone child of a single-column grid fills its
293
+ track. Measured, hosts 492 and anchors 183/246 → 492/492.
294
+ 3. **The mark dominated the panel at 320** — 182px on a 256px panel is **71%**,
295
+ against 30% in the design, so a corner signature became a caption laid
296
+ across the picture. The numbers said it fitted, and it did fit; fitting was
297
+ not the question. It steps to the \`sm\` icon step below md — 121px, 47% of
298
+ that panel — and \`--bd-header-mark-height\` steps with it so the scrim's
299
+ plateau still ends exactly on the mark's top edge.
300
+
301
+ Re-verified after, with the mark hidden and the composited backdrop sampled at
302
+ six widths: the plateau covers the mark at **every** width (48px desktop, 32px
303
+ narrow) and the worst contrast is **4.84:1** at 320, with 0% of the mark's area
304
+ under 4.5:1.
305
+
306
+ #### And one silent defect, which is why the lint gained a rule
307
+
308
+ An earlier edit in this pass dropped a single \`}\` from the \`below('md')\`
309
+ block. That is valid JavaScript: the module parsed, the import succeeded,
310
+ \`lint:tokens\` stayed clean, all fifteen \`check:ds\` gates passed and so did
311
+ all five browser gates. At runtime every rule after it folded into the media
312
+ query, so at desktop the product name silently collapsed from two lines to one
313
+ and the section lost 63px. **It was caught by looking at a screenshot.**
314
+
315
+ \`lint:tokens\` now carries \`unbalanced-css-braces\` beside the
316
+ \`declaration-inside-comment\` rule it is modelled on — counting braces per
317
+ \`css\`\`\` template after stripping comments, \`\${…}\` interpolations (every
318
+ one ends in a brace) and strings (\`content: "}"\` is legal CSS). Seven
319
+ selftest cases, and it was proved on the real defect: removing that brace again
320
+ makes it the only new finding, restoring it returns the lint to clean.
321
+
322
+ #### Two things deliberately left alone
323
+
324
+ - **The headline does not step down.** \`bd-h\`'s type lives on its inner element
325
+ inside its shadow root, so a \`font-size\` from here is inert, and no atom in
326
+ this package changes its type with the viewport. Measured, 48px runs two
327
+ lines to 500px, three at 375 and four at 320 — and at 320 that is one word
328
+ per line, which is the most visibly wrong thing left on this page.
329
+
330
+ Note the icon and the button above were both reachable because each atom's
331
+ rule is on \`:host\`, where an outer-tree rule wins. \`bd-h\` puts its type on
332
+ the **inner** element, so there is no CSS route at all — only two real ones:
333
+ give \`bd-h\` responsive roles, which is a foundation decision touching every
334
+ consumer and would be this package's first responsive type; or switch
335
+ \`visual-level\` from JS the way \`bd-footer\`'s \`_isMobile\` already reads the
336
+ viewport through \`matchesBelow()\`. Neither is free: the step to move to is
337
+ \`heading-h2\`, 36/bold, which is **also the final price's role**, so at 36 the
338
+ headline and the price become the same size and the hierarchy the semibold
339
+ kicker was added to sharpen flattens. 375 — the common phone width — is three
340
+ lines and reads fine; 320 is the edge. A design decision, not a CSS one.
341
+ - **Nothing else.** The stacked-CTA raggedness that was listed here is fixed
342
+ above.
343
+
344
+ #### The content is left-aligned at every width
345
+
346
+ It used to centre below \`md\`. It no longer does, and the reasoning is worth
347
+ keeping because the centring looked harmless:
348
+
349
+ - **The design never asked for it.** \`Header / v2\` is left-aligned and there is
350
+ no mobile frame in the file that centres. Changing the axis at a breakpoint was
351
+ a decision nobody recorded making.
352
+ - **The content is the wrong shape for it.** Six stacked blocks of very different
353
+ widths — a two-line kicker, a two-line headline, a five-line description, a
354
+ price pair, two CTAs, a two-line disclaimer — share no edge when centred, so the
355
+ eye gets no vertical line to return to down a five-line paragraph at 288px.
356
+ Centring suits a poster: a headline and one button. This is a specification.
357
+ - **It needed a patch to stop looking broken, and that is the argument.** The price
358
+ row had to be given \`justify-content: center\` because \`text-align\` cannot
359
+ move a flex row. A treatment that needs a correction to look deliberate usually
360
+ is not the right treatment — and dropping the centring removed the patch with it.
361
+
362
+ The full-width stacked CTAs stay: equal tap targets that do not vary with the copy
363
+ hold either way.
364
+
365
+ ### The brand signature over the image
366
+
367
+ When \`header-picture\` is filled, the header lays two things over it — a
368
+ \`--color-blue-950\` scrim band across the bottom, and
369
+ \`<bd-brand-signature-icon>\` 24px in from the bottom-right corner (measured:
370
+ 24 and 24). Both come from \`Header / v2\`, whose media-panel stacks
371
+ \`media → scrim → brand-signature\` in that order.
372
+
373
+ Neither is a slot and neither is a prop. The signature is the brand's, not the
374
+ page's, so there is nothing for a consumer to decide; the scrim exists so the
375
+ white mark is legible over an image the design system has never seen, so
376
+ turning it off would be turning off the only thing making that true. What the
377
+ consumer controls is the image.
378
+
379
+ They appear **only** when something is actually slotted. A blue band and a
380
+ white mark over an empty column would be a wash of colour and an invisible
381
+ mark, so \`_hasPicture\` is tracked from the slot's own \`slotchange\` and the
382
+ pair is not rendered at all otherwise — see *No Product Image* below, which is
383
+ a documented state rather than an accident.
384
+
385
+ **What replaced what.** This used to be the words "Trusted. Always." at 32px
386
+ bold, 16px from the corner. That was type doing a mark's job, it was English
387
+ hardcoded next to a stylesheet, and it clipped: measured at 320px the 133px run
388
+ sat inside a 96px column with \`overflow: hidden\` and read "rusted. lways.".
389
+ The mark is the same words as artwork, so nothing needs translating and the
390
+ width is fixed at 182.
391
+
392
+ ### The prices are empty in Storybook
393
+
394
+ The struck price, the discount and the final price are store-driven —
395
+ \`data-store-render\` with \`data-store-price\` / \`data-store-discount\` — and
396
+ there is no store in Storybook, so all three render empty. The discount badge
397
+ therefore shows as a small green pill with no text. That is the harness, not
398
+ the design: on a real page \`data-store-hide\` removes the whole row when the
399
+ option is not discounted.
400
+
37
401
  ### Notes
38
402
  - Pricing zones use \`data-store-render\` and \`data-store-price\` attributes for store integration
39
- - \`shadow\` prop reflects as an attribute and defaults to \`true\`
403
+ - \`shadow\` reflects as an attribute and defaults to \`true\`, and it is **not a
404
+ styling flag** despite the name. It is how
405
+ \`@repobit/dex-store-elements\` discovers that this component's shadow root
406
+ should be walked for \`[data-store-render]\` and \`[data-store-hide]\` nodes
407
+ (its \`SHADOW_HOST_SELECTOR\` is \`'[shadow], [data-bd-watch-shadow]'\`). Every
408
+ price zone in this component is inside that shadow root, so dropping the
409
+ attribute silently stops the prices rendering on a real page
410
+ - **\`fullPrice\`, \`discount\` and \`finalPrice\` have been removed.** Nothing read
411
+ them -- not the template, not the stylesheet, not the store, which computes
412
+ prices from \`product-id\` + \`devices\` + \`subscription\` and never looks at a
413
+ host property. They were listed in the component manifest as if they worked,
414
+ which is the worse half of the defect: that file is the contract a page
415
+ generator is handed. Wiring them instead would have given one value two
416
+ sources that can disagree, with no answer to which wins
40
417
  `
41
418
  }
42
419
  }
@@ -44,19 +421,58 @@ export default {
44
421
  argTypes: {
45
422
  title: {
46
423
  control : 'text',
47
- description: 'Product name rendered as `<bd-h as="h2">`',
48
- table : { type: { summary: 'string' }, defaultValue: { summary: '' }, category: 'Content' }
424
+ description: 'The product name — the kicker above the headline. Rendered as a '
425
+ + '`<p class="bd-product-name">` resolving through the `heading-h4` role, '
426
+ + 'NOT as a heading: a kicker in the document outline adds a rung nobody '
427
+ + 'navigates by. (This used to say `<bd-h as="h2">`, which is what it was '
428
+ + 'before the heading structure was fixed.)',
429
+ table: { type: { summary: 'string' }, defaultValue: { summary: '' }, category: 'Content' }
49
430
  },
50
431
  subtitle: {
51
432
  control : 'text',
52
- description: 'Value proposition rendered as `<bd-h as="h4">`',
53
- table : { type: { summary: 'string' }, defaultValue: { summary: '' }, category: 'Content' }
433
+ description: 'The value proposition — the actual heading of the section, rendered '
434
+ + 'as `<bd-h as="h1" visual-level="h1">`. Its level is the `headingLevel` prop; '
435
+ + 'its size is fixed. (This used to say `<bd-h as="h4">`, from before the fix.)',
436
+ table: { type: { summary: 'string' }, defaultValue: { summary: '' }, category: 'Content' }
54
437
  },
55
438
  description: {
56
439
  control : 'text',
57
- description: 'Description paragraph rendered as `<bd-p kind="regular">`',
440
+ description: 'Description paragraph rendered as `<bd-p variant="regular">`',
58
441
  table : { type: { summary: 'string' }, defaultValue: { summary: '' }, category: 'Content' }
59
442
  },
443
+ ctaLabel: {
444
+ control : 'text',
445
+ description: 'Visible label on the primary CTA. Defaults to `Buy now`, the string this '
446
+ + 'component has always rendered; the design reads **Get Protection**. Its '
447
+ + 'destination is not a prop \u2014 the primary carries `buyLink`, so the store '
448
+ + 'injects the buy URL at runtime.',
449
+ table: { type: { summary: 'string' }, defaultValue: { summary: 'Buy now' }, category: 'CTA' }
450
+ },
451
+ ctaSecondaryLabel: {
452
+ control : 'text',
453
+ description: 'Visible label on the secondary CTA. Defaults to `View more plans`; the '
454
+ + 'design reads **Start your 30-day trial**.',
455
+ table: { type: { summary: 'string' }, defaultValue: { summary: 'View more plans' }, category: 'CTA' }
456
+ },
457
+ ctaSecondaryHref: {
458
+ control : 'text',
459
+ description: '**Set this.** Destination for the secondary CTA, with no default \u2014 and '
460
+ + 'until a page gives it one the control is dead: `bd-button-link` drops the `href` '
461
+ + 'attribute when it is empty, so the anchor is not focusable, is not a link to a '
462
+ + 'screen reader, and does nothing on click. It has rendered that way since the '
463
+ + 'component was written.',
464
+ table: { type: { summary: 'string' }, defaultValue: { summary: "''" }, category: 'CTA' }
465
+ },
466
+ headingLevel: {
467
+ control : { type: 'select' },
468
+ options : ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'],
469
+ description: 'Where the headline sits in the **document outline**. Defaults to '
470
+ + '`h1`, which is what a product hero is on the page it leads. Changing it '
471
+ + 'does not change how the headline looks — `visual-level` is pinned to `h1`, '
472
+ + 'so the hero stays 48/700/1.25 at every level. Off-list values fall back to '
473
+ + '`h2` in `bd-h`: still a heading, never the page\'s.',
474
+ table: { type: { summary: "'h1'|'h2'|'h3'|'h4'|'h5'|'h6'" }, defaultValue: { summary: 'h1' }, category: 'Content' }
475
+ },
60
476
  productId: {
61
477
  control : 'text',
62
478
  description: 'Product ID passed to `<bd-product>` for store price lookup',
@@ -74,8 +490,11 @@ export default {
74
490
  },
75
491
  shadow: {
76
492
  control : 'boolean',
77
- description: 'Reflected attribute for shadow styling',
78
- table : { type: { summary: 'boolean' }, defaultValue: { summary: 'true' }, category: 'Appearance' }
493
+ description: 'NOT styling. It is how @repobit/dex-store-elements finds this '
494
+ + "component's shadow root -- its SHADOW_HOST_SELECTOR is "
495
+ + '\'[shadow], [data-bd-watch-shadow]\' -- so every [data-store-render] price '
496
+ + 'zone inside the shadow tree depends on it. Load-bearing; leave it set.',
497
+ table: { type: { summary: 'boolean' }, defaultValue: { summary: 'true' }, category: 'Appearance' }
79
498
  },
80
499
  dataLayerEvent: {
81
500
  control : 'text',
@@ -90,13 +509,17 @@ const render = (args) => html`
90
509
  title="${args.title}"
91
510
  subtitle="${args.subtitle}"
92
511
  description="${args.description}"
512
+ heading-level="${args.headingLevel}"
513
+ cta-label="${args.ctaLabel}"
514
+ cta-secondary-label="${args.ctaSecondaryLabel}"
515
+ cta-secondary-href="${args.ctaSecondaryHref}"
93
516
  product-id="${args.productId}"
94
517
  devices="${args.devices}"
95
518
  subscription="${args.subscription}"
96
519
  ?shadow="${args.shadow}"
97
520
  data-layer-event="${args.dataLayerEvent}"
98
521
  >
99
- <img slot="header-picture" src="/assets/product-header.png" alt="Product screenshot" />
522
+ <img slot="header-picture" src="/images/bd-header-img.jpg" alt="Product screenshot" />
100
523
  <span slot="disclaimer-top">
101
524
  Protection for 1 account & ${args.devices} PCs, Macs, tablets, or smartphones.
102
525
  Windows® | macOS® | Android™ | iOS®
@@ -105,11 +528,15 @@ const render = (args) => html`
105
528
  `;
106
529
 
107
530
  const defaultArgs = {
108
- title : 'Bitdefender Total Security',
109
- subtitle : 'Complete real-time data protection',
110
- description : 'Award-winning cybersecurity for all your devices. Protection against viruses, malware, ransomware, and more.',
111
- productId : 'ts', devices : '5', subscription : '1',
112
- shadow : true, dataLayerEvent: 'info'
531
+ title : 'Bitdefender Total Security',
532
+ subtitle : 'Complete real-time data protection',
533
+ description : 'Award-winning cybersecurity for all your devices. Protection against viruses, malware, ransomware, and more.',
534
+ headingLevel : 'h1',
535
+ ctaLabel : 'Buy now',
536
+ ctaSecondaryLabel: 'View more plans',
537
+ ctaSecondaryHref : '',
538
+ productId : 'ts', devices : '5', subscription : '1',
539
+ shadow : true, dataLayerEvent : 'info'
113
540
  };
114
541
 
115
542
  // ─── Stories ───────────────────────────────────────────────────────────────
@@ -143,7 +570,7 @@ export const WithDisclaimers = {
143
570
  description="Unlimited devices, unlimited VPN, identity theft protection."
144
571
  product-id="ps" devices="unlimited" subscription="1"
145
572
  >
146
- <img slot="header-picture" src="/assets/premium.png" alt="Premium Security" />
573
+ <img slot="header-picture" src="/images/bd-header-us.jpg" alt="Premium Security" />
147
574
  <span slot="disclaimer-top">Protects unlimited devices.</span>
148
575
  <span slot="disclaimer-bottom-text-start">By purchasing you agree to our </span>
149
576
  <a slot="disclaimer-bottom-link" href="/terms">Terms of Service</a>
@@ -179,5 +606,401 @@ export const Playground = {
179
606
  name : '🛝 Playground',
180
607
  args : { ...defaultArgs },
181
608
  render,
182
- parameters: { docs: { description: { story: 'Interactive playground. Adjust title, subtitle, description, product-id, devices, and subscription via Controls.' } } }
183
- };
609
+ parameters: { docs: { description: { story: 'Interactive playground. Adjust title, subtitle, description, heading-level, product-id, devices, and subscription via Controls.' } } }
610
+ };
611
+
612
+ export const HeadingLevel = {
613
+ name : 'Heading Level (the outline is the page\'s)',
614
+ parameters: {
615
+ docs: {
616
+ description: {
617
+ story: `Same header twice. The top one takes the default — the headline
618
+ is the page's \`<h1>\`. The bottom one is \`heading-level="h3"\`.
619
+
620
+ **Look at the type: nothing moved.** Both headlines render 48px / 700 /
621
+ 60px, because \`visual-level\` is pinned to \`h1\` inside the component. The
622
+ level is the document outline and the page owns it; the size is the design and
623
+ the component owns it. \`bd-h\` keeps those on separate props for exactly this
624
+ case, and pinning one of them is what stops an outline decision from
625
+ repainting the design — unpinned, this second header would have rendered its
626
+ hero at the h3 role: measured 32px here and 24px at 320, against 48 and 30.
627
+
628
+ Inspect the shadow roots to see the difference, because that is the only place
629
+ it exists: \`h1.bd-heading.h1\` above, \`h3.bd-heading.h1\` below. The class is
630
+ the type role and stays \`h1\`; the tag is the level and follows the prop.
631
+
632
+ Measured, both headers, in this story: 48px / 700 / 60px and a section
633
+ **484px** tall at 1024, 30px and **728px** tall at 320 — identical to the
634
+ pixel, differing only in tag name. A level off the h1–h6 list behaves the same
635
+ way on the size and falls back on the level: \`heading-level="zzz"\` renders
636
+ \`h2.bd-heading.h1\` at 48px, which is \`bd-h\`'s own fallback — still a
637
+ heading, never the page's.
638
+
639
+ **Reach for this deliberately.** A hero that is not the page's main heading is
640
+ usually a sign the hero is in the wrong place. The prop exists because the
641
+ decision belongs to the dev and SEO teams composing the page, not because
642
+ \`h1\` is in doubt.`
643
+ }
644
+ }
645
+ },
646
+ render: () => html`
647
+ <div style="display:flex; flex-direction:column; gap:var(--spacing-64);">
648
+ <div>
649
+ <p style="margin:0 0 var(--spacing-8); font: 500 12px/1.4 var(--typography-fontFamily-sans); color: var(--color-text-muted);">
650
+ default — renders &lt;h1 class="bd-heading h1"&gt;
651
+ </p>
652
+ <bd-header
653
+ title="Bitdefender Total Security"
654
+ subtitle="Complete real-time data protection"
655
+ description="Award-winning cybersecurity for all your devices."
656
+ product-id="ts" devices="5" subscription="1">
657
+ <img slot="header-picture" src="/images/bd-header-img.jpg" alt="Product screenshot" />
658
+ </bd-header>
659
+ </div>
660
+ <div>
661
+ <p style="margin:0 0 var(--spacing-8); font: 500 12px/1.4 var(--typography-fontFamily-sans); color: var(--color-text-muted);">
662
+ heading-level="h3" — renders &lt;h3 class="bd-heading h1"&gt;, same 48px
663
+ </p>
664
+ <bd-header
665
+ heading-level="h3"
666
+ title="Bitdefender Total Security"
667
+ subtitle="Complete real-time data protection"
668
+ description="Award-winning cybersecurity for all your devices."
669
+ product-id="ts" devices="5" subscription="1">
670
+ <img slot="header-picture" src="/images/bd-header-img.jpg" alt="Product screenshot" />
671
+ </bd-header>
672
+ </div>
673
+ </div>
674
+ `
675
+ };
676
+
677
+ export const CtaCopy = {
678
+ name : 'Design copy & a live secondary',
679
+ parameters: {
680
+ docs: {
681
+ description: {
682
+ story: `Two headers, same component. **Top** is the default: \`Buy now\` /
683
+ \`View more plans\`, and the secondary has no \`href\`. **Bottom** sets the strings
684
+ Header / v2 actually reads and gives the secondary somewhere to go.
685
+
686
+ Inspect the two secondary buttons and the difference is not the words — the top one
687
+ renders \`<a>\` with **no href attribute at all**, because \`bd-button-link\` drops it
688
+ when the prop is empty. That anchor is not focusable, does not appear in a screen
689
+ reader's list of links, and does nothing when clicked. It has been that way since the
690
+ component was written, and it is what \`cta-secondary-href\` is for.
691
+
692
+ The primary needs no href either way: it carries \`buyLink\`, so
693
+ \`@repobit/dex-store\` injects the buy URL at runtime — which is also why there is no
694
+ \`cta-href\` prop to match the label one.
695
+
696
+ **Careful reading this page, though: in Storybook BOTH primaries are inert too**, for
697
+ the same reason the price zones are empty here — there is no store to inject anything.
698
+ Measured on this story: all three hrefless anchors refuse focus and match neither
699
+ \`a[href]\` nor \`:enabled\`, while the one with \`cta-secondary-href\` takes focus
700
+ normally. On a real page the primary gets its href from the store and the secondary
701
+ still does not, unless you set it.
702
+
703
+ Accessible names come from the visible text now rather than from a \`label\` attribute —
704
+ measured here as "Buy now", "View more plans", "Get Protection" and "Start your 30-day
705
+ trial". Passing both, as this component used to, gave the anchor an \`aria-label\`
706
+ duplicating its own content.`
707
+ }
708
+ }
709
+ },
710
+ render: () => html`
711
+ <div style="display:flex; flex-direction:column; gap:var(--spacing-64)">
712
+ <div>
713
+ <p style="margin:0 0 var(--spacing-8); font:500 12px/1.4 var(--typography-fontFamily-sans);
714
+ color:var(--color-text-muted)">
715
+ defaults — and the secondary CTA has no destination
716
+ </p>
717
+ <bd-header
718
+ title="Bitdefender Total Security"
719
+ subtitle="Complete real-time data protection"
720
+ description="Award-winning cybersecurity for all your devices."
721
+ product-id="ts" devices="5" subscription="1">
722
+ <img slot="header-picture" src="/images/bd-header-img.jpg" alt="Product screenshot" />
723
+ </bd-header>
724
+ </div>
725
+ <div>
726
+ <p style="margin:0 0 var(--spacing-8); font:500 12px/1.4 var(--typography-fontFamily-sans);
727
+ color:var(--color-text-muted)">
728
+ the design's own copy, and a secondary that navigates
729
+ </p>
730
+ <bd-header
731
+ title="Bitdefender Ultimate Security"
732
+ subtitle="Complete real-time data protection"
733
+ description="Award-winning cybersecurity for all your devices."
734
+ cta-label="Get Protection"
735
+ cta-secondary-label="Start your 30-day trial"
736
+ cta-secondary-href="#trial"
737
+ product-id="ts" devices="5" subscription="1">
738
+ <img slot="header-picture" src="/images/bd-header-img.jpg" alt="Product screenshot" />
739
+ </bd-header>
740
+ </div>
741
+ </div>
742
+ `
743
+ };
744
+
745
+ export const BrandSignature = {
746
+ name : 'Brand Signature & Scrim',
747
+ parameters: {
748
+ docs: {
749
+ description: {
750
+ story: `The two hardest grounds the mark has to survive, side by side.
751
+
752
+ **Left** is the real product photo. **Right** is a deliberately pale panel
753
+ standing in for a light image — no asset in the package is bright enough to be
754
+ the worst case, and the worst case is the one worth showing, because it is the
755
+ one that decided the colour. A bright brand blue cannot serve here at all:
756
+ white on \`--color-blue-500\` is **4.53:1 solid**, so no partial coverage of it
757
+ can ever reach the minimum. \`--color-blue-950\` at 60% holds white above 4.5:1
758
+ over any image including a white one.
759
+
760
+ Measured against the photo on the left: worst neighbourhood **8.64:1**, nothing
761
+ under 4.5. With the scrim removed the same crop measured **1.00:1**, with 33.5%
762
+ of the mark's area under the 3:1 WCAG 1.4.11 asks of a non-text graphic — so
763
+ before this the mark was legible by luck of the crop.
764
+
765
+ The band's geometry is derived rather than tuned: it is 48 + 112 tall
766
+ (measured: 160px), where 48 is the mark's own box — a \`--spacing-24\` inset
767
+ plus the 24px the artwork is drawn at — so the 60% plateau stop at 70% lands
768
+ exactly on the mark's top edge. The arithmetic and the one case it does not
769
+ hold — a root font size *below* 16px — are written out in \`header.css.js\`.
770
+
771
+ Both headers here are the full chosen configuration, so this story doubles as
772
+ the reference for the type scale and the 16/8/8 rhythm described above.`
773
+ }
774
+ }
775
+ },
776
+ render: () => html`
777
+ <div style="display:flex; flex-direction:column; gap:var(--spacing-64);">
778
+ <bd-header
779
+ title="Bitdefender Total Security"
780
+ subtitle="Complete protection for all your devices"
781
+ description="Award-winning security for Windows, macOS, Android and iOS."
782
+ product-id="ts" devices="5" subscription="1"
783
+ >
784
+ <img slot="header-picture" src="/images/bd-header-img.jpg" alt="Total Security" />
785
+ </bd-header>
786
+
787
+ <bd-header
788
+ title="Bitdefender Total Security"
789
+ subtitle="The same mark on a pale image"
790
+ description="The stand-in panel on the right is as light as an image can get."
791
+ product-id="ts" devices="5" subscription="1"
792
+ >
793
+ <div
794
+ slot="header-picture"
795
+ role="presentation"
796
+ style="
797
+ width: 100%;
798
+ aspect-ratio: 613 / 456;
799
+ background: linear-gradient(
800
+ 140deg,
801
+ var(--color-neutral-0),
802
+ var(--color-blue-100) 55%,
803
+ var(--color-neutral-0)
804
+ );
805
+ "
806
+ ></div>
807
+ </bd-header>
808
+ </div>
809
+ `
810
+ };
811
+
812
+ /* ---------------------------------------------------------------------------
813
+ THE TARGET REFERENCE.
814
+
815
+ This is the one story in the file that is NOT bd-header. It is Figma
816
+ `Header / v2` (node 340:4) drawn with this package's atoms and filled with
817
+ the file's own dummy content, so a developer can see what the component is
818
+ meant to become rather than read a description of it.
819
+
820
+ IT BORROWS THE COMPONENT'S STYLESHEET. headerCSS is injected verbatim, so
821
+ every gap, every type role and the scrim geometry come from the same source
822
+ the component uses and cannot drift from it. Exactly three declarations do
823
+ not survive the move out of shadow DOM, and they are restated below with
824
+ that reason attached: :host and two ::slotted() rules. That is also the
825
+ point the story is making -- the delta between this and bd-header is MARKUP,
826
+ not styling.
827
+
828
+ THE CONTENT IS COPIED, NOT INVENTED. Every string is Figma's, read off the
829
+ node rather than retyped: "Bitdefender" is the fixed brand line and
830
+ "Ultimate Security" is the productName property's default, which is why the
831
+ name is two lines and where the split falls. */
832
+ const FIGMA = {
833
+ brand : 'Bitdefender',
834
+ product : 'Ultimate Security',
835
+ valueProp : 'Your Ultimate Digital Protection',
836
+ description: 'Award-winning protection against digital threats, including '
837
+ + 'advanced scams, with unlimited VPN for private browsing, password '
838
+ + 'manager and digital identity protection.',
839
+ fullPrice : '$159.99',
840
+ discount : '44% OFF',
841
+ finalPrice : '$89.99',
842
+ ctaPrimary : 'Get Protection',
843
+ ctaSecondary : 'Start your 30-day trial',
844
+ disclaimerTop : 'Works on: Windows® | macOS® | Android™ | iOS®.',
845
+ disclaimerNext: 'First year price. Sales tax included. See Terms of Use below.'
846
+ };
847
+
848
+ export const IntendedDesign = {
849
+ name : 'Intended design (Figma reference)',
850
+ parameters: {
851
+ controls: { disable: true },
852
+ docs : {
853
+ description: {
854
+ story: `**This is the target, not the component.** Every other story on
855
+ this page renders \`<bd-header>\`; this one is Figma \`Header / v2\`
856
+ (node \`340:4\`, 1290 × 566) rebuilt from this package's atoms and filled with
857
+ the Figma file's own dummy content. Build against it.
858
+
859
+ It is **not** a second implementation to copy markup from. It injects
860
+ \`headerCSS\` — the component's real stylesheet — so the spacing, the type
861
+ roles and the scrim all come from the same source \`bd-header\` uses and cannot
862
+ drift from it. Only three declarations do not survive the move out of shadow
863
+ DOM, and the story restates them: \`:host\` (the section's ground and its 16px
864
+ padding) and two \`::slotted()\` rules (the picture's cover fill, the disclaimer
865
+ link's ink). **So the delta below is markup, not styling** — the type scale and
866
+ the 16/8/8 rhythm already match, and were measured doing so.
867
+
868
+ ### What \`bd-header\` still cannot render
869
+
870
+ | intended | \`bd-header\` today | what it needs |
871
+ |---|---|---|
872
+ | The product name on **two lines** — "Bitdefender" fixed, "Ultimate Security" variable | one \`title\` string, one line | **decided 2026-09-17: nothing, here.** The page builds it. \`title\` stays one string and one line; a page that needs the two-line kicker composes it from the atoms against this story, which is what this story is for. See below |
873
+ | CTA labels "Get Protection" / "Start your 30-day trial" | \`cta-label\` and \`cta-secondary-label\`, defaulting to "Buy now" / "View more plans" | **the page sets them, as of 2026-09-17.** Which words sell a subscription is still funnel copy and still not ours — but it is now a value in markup rather than a literal in this file |
874
+ | \`$159.99\` / \`44% OFF\` / \`$89.99\` visible | store-driven, so empty in Storybook | **nothing.** This is correct behaviour — the reference hardcodes them only so the price block's layout is visible here |
875
+ | the disclaimer as one two-line paragraph | two \`bd-p\` blocks behind three slots | **nothing.** Measured identical: 8px from the CTA row, 0 between the lines |
876
+
877
+ The Figma component exposes seven properties (\`productName\`, \`subtitle\`,
878
+ \`description\`, \`fullPrice\`, \`finalPrice\`, \`disclaimer\`, and \`media\` as an
879
+ instance swap), and the "Bitdefender" line is deliberately not one of them — it
880
+ is the same word on every product, so the design holds it fixed and lets only
881
+ the rest of the name vary.
882
+
883
+ ### Where each of the two gaps landed
884
+
885
+ **The two-line product name is the page's, not the component's.** Decided
886
+ 2026-09-17. \`bd-header\` keeps one \`title\` string, because the split is
887
+ structural rather than textual — Figma has a fixed brand layer and a variable
888
+ \`productName\`, and no single escaped string reproduces that — and the honest
889
+ place to reproduce structure is markup the page owns. So this story is the
890
+ deliverable, not an interim sketch: it shows the two text nodes in a zero-gap
891
+ vertical stack, with the real \`headerCSS\` behind them, which is what a page
892
+ implementing the two-line kicker builds against. If it later turns out every
893
+ consumer wants it, that is the moment to give \`bd-header\` the second prop —
894
+ not before, because an API change with live consumers should be paid for by
895
+ more than one page.
896
+
897
+ **The CTA labels are the page's now**, which is as far as this package can
898
+ take them. \`cta-label\` and \`cta-secondary-label\` default to the strings
899
+ this component always rendered, so nothing changes for a consumer who says
900
+ nothing, and the copy owner's answer no longer needs a code change to land.
901
+ Props rather than slots: a label is a plain string with no markup, which is
902
+ what \`title\` and \`subtitle\` already are, and props appear in the manifest a
903
+ page generator reads while slots do not.
904
+
905
+ **And doing that turned up a dead control.** The secondary CTA has never had a
906
+ destination — \`bd-button-link\` drops the \`href\` attribute when it is empty,
907
+ so it renders an \`<a>\` with no href: not focusable, not a link to a screen
908
+ reader, nothing on click. The primary is fine, because \`buyLink\` has the store
909
+ inject the buy URL. So \`cta-secondary-href\` has no default and wants setting;
910
+ *Design copy & a live secondary* below shows both.
911
+
912
+ **And the heading level is no longer a gap.** It was a third one, unlisted
913
+ above because it is not visible in a screenshot: the headline's \`as="h1"\` was
914
+ hardcoded, so a page could not place the hero in its own outline. It is the
915
+ \`heading-level\` prop as of 2026-09-17 — see the heading-structure section in
916
+ this component's docs. Same decision as the product name, reached the other
917
+ way: the page owns the outline, so the page gets the say.`
918
+ }
919
+ }
920
+ },
921
+ render: () => html`
922
+ <style>
923
+ ${ensembleCSS.cssText}
924
+ ${headerCSS.cssText}
925
+
926
+ /* (1) stands in for :host, which cannot match in light DOM. The two
927
+ declarations are headerCSS's own -- the section's ground and the 16px
928
+ row padding the height study bought. */
929
+ .bd-header-reference {
930
+ display: block;
931
+ background: var(--color-surface-default);
932
+ padding-block: var(--spacing-16);
933
+ }
934
+
935
+ /* (2) stands in for ::slotted([slot="header-picture"]). Same four
936
+ declarations; the picture is a real child here rather than slotted,
937
+ so the shadow pseudo-element never matches it. */
938
+ .bd-header-reference .bd-header-image {
939
+ width: 100%;
940
+ height: 100%;
941
+ object-fit: cover;
942
+ }
943
+
944
+ /* (3) stands in for ::slotted([slot="disclaimer-bottom-link"]). */
945
+ .bd-header-reference .bd-disclaimer-bottom a {
946
+ color: var(--color-text-primary);
947
+ text-decoration: underline;
948
+ }
949
+
950
+ /* NOT a stand-in -- the one thing the reference draws that the component
951
+ has no markup for yet, and the whole reason this story exists. Figma
952
+ stacks the product name as two text nodes in a zero-gap vertical
953
+ frame, which is two block lines on the kicker's own 1.3 leading. The
954
+ type comes from .bd-product-name in headerCSS above; these spans only
955
+ make the break. */
956
+ .bd-header-reference .bd-product-name span {
957
+ display: block;
958
+ }
959
+ </style>
960
+
961
+ <div class="bd-header-reference">
962
+ <div class="bd-ensemble bd-container">
963
+ <div class="bd-header-left">
964
+ <p class="bd-product-name">
965
+ <span>${FIGMA.brand}</span><span>${FIGMA.product}</span>
966
+ </p>
967
+ <bd-h as="h1" class="bd-value-prop">${FIGMA.valueProp}</bd-h>
968
+ <bd-p variant="regular" class="bd-description">${FIGMA.description}</bd-p>
969
+
970
+ <div class="bd-price-zone-top">
971
+ <bd-p variant="regular" font-weight="medium" class="original-price">
972
+ <s>${FIGMA.fullPrice}</s>
973
+ </bd-p>
974
+ <bd-badge tone="success" variant="fill" size="lg" class="bd-discount"
975
+ >${FIGMA.discount}</bd-badge
976
+ >
977
+ </div>
978
+
979
+ <span class="bd-price-zone-final">${FIGMA.finalPrice}</span>
980
+
981
+ <div class="bd-cta">
982
+ <bd-button-link label=${FIGMA.ctaPrimary} variant="accent" size="lg">
983
+ ${FIGMA.ctaPrimary}
984
+ </bd-button-link>
985
+ <bd-button-link label=${FIGMA.ctaSecondary} variant="tertiary" size="lg">
986
+ ${FIGMA.ctaSecondary}
987
+ </bd-button-link>
988
+ </div>
989
+
990
+ <bd-p variant="xsmall" class="bd-disclaimer-top">${FIGMA.disclaimerTop}</bd-p>
991
+ <bd-p variant="xsmall" class="bd-disclaimer-bottom"
992
+ >${FIGMA.disclaimerNext}</bd-p
993
+ >
994
+ </div>
995
+
996
+ <div class="bd-header-right">
997
+ <div class="bd-header-image-wrapper">
998
+ <img class="bd-header-image" src="/images/bd-header-img.jpg" alt="" />
999
+ <div class="bd-header-scrim" aria-hidden="true"></div>
1000
+ <bd-brand-signature-icon class="bd-header-signature"></bd-brand-signature-icon>
1001
+ </div>
1002
+ </div>
1003
+ </div>
1004
+ </div>
1005
+ `
1006
+ };