@repobit/dex-system-design 0.23.10 → 0.23.11

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.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,14 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [0.23.11](https://github.com/bitdefender/dex-core/compare/@repobit/dex-system-design@0.23.10...@repobit/dex-system-design@0.23.11) (2026-04-01)
7
+
8
+ ### Bug Fixes
9
+
10
+ * **DEX-1014:** fix gap and card sizes
11
+ * **DEX-1014:** new changes for anchor-nav and compare to solve some issues
12
+
13
+
6
14
  ## [0.23.10](https://github.com/bitdefender/dex-core/compare/@repobit/dex-system-design@0.23.9...@repobit/dex-system-design@0.23.10) (2026-03-31)
7
15
 
8
16
  ### Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@repobit/dex-system-design",
3
- "version": "0.23.10",
3
+ "version": "0.23.11",
4
4
  "description": "Design system based on Web Components.",
5
5
  "author": "Iordache Matei Cezar <miordache@bitdefender.com>",
6
6
  "homepage": "https://github.com/bitdefender/dex-core#readme",
@@ -86,5 +86,5 @@
86
86
  "volta": {
87
87
  "node": "24.14.0"
88
88
  },
89
- "gitHead": "d6c2015f70aedf55bb7fe4860b3c2ce1978a52f4"
89
+ "gitHead": "d585162a40165e7aa8f96463433be03d5aef2f05"
90
90
  }
@@ -1,92 +1,341 @@
1
1
  import { css } from "lit";
2
2
 
3
3
  export const anchorNavStyles = css`
4
- :host {
5
- padding: var(--spacing-0) 15em;
6
- margin: var(--spacing-0) auto;
7
- --bd-accesibility-focus: var(--color-blue-500);
8
- }
9
-
10
- .bd-container {
11
- flex-wrap: wrap;
12
- justify-content: space-between;
13
- gap: 2.5em;
14
- }
15
-
16
- a:focus-visible {
17
- outline: var(--spacing-2) solid var(--bd-accesibility-focus);
18
- outline-offset: var(--spacing-2);
19
- border-radius: var(--space-2xs);
20
- }
21
-
22
- nav {
23
- position: sticky;
24
- top: 0;
25
- z-index: 1000;
26
- box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
27
- display: flex;
28
- justify-content: space-between;
29
- align-items: center;
30
- border-top: 1px solid #f0f0f0;
31
- border-bottom: 1px solid #f0f0f0;
32
- padding: var(--spacing-10) 15em;
33
- width: 100%;
34
- box-sizing: border-box;
35
- background: white;
36
- margin-bottom: var(--spacing-64);
37
- }
38
-
39
- .bd-container {
40
- padding: 50px;
41
- }
42
-
43
-
44
- [id$="-section"] {
45
- scroll-margin-top: 120px;
46
- }
47
-
48
- .anchor-links {
49
- position: relative;
50
- display: flex;
51
- gap: var(--spacing-32);
52
- }
53
-
54
- a {
55
- position: relative;
56
- text-decoration: none;
57
- color: black;
58
- font-weight: 600;
59
- padding: var(--spacing-10) var(--spacing-0);
60
- display: inline-block;
61
- font-size: var(--typography-body-regular-fontSize) !important;
62
- font-family: var(--typography-fontFamily-sans) !important;
63
- }
64
-
65
- a::after {
66
- content: "";
67
- position: absolute;
68
- left: var(--spacing-0);
69
- bottom: -14px;
70
- width: 0%;
71
- height: 3px;
72
- background-color: #006dff;
73
- transition: width 0.3s ease;
74
- }
75
-
76
- a.active::after {
77
- width: 100%;
78
- }
79
-
80
- `;
81
-
82
- export const anchorNavItemStyles = css`
83
4
  :host {
84
- display: none;
5
+ display: block;
6
+ position: sticky;
7
+ top: var(--spacing-0);
8
+ z-index: 1000;
9
+ border-top: var(--border-width-1) solid var(--color-neutral-100);
10
+ border-bottom: var(--border-width-1) solid var(--color-neutral-100);
11
+ padding-block: var(--spacing-10);
12
+ padding-inline: var(--layout-ensemble-inline-padding);
13
+ width: 100%;
14
+ box-sizing: border-box;
15
+ background: var(--color-neutral-0);
16
+ margin-bottom: var(--spacing-0);
17
+ --bd-accesibility-focus: var(--color-blue-500);
85
18
  }
86
19
 
87
- :host([active]) {
20
+ a:focus-visible,
21
+ .bd-anchor-nav__cta:focus-visible,
22
+ .bd-anchor-nav__dropdown-toggle:focus-visible,
23
+ .bd-anchor-nav__dropdown-option:focus-visible {
24
+ outline: var(--spacing-2) solid var(--bd-accesibility-focus);
25
+ outline-offset: var(--spacing-2);
26
+ border-radius: var(--space-2xs);
27
+ }
28
+
29
+ nav {
88
30
  display: block;
89
- padding: var(--spacing-0) 15em;
90
- margin: var(--spacing-0) auto;
31
+ margin: var(--spacing-0);
32
+ padding: var(--spacing-0);
33
+ width: 100%;
34
+ box-sizing: border-box;
35
+ }
36
+
37
+ .bd-anchor-nav__inner {
38
+ display: flex;
39
+ flex-wrap: wrap;
40
+ justify-content: space-between;
41
+ align-items: center;
42
+ gap: var(--spacing-32);
43
+ width: 100%;
44
+ max-width: var(--layout-compare-grid-max);
45
+ margin-inline: auto;
46
+ box-sizing: border-box;
47
+ }
48
+
49
+ .anchor-links {
50
+ position: relative;
51
+ display: flex;
52
+ flex-wrap: wrap;
53
+ align-items: center;
54
+ gap: var(--spacing-16) var(--spacing-32);
55
+ row-gap: var(--spacing-8);
56
+ flex: 1 1 auto;
57
+ min-width: 0;
58
+ }
59
+
60
+ .anchor-links--desktop {
61
+ display: flex;
62
+ }
63
+
64
+ .bd-anchor-nav__dropdown {
65
+ display: none;
66
+ position: relative;
67
+ flex: 1 1 auto;
68
+ min-width: 0;
69
+ width: 100%;
70
+ }
71
+
72
+ /* ── Dropdown toggle ── */
73
+
74
+ .bd-anchor-nav__dropdown-toggle {
75
+ display: flex;
76
+ align-items: center;
77
+ justify-content: space-between;
78
+ gap: var(--spacing-12);
79
+ width: 100%;
80
+ min-height: var(--dimension-48px);
81
+ padding-block: var(--spacing-10);
82
+ padding-inline: var(--spacing-16);
83
+ box-sizing: border-box;
84
+ font-family: var(--typography-fontFamily-sans);
85
+ font-size: var(--typography-body-regular-fontSize);
86
+ font-weight: var(--typography-fontWeight-semibold);
87
+ color: var(--color-neutral-900);
88
+ background: var(--color-neutral-25);
89
+ border: var(--border-width-1) solid var(--color-neutral-200);
90
+ border-radius: var(--radius-lg);
91
+ cursor: pointer;
92
+ text-align: left;
93
+ transition:
94
+ background var(--transition-duration-fast) var(--transition-easing-smooth),
95
+ border-color var(--transition-duration-fast) var(--transition-easing-smooth),
96
+ border-radius var(--transition-duration-fast) var(--transition-easing-smooth);
97
+ }
98
+
99
+ .bd-anchor-nav__dropdown-toggle:hover {
100
+ background: var(--color-neutral-50);
101
+ border-color: var(--color-neutral-300);
102
+ }
103
+
104
+ .bd-anchor-nav__dropdown-toggle[aria-expanded="true"] {
105
+ background: var(--color-neutral-50);
106
+ border-color: var(--color-blue-200);
107
+ border-radius: var(--radius-lg) var(--radius-lg) var(--spacing-0) var(--spacing-0);
108
+ }
109
+
110
+ .bd-anchor-nav__dropdown-label {
111
+ flex: 1 1 auto;
112
+ min-width: 0;
113
+ overflow: hidden;
114
+ text-overflow: ellipsis;
115
+ white-space: nowrap;
116
+ transform-origin: left center;
117
+ }
118
+
119
+ .bd-anchor-nav__dropdown-chevron {
120
+ flex: 0 0 auto;
121
+ display: flex;
122
+ color: var(--color-neutral-600);
123
+ transition: transform var(--transition-duration-normal) var(--transition-easing-smooth);
124
+ }
125
+
126
+ .bd-anchor-nav__dropdown-toggle[aria-expanded="true"] .bd-anchor-nav__dropdown-chevron {
127
+ transform: rotate(180deg);
128
+ color: var(--color-blue-500);
129
+ }
130
+
131
+ /* ── Dropdown panel ── */
132
+
133
+ .bd-anchor-nav__dropdown-panel {
134
+ position: absolute;
135
+ left: var(--spacing-0);
136
+ right: var(--spacing-0);
137
+ top: 100%;
138
+ z-index: 1001;
139
+ max-height: min(50vh, 20rem);
140
+ overflow-y: auto;
141
+ background: var(--color-neutral-0);
142
+ border: var(--border-width-1) solid var(--color-blue-200);
143
+ border-top: none;
144
+ border-radius: var(--spacing-0) var(--spacing-0) var(--radius-lg) var(--radius-lg);
145
+ box-shadow: var(--shadow-md);
146
+ }
147
+
148
+ .bd-anchor-nav__dropdown-list {
149
+ list-style: none;
150
+ margin: var(--spacing-0);
151
+ padding: var(--spacing-8);
152
+ display: flex;
153
+ flex-direction: column;
154
+ gap: var(--spacing-2);
155
+ }
156
+
157
+ /* ── Dropdown options ── */
158
+
159
+ .bd-anchor-nav__dropdown-option {
160
+ display: flex;
161
+ align-items: center;
162
+ width: 100%;
163
+ min-height: var(--dimension-48px);
164
+ padding-block: var(--spacing-12);
165
+ padding-inline: var(--spacing-16);
166
+ box-sizing: border-box;
167
+ font-family: var(--typography-fontFamily-sans);
168
+ font-size: var(--typography-body-regular-fontSize);
169
+ font-weight: var(--typography-fontWeight-normal);
170
+ color: var(--color-neutral-900);
171
+ background: transparent;
172
+ border: none;
173
+ border-radius: var(--radius-md);
174
+ cursor: pointer;
175
+ text-align: left;
176
+ transition:
177
+ color var(--transition-duration-fast) var(--transition-easing-smooth),
178
+ background var(--transition-duration-fast) var(--transition-easing-smooth);
179
+ }
180
+
181
+ .bd-anchor-nav__dropdown-option:hover:not(.bd-anchor-nav__dropdown-option--active) {
182
+ color: var(--color-blue-500);
183
+ background: var(--color-neutral-25);
184
+ }
185
+
186
+ .bd-anchor-nav__dropdown-option--active {
187
+ font-weight: var(--typography-fontWeight-semibold);
188
+ background: var(--color-blue-50);
189
+ color: var(--color-blue-700);
190
+ }
191
+
192
+ .bd-anchor-nav__dropdown-option--active:hover {
193
+ color: var(--color-blue-700);
194
+ background: var(--color-blue-50);
195
+ }
196
+
197
+ /* ── Desktop links ── */
198
+
199
+ a {
200
+ position: relative;
201
+ text-decoration: none;
202
+ color: var(--color-neutral-900);
203
+ font-weight: var(--typography-fontWeight-normal);
204
+ padding: var(--spacing-10) var(--spacing-0);
205
+ display: inline-block;
206
+ font-size: var(--typography-body-regular-fontSize);
207
+ font-family: var(--typography-body-regular-fontFamily);
208
+ transition: color var(--transition-duration-fast) var(--transition-easing-smooth);
209
+ }
210
+
211
+ a.active {
212
+ font-weight: var(--typography-fontWeight-semibold);
213
+ color: var(--color-neutral-900);
214
+ }
215
+
216
+ a:hover:not(.active) {
217
+ color: var(--color-blue-500);
218
+ }
219
+
220
+ a.active:hover {
221
+ color: var(--color-neutral-900);
222
+ }
223
+
224
+ a::after {
225
+ content: "";
226
+ position: absolute;
227
+ left: var(--spacing-0);
228
+ bottom: calc(-1 * var(--spacing-14));
229
+ width: 0%;
230
+ height: var(--border-width-3);
231
+ background-color: var(--color-blue-500);
232
+ transition: width var(--transition-duration-slow) var(--transition-easing-ease-out);
233
+ }
234
+
235
+ a.active::after {
236
+ width: 100%;
237
+ }
238
+
239
+ /* ── CTA button ── */
240
+
241
+ .bd-anchor-nav__cta {
242
+ font-family: var(--typography-fontFamily-sans);
243
+ font-size: var(--typography-fontSize-lg);
244
+ font-weight: var(--typography-fontWeight-semibold);
245
+ line-height: var(--typography-lineHeight-normal);
246
+ padding: var(--spacing-10) var(--spacing-20);
247
+ border: none;
248
+ border-radius: var(--radius-md);
249
+ cursor: pointer;
250
+ background: var(--color-red-500);
251
+ color: var(--color-neutral-0);
252
+ transition: background var(--transition-duration-fast) var(--transition-easing-smooth);
253
+ }
254
+
255
+ .bd-anchor-nav__cta:hover {
256
+ background: var(--color-red-600);
257
+ }
258
+
259
+ .bd-anchor-nav__cta:active {
260
+ background: var(--color-red-700);
261
+ }
262
+
263
+ /* ── Animations ── */
264
+
265
+ @keyframes bd-anchor-nav-dropdown-label-swap {
266
+ from {
267
+ opacity: 0.35;
268
+ transform: translateY(var(--spacing-4));
269
+ }
270
+ to {
271
+ opacity: 1;
272
+ transform: translateY(var(--spacing-0));
273
+ }
274
+ }
275
+
276
+ @media (prefers-reduced-motion: reduce) {
277
+ a::after,
278
+ .bd-anchor-nav__dropdown-chevron,
279
+ .bd-cta,
280
+ .bd-anchor-nav__dropdown-label--swap {
281
+ animation: none !important;
282
+ }
283
+ }
284
+
285
+ /* ── Mobile ── */
286
+
287
+ @media (max-width: 768px) {
288
+ :host {
289
+ padding-block: var(--spacing-10);
290
+ padding-inline: var(--layout-ensemble-inline-padding);
291
+ }
292
+
293
+ .bd-anchor-nav__inner {
294
+ flex-direction: column;
295
+ align-items: stretch;
296
+ gap: var(--spacing-16);
297
+ }
298
+
299
+ .anchor-links--desktop {
300
+ display: none !important;
301
+ }
302
+
303
+ .bd-anchor-nav__dropdown {
304
+ display: block;
305
+ }
306
+
307
+ .bd-anchor-nav__dropdown-label.bd-anchor-nav__dropdown-label--swap {
308
+ animation: bd-anchor-nav-dropdown-label-swap var(--transition-duration-normal)
309
+ var(--transition-easing-smooth);
310
+ }
311
+
312
+ .bd-cta {
313
+ align-self: stretch;
314
+ display: block;
315
+ overflow: hidden;
316
+ max-height: var(--spacing-0);
317
+ opacity: 0;
318
+ transform: translateY(calc(-1 * var(--spacing-8)));
319
+ pointer-events: none;
320
+ margin-top: calc(-1 * var(--spacing-16));
321
+ transition:
322
+ max-height var(--transition-duration-slow) var(--transition-easing-ease-out),
323
+ opacity var(--transition-duration-normal) var(--transition-easing-smooth),
324
+ transform var(--transition-duration-normal) var(--transition-easing-smooth),
325
+ margin-top var(--transition-duration-normal) var(--transition-easing-smooth);
326
+ }
327
+
328
+ .bd-anchor-nav__inner[data-mobile-cta-visible] .bd-cta {
329
+ max-height: var(--spacing-128);
330
+ opacity: 1;
331
+ transform: translateY(var(--spacing-0));
332
+ pointer-events: auto;
333
+ margin-top: var(--spacing-0);
334
+ }
335
+
336
+ .bd-anchor-nav__cta {
337
+ width: 100%;
338
+ box-sizing: border-box;
339
+ }
91
340
  }
92
- `;
341
+ `;
@@ -1,8 +1,20 @@
1
- import { LitElement, html } from "lit";
1
+ import { LitElement, html, nothing } from "lit";
2
2
  import { tokens } from "../../tokens/tokens.js";
3
- import "../Button/Button.js";
4
3
  import { anchorNavStyles } from "./anchor-nav.css.js";
5
4
 
5
+ /** Parse token length from computed style (e.g. `8rem` → px). */
6
+ function parseLengthToPx(value) {
7
+ const s = (value || "").trim();
8
+ if (!s) return 0;
9
+ if (s.endsWith("px")) return parseFloat(s);
10
+ if (s.endsWith("rem")) {
11
+ const root =
12
+ parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
13
+ return parseFloat(s) * root;
14
+ }
15
+ return parseFloat(s) || 0;
16
+ }
17
+
6
18
  class BdAnchorNavItem extends HTMLElement {
7
19
  constructor() {
8
20
  super();
@@ -14,67 +26,267 @@ class BdAnchorNavItem extends HTMLElement {
14
26
  }
15
27
 
16
28
  render() {
17
- this.shadowRoot.innerHTML = `
18
- <nav>
19
- <slot></slot>
20
- </nav>
21
- `;
29
+ this.shadowRoot.innerHTML = `<slot></slot>`;
22
30
  }
23
31
  }
24
32
 
25
-
26
33
  class BdAnchorNav extends LitElement {
34
+ static properties = {
35
+ activeId : { type: String, state: true },
36
+ /** Landmark label for the outer `<nav>` (screen readers). */
37
+ navLabel : { type: String, attribute: "aria-label" },
38
+ /** Mobile dropdown open state */
39
+ _dropdownOpen : { type: Boolean, state: true },
40
+ /** Mobile: show Buy now only after first anchor section has been scrolled past */
41
+ _mobileCtaRevealed: { type: Boolean, state: true }
42
+ };
43
+
27
44
  static styles = [tokens, anchorNavStyles];
28
45
 
29
46
  constructor() {
30
47
  super();
31
48
  this.activeId = "";
49
+ this.navLabel = "Section navigation";
50
+ this._dropdownOpen = false;
51
+ this._mobileCtaRevealed = false;
52
+ this._panelId = `bd-anchor-nav-panel-${Math.random().toString(36)
53
+ .slice(2, 11)}`;
54
+ this._onScrollSpy = this._onScrollSpy.bind(this);
55
+ this._onDocumentPointerDown = this._onDocumentPointerDown.bind(this);
56
+ this._onKeydown = this._onKeydown.bind(this);
57
+ this._scrollPending = false;
58
+ /** While set, scrollspy must not overwrite activeId during smooth scroll to a clicked section */
59
+ this._programmaticScrollTargetId = null;
60
+ this._unlockScrollSpyTimer = null;
61
+ this._scrollEndUnlockHandler = null;
62
+ }
63
+
64
+ connectedCallback() {
65
+ super.connectedCallback();
66
+ window.addEventListener("scroll", this._onScrollSpy, { passive: true });
67
+ window.addEventListener("resize", this._onScrollSpy, { passive: true });
68
+ document.addEventListener("pointerdown", this._onDocumentPointerDown, true);
69
+ document.addEventListener("keydown", this._onKeydown, true);
70
+ queueMicrotask(() => this._runScrollSpy());
71
+ }
72
+
73
+ disconnectedCallback() {
74
+ super.disconnectedCallback();
75
+ this._cancelScrollSpyUnlock();
76
+ window.removeEventListener("scroll", this._onScrollSpy);
77
+ window.removeEventListener("resize", this._onScrollSpy);
78
+ document.removeEventListener("pointerdown", this._onDocumentPointerDown, true);
79
+ document.removeEventListener("keydown", this._onKeydown, true);
80
+ }
81
+
82
+ _cancelScrollSpyUnlock() {
83
+ if (this._unlockScrollSpyTimer != null) {
84
+ clearTimeout(this._unlockScrollSpyTimer);
85
+ this._unlockScrollSpyTimer = null;
86
+ }
87
+ if (this._scrollEndUnlockHandler) {
88
+ window.removeEventListener("scrollend", this._scrollEndUnlockHandler);
89
+ this._scrollEndUnlockHandler = null;
90
+ }
91
+ }
92
+
93
+ /** After in-nav navigation, ignore scrollspy until scroll finishes (avoids flashing 2→3→4). */
94
+ _scheduleScrollSpyUnlock() {
95
+ this._cancelScrollSpyUnlock();
96
+ const onScrollEnd = () => {
97
+ if (this._unlockScrollSpyTimer != null) {
98
+ clearTimeout(this._unlockScrollSpyTimer);
99
+ this._unlockScrollSpyTimer = null;
100
+ }
101
+ window.removeEventListener("scrollend", onScrollEnd);
102
+ this._scrollEndUnlockHandler = null;
103
+ this._programmaticScrollTargetId = null;
104
+ this._runScrollSpy();
105
+ };
106
+ this._scrollEndUnlockHandler = onScrollEnd;
107
+ window.addEventListener("scrollend", onScrollEnd, { passive: true });
108
+ this._unlockScrollSpyTimer = window.setTimeout(() => {
109
+ this._unlockScrollSpyTimer = null;
110
+ if (this._scrollEndUnlockHandler) {
111
+ window.removeEventListener("scrollend", this._scrollEndUnlockHandler);
112
+ this._scrollEndUnlockHandler = null;
113
+ }
114
+ if (this._programmaticScrollTargetId) {
115
+ this._programmaticScrollTargetId = null;
116
+ this._runScrollSpy();
117
+ }
118
+ }, 1000);
119
+ }
120
+
121
+ _onKeydown(e) {
122
+ if (e.key === "Escape" && this._dropdownOpen) {
123
+ this._dropdownOpen = false;
124
+ this.requestUpdate();
125
+ }
126
+ }
127
+
128
+ _onDocumentPointerDown(e) {
129
+ if (!this._dropdownOpen) return;
130
+ const path = e.composedPath();
131
+ if (path.includes(this)) return;
132
+ this._dropdownOpen = false;
133
+ this.requestUpdate();
134
+ }
135
+
136
+ _onScrollSpy() {
137
+ if (this._scrollPending) return;
138
+ this._scrollPending = true;
139
+ requestAnimationFrame(() => {
140
+ this._scrollPending = false;
141
+ this._runScrollSpy();
142
+ });
143
+ }
144
+
145
+ _runScrollSpy() {
146
+ const count = this._navItemCount();
147
+ let next = this.activeId;
148
+ if (count && !this._programmaticScrollTargetId) {
149
+ const y = window.scrollY + this._stickyOffsetPx() + 2;
150
+ next = "anchor-1";
151
+ for (let i = 0; i < count; i++) {
152
+ const el = document.getElementById(`anchor-${i + 1}-section`);
153
+ if (!el) continue;
154
+ const top = el.getBoundingClientRect().top + window.scrollY;
155
+ if (top <= y) next = `anchor-${i + 1}`;
156
+ }
157
+ }
158
+
159
+ const isMobile = window.matchMedia("(max-width: 768px)").matches;
160
+ const first = document.getElementById("anchor-1-section");
161
+ let nextCta;
162
+ if (!isMobile) {
163
+ nextCta = true;
164
+ } else if (!first) {
165
+ nextCta = true;
166
+ } else {
167
+ /* Show only while first section is fully scrolled past; hide again when it scrolls back into view */
168
+ nextCta = first.getBoundingClientRect().bottom <= 0;
169
+ }
170
+
171
+ if (this.activeId !== next || this._mobileCtaRevealed !== nextCta) {
172
+ this.activeId = next;
173
+ this._mobileCtaRevealed = nextCta;
174
+ this.requestUpdate();
175
+ }
176
+ }
177
+
178
+ _navItemCount() {
179
+ return Array.from(this.children).filter(
180
+ (el) => el.tagName === "BD-ANCHOR-NAV-ITEM"
181
+ ).length;
32
182
  }
33
183
 
34
184
  firstUpdated() {
35
- const firstItem = this.querySelector("bd-anchor-nav-item");
36
- if (firstItem) {
37
- this.activeId = firstItem.id;
185
+ const n = this._navItemCount();
186
+ if (n) {
187
+ this.activeId = "anchor-1";
38
188
  this.requestUpdate();
39
189
  }
190
+ queueMicrotask(() => this._runScrollSpy());
191
+ }
192
+
193
+ updated(changedProperties) {
194
+ super.updated(changedProperties);
195
+ if (!changedProperties.has("activeId")) return;
196
+ const prev = changedProperties.get("activeId");
197
+ if (prev === undefined || prev === "") return;
198
+ if (prev === this.activeId) return;
199
+ queueMicrotask(() => this._playMobileDropdownLabelSwap());
200
+ }
201
+
202
+ /** Mobile dropdown title: subtle fade + slide when scrollspy (or tap) changes section. */
203
+ _playMobileDropdownLabelSwap() {
204
+ if (typeof window === "undefined") return;
205
+ if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
206
+ if (!window.matchMedia("(max-width: 768px)").matches) return;
207
+ const el = this.renderRoot?.querySelector(".bd-anchor-nav__dropdown-label");
208
+ if (!el) return;
209
+ el.classList.remove("bd-anchor-nav__dropdown-label--swap");
210
+ void el.offsetWidth;
211
+ el.classList.add("bd-anchor-nav__dropdown-label--swap");
212
+ el.addEventListener(
213
+ "animationend",
214
+ () => el.classList.remove("bd-anchor-nav__dropdown-label--swap"),
215
+ { once: true }
216
+ );
217
+ }
218
+
219
+ /**
220
+ * Scroll clearance = actual sticky bar height + small inset (not a fixed 128px token).
221
+ * Matches best practice: target headings sit just under the bar, not with extra dead space.
222
+ */
223
+ _stickyOffsetPx() {
224
+ const barPx = this.offsetHeight;
225
+ const insetRaw = getComputedStyle(this).getPropertyValue("--spacing-8")
226
+ .trim();
227
+ const insetPx = parseLengthToPx(insetRaw) || 8;
228
+ if (barPx > 0) return barPx + insetPx;
229
+ const fallback = getComputedStyle(this).getPropertyValue("--spacing-128")
230
+ .trim();
231
+ return parseLengthToPx(fallback) || 128;
232
+ }
233
+
234
+ _scrollToY(event, getTarget) {
235
+ if (event?.preventDefault) event.preventDefault();
236
+ const el = getTarget();
237
+ if (!el) return;
238
+ const offset = this._stickyOffsetPx();
239
+ const rect = el.getBoundingClientRect();
240
+ const scrollTop = window.scrollY ?? window.pageYOffset;
241
+ const elementY = rect.top + scrollTop;
242
+ const prefersReduced =
243
+ typeof window !== "undefined" &&
244
+ window.matchMedia("(prefers-reduced-motion: reduce)").matches;
245
+ window.scrollTo({
246
+ top : elementY - offset,
247
+ behavior: prefersReduced ? "auto" : "smooth"
248
+ });
40
249
  }
41
250
 
42
251
  handleClick(event, id) {
43
252
  event.preventDefault();
253
+ this._cancelScrollSpyUnlock();
254
+ this._programmaticScrollTargetId = id;
44
255
  this.activeId = id;
45
256
 
46
- const sectionEl = document.querySelector(`#${id}-section`);
257
+ const sectionEl = document.getElementById(`${id}-section`);
47
258
  if (sectionEl) {
48
- const offset = 120;
49
- const rect = sectionEl.getBoundingClientRect();
50
- const scrollTop = window.pageYOffset;
51
- const elementY = rect.top + scrollTop;
52
- const offsetY = elementY - offset;
53
-
54
- window.scrollTo({
55
- top : offsetY,
56
- behavior: "smooth"
57
- });
259
+ this._scrollToY(event, () => sectionEl);
260
+ this._scheduleScrollSpyUnlock();
261
+ } else {
262
+ this._programmaticScrollTargetId = null;
58
263
  }
59
264
 
60
265
  this.requestUpdate();
61
266
  }
62
267
 
268
+ _toggleDropdown(e) {
269
+ e.preventDefault();
270
+ e.stopPropagation();
271
+ this._dropdownOpen = !this._dropdownOpen;
272
+ this.requestUpdate();
273
+ }
63
274
 
275
+ _selectFromDropdown(e, id) {
276
+ this.handleClick(e, id);
277
+ this._dropdownOpen = false;
278
+ this.requestUpdate();
279
+ }
64
280
 
65
281
  scrollToPricing(event) {
66
- event.preventDefault();
67
- const pricingSection = document.querySelector("#section-pricing");
68
- if (pricingSection) {
69
- const offset = 120;
70
- const elementY = pricingSection.getBoundingClientRect().top + window.pageYOffset;
71
- const offsetY = elementY - offset;
72
-
73
- window.scrollTo({
74
- top : offsetY,
75
- behavior: "smooth"
76
- });
77
- }
282
+ this._scrollToY(event, () => document.getElementById("section-pricing"));
283
+ }
284
+
285
+ _activeLabel(items) {
286
+ const m = /^anchor-(\d+)$/.exec(this.activeId || "");
287
+ const n = m ? parseInt(m[1], 10) : 1;
288
+ const idx = Math.min(Math.max(n - 1, 0), Math.max(0, items.length - 1));
289
+ return items[idx]?.getAttribute("title") || items[0]?.getAttribute("title") || "Section";
78
290
  }
79
291
 
80
292
  render() {
@@ -82,39 +294,102 @@ class BdAnchorNav extends LitElement {
82
294
  (el) => el.tagName === "BD-ANCHOR-NAV-ITEM"
83
295
  );
84
296
 
297
+ const linkRow = (cls) => html`
298
+ <div class="${cls}">
299
+ ${items.map((item, index) => {
300
+ const id = `anchor-${index + 1}`;
301
+ const isActive = this.activeId === id;
302
+ return html`
303
+ <a
304
+ href="#${id}-section"
305
+ class="${isActive ? "active" : ""}"
306
+ aria-current=${isActive ? "true" : nothing}
307
+ @click=${(e) => this.handleClick(e, id)}
308
+ >
309
+ ${item.getAttribute("title")}
310
+ </a>
311
+ `;
312
+ })}
313
+ </div>
314
+ `;
315
+
316
+ const dropdownLabel = this._activeLabel(items);
317
+
85
318
  return html`
86
- <nav>
87
- <div class="anchor-links">
88
- ${items.map((item, index) => {
89
- const id = `anchor-${index + 1}`;
90
- return html`
91
- <a
92
- href="#${id}"
93
- class="${this.activeId === id ? "active" : ""}"
94
- @click=${(e) => this.handleClick(e, id)}
95
-
96
- >
97
- ${item.getAttribute("title")}
98
- </a>
99
- `;
100
- })}
319
+ <nav aria-label=${this.navLabel}>
320
+ <div
321
+ class="bd-anchor-nav__inner"
322
+ part="inner"
323
+ ?data-mobile-cta-visible=${this._mobileCtaRevealed}
324
+ >
325
+ ${linkRow("anchor-links anchor-links--desktop")}
326
+
327
+ <div class="bd-anchor-nav__dropdown" part="dropdown">
328
+ <button
329
+ type="button"
330
+ class="bd-anchor-nav__dropdown-toggle"
331
+ aria-expanded=${this._dropdownOpen ? "true" : "false"}
332
+ aria-controls=${this._panelId}
333
+ aria-haspopup="listbox"
334
+ @click=${this._toggleDropdown}
335
+ >
336
+ <span class="bd-anchor-nav__dropdown-label">${dropdownLabel}</span>
337
+ <span class="bd-anchor-nav__dropdown-chevron" aria-hidden="true">
338
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="none">
339
+ <path
340
+ d="M5 8l5 5 5-5"
341
+ stroke="currentColor"
342
+ stroke-width="2"
343
+ stroke-linecap="round"
344
+ stroke-linejoin="round"
345
+ />
346
+ </svg>
347
+ </span>
348
+ </button>
349
+ <div
350
+ class="bd-anchor-nav__dropdown-panel"
351
+ id=${this._panelId}
352
+ role="listbox"
353
+ aria-label=${this.navLabel}
354
+ ?hidden=${!this._dropdownOpen}
355
+ >
356
+ <ul class="bd-anchor-nav__dropdown-list" role="presentation">
357
+ ${items.map((item, index) => {
358
+ const id = `anchor-${index + 1}`;
359
+ const isActive = this.activeId === id;
360
+ return html`
361
+ <li role="presentation">
362
+ <button
363
+ type="button"
364
+ role="option"
365
+ class="bd-anchor-nav__dropdown-option ${isActive
366
+ ? "bd-anchor-nav__dropdown-option--active"
367
+ : ""}"
368
+ aria-selected=${isActive ? "true" : "false"}
369
+ @click=${(e) => this._selectFromDropdown(e, id)}
370
+ >
371
+ ${item.getAttribute("title")}
372
+ </button>
373
+ </li>
374
+ `;
375
+ })}
376
+ </ul>
377
+ </div>
101
378
  </div>
102
379
 
103
380
  <div class="bd-cta">
104
- <bd-button
105
- label="Buy now"
106
- kind="danger"
107
- size="md"
381
+ <button
382
+ type="button"
383
+ class="bd-anchor-nav__cta"
108
384
  part="buy-button"
109
- font-size="18px"
110
- font-weight="600"
111
385
  @click=${(e) => this.scrollToPricing(e)}
112
386
  >
113
387
  Buy now
114
- </bd-button>
388
+ </button>
115
389
  </div>
116
- </nav>
117
- `;
390
+ </div>
391
+ </nav>
392
+ `;
118
393
  }
119
394
  }
120
395
 
@@ -1,8 +1,13 @@
1
- import { LitElement, html } from "lit";
1
+ import { LitElement, css, html } from "lit";
2
2
  import { tokens } from "../../tokens/tokens.js";
3
3
  import "../awards/awards-icon.js";
4
4
  import { awardsCSS } from "./awards.css.js";
5
5
 
6
+ const awardslightOverride = css`
7
+ .bd-awards-root {
8
+ gap: 0;
9
+ }
10
+ `;
6
11
  export class AwardsSection extends LitElement {
7
12
  static properties = {
8
13
  tagline : { type: String },
@@ -16,7 +21,8 @@ export class AwardsSection extends LitElement {
16
21
  _featureNarrow: { state: true }
17
22
  };
18
23
 
19
- static styles = [tokens, awardsCSS];
24
+
25
+ static styles = [tokens, awardsCSS, awardslightOverride];
20
26
 
21
27
  /** @type {MediaQueryList | null} */
22
28
  _mql599 = null;
@@ -1,9 +1,10 @@
1
1
  import { html } from "lit";
2
- import "../awards/awards-light.js";
3
2
  import "../heading/heading.js";
4
3
  import "../image/image.js";
5
4
  import "../link/link.js";
6
5
  import "../paragraph/paragraph.js";
6
+ import "./awards-light.js";
7
+ import "./awards.js";
7
8
 
8
9
  export default {
9
10
  title : "Components/Awards Section Light",
@@ -14,40 +15,24 @@ export default {
14
15
  docs : {
15
16
  description: {
16
17
  component: `
17
- **AwardsSection** is a Lit web component that showcases Bitdefender's industry awards and trust signals.
18
+ **AwardsSectionLight** is a lightweight variant of the AwardsSection component that renders only the trust banner and award badges — without the "What's our secret" section or feature cards.
18
19
 
19
- It includes:
20
- - A trust banner with tagline, headline, and subtext
21
- - A responsive awards bar (7 badges split into two rows)
22
- - A "What's our secret" section
23
- - Two feature cards (Machine Learning & Behavioral Analysis)
24
-
25
- ### Responsive Behavior
26
- | Breakpoint | Behavior |
27
- |---|---|
28
- | \`≤ 599px\` | Narrow layout: smaller typography, compact badges |
29
- | \`≤ 767px\` | Feature cards use small typography |
30
- | \`≥ 768px\` | Full desktop layout |
20
+ ### Usage
21
+ \`\`\`html
22
+ <bd-awards-section-light></bd-awards-section-light>
23
+ \`\`\`
31
24
 
32
25
  ### Attributes
33
- | Attribute | Type | Default | Description |
34
- |----------------|--------|---------|------------------------------------------|
35
- | \`tagline\` | String | — | Small text above the headline |
36
- | \`headline\` | String | — | Main heading of the trust banner |
37
- | \`subtext\` | String | — | Supporting paragraph below the headline |
38
- | \`secretTitle\` | String | — | Heading of the "What's our secret" block |
39
- | \`secretSubtext\`| String | — | Subtext of the "What's our secret" block |
40
- | \`desktopAwards\`| Array | — | Award badge objects (see below) |
41
- | \`features\` | Array | — | Feature card objects (see below) |
26
+ | Attribute | Type | Default | Description |
27
+ |----------------|--------|---------|-----------------------------------------|
28
+ | \`tagline\` | String | — | Small text above the headline |
29
+ | \`headline\` | String | — | Main heading of the trust banner |
30
+ | \`subtext\` | String | — | Supporting paragraph below the headline |
31
+ | \`desktopAwards\`| Array | — | Award badge objects |
42
32
 
43
33
  ### Badge object shape
44
34
  \`\`\`js
45
35
  { id: string, label: string, type: "red" | "dark", format: "svg", src: string }
46
- \`\`\`
47
-
48
- ### Feature object shape
49
- \`\`\`js
50
- { icon: "brain" | "chart", title: string, description: string }
51
36
  \`\`\`
52
37
  `
53
38
  }
@@ -69,16 +54,6 @@ It includes:
69
54
  description: "Supporting paragraph below the headline",
70
55
  table : { type: { summary: "string" }, category: "Content" }
71
56
  },
72
- secretTitle: {
73
- control : "text",
74
- description: "Title of the 'What's our secret' section",
75
- table : { type: { summary: "string" }, category: "Content" }
76
- },
77
- secretSubtext: {
78
- control : "text",
79
- description: "Subtext of the 'What's our secret' section",
80
- table : { type: { summary: "string" }, category: "Content" }
81
- },
82
57
  desktopAwards: {
83
58
  control : "object",
84
59
  description: "Array of award badge objects",
@@ -86,14 +61,6 @@ It includes:
86
61
  type : { summary: "Array<{ id, label, type, format, src }>" },
87
62
  category: "Badges"
88
63
  }
89
- },
90
- features: {
91
- control : "object",
92
- description: "Array of feature card objects",
93
- table : {
94
- type : { summary: "Array<{ icon, title, description }>" },
95
- category: "Features"
96
- }
97
64
  }
98
65
  }
99
66
  };
@@ -105,8 +72,6 @@ const defaultArgs = {
105
72
  tagline : "You're Making A Great Choice",
106
73
  headline : "World-class security software you can trust. Always.",
107
74
  subtext : "We've added 25 new accolades in the past two years to the hundreds we have won since we started in 2001 - so you know you are in good hands.",
108
- secretTitle : "What's our secret",
109
- secretSubtext: "At the heart of Bitdefender lies a powerhouse of proprietary technology, setting it miles apart from the competition.",
110
75
  desktopAwards: [
111
76
  {
112
77
  label : "PC MAG\nEDITORS' CHOICE",
@@ -157,18 +122,6 @@ const defaultArgs = {
157
122
  src : "/assets/top-product.svg",
158
123
  id : "top-product"
159
124
  }
160
- ],
161
- features: [
162
- {
163
- icon : "brain",
164
- title : "Machine Learning",
165
- description: "Imagine Bitdefender as a super-smart detective, constantly learning and getting better at spotting bad guys. Just like how you learn from your experiences, Bitdefender learns from millions of cyber threats it encounters every day. It studies their patterns, figures out their tricks, and uses that knowledge to protect your devices from future attacks."
166
- },
167
- {
168
- icon : "chart",
169
- title : "Behavioral Analysis",
170
- description: "Bitdefender keeps an eye on how you and your devices normally behave. So, when something unusual happens, like a strange file trying to sneak into your computer or a suspicious website trying to steal your info, Bitdefender sounds the alarm. It's like having a loyal sidekick that never sleeps, always on the lookout for trouble and ready to jump into action to keep you safe."
171
- }
172
125
  ]
173
126
  };
174
127
 
@@ -177,10 +130,7 @@ const render = (args) => html`
177
130
  .tagline=${args.tagline}
178
131
  .headline=${args.headline}
179
132
  .subtext=${args.subtext}
180
- .secretTitle=${args.secretTitle}
181
- .secretSubtext=${args.secretSubtext}
182
133
  .desktopAwards=${args.desktopAwards}
183
- .features=${args.features}
184
134
  ></bd-awards-section-light>
185
135
  `;
186
136
 
@@ -195,7 +145,7 @@ export const Default = {
195
145
  viewport: { defaultViewport: "desktop" },
196
146
  docs : {
197
147
  description: {
198
- story: "Full desktop layout with all 7 award badges split across two rows (3 top + 4 bottom) and both feature cards."
148
+ story: "Full desktop layout. Trust banner with tagline, headline, subtext, and all 7 award badges split across two rows (3 top + 4 bottom)."
199
149
  }
200
150
  }
201
151
  }
@@ -212,7 +162,7 @@ export const Mobile = {
212
162
  viewport: { defaultViewport: "mobile1" },
213
163
  docs : {
214
164
  description: {
215
- story: "Mobile layout at 375px. The `_narrow` state activates at ≤599px, switching to smaller typography variants (`bd-p kind='small'`, `bd-h as='h4'`) and compact badge sizes."
165
+ story: "Mobile layout at 375px. The `_narrow` state activates at ≤599px, switching to smaller typography variants and compact badge sizes."
216
166
  }
217
167
  }
218
168
  }
@@ -229,30 +179,28 @@ export const Tablet = {
229
179
  viewport: { defaultViewport: "tablet" },
230
180
  docs : {
231
181
  description: {
232
- story: "Tablet layout at 768px. Desktop typography is used since the viewport exceeds 599px. Feature cards switch to small text at ≤767px."
182
+ story: "Tablet layout at 768px. Desktop typography is active since the viewport exceeds 599px."
233
183
  }
234
184
  }
235
185
  }
236
186
  };
237
187
 
238
188
 
239
- // ─── Custom text content ──────────────────────────────────────────────────────
189
+ // ─── Custom text ──────────────────────────────────────────────────────────────
240
190
 
241
191
  export const CustomContent = {
242
192
  name: "Custom Text Content",
243
193
  args: {
244
194
  ...defaultArgs,
245
- tagline : "Trusted by Millions Worldwide",
246
- headline : "Award-winning protection for every device.",
247
- subtext : "From homes to enterprises, Bitdefender has been recognised by the world's top security testing labs year after year.",
248
- secretTitle : "Why we win",
249
- secretSubtext: "Our proprietary AI engine processes over 500 billion queries per day to stay ahead of the most sophisticated threats."
195
+ tagline : "Trusted by Millions Worldwide",
196
+ headline: "Award-winning protection for every device.",
197
+ subtext : "From homes to enterprises, Bitdefender has been recognised by the world's top security testing labs year after year."
250
198
  },
251
199
  render,
252
200
  parameters: {
253
201
  docs: {
254
202
  description: {
255
- story: "All text props replaced with custom values. Use the Controls panel to live-edit any string prop."
203
+ story: "All text props replaced with custom values. Use the Controls panel to live-edit tagline, headline, and subtext."
256
204
  }
257
205
  }
258
206
  }
@@ -293,7 +241,7 @@ export const FewBadges = {
293
241
  parameters: {
294
242
  docs: {
295
243
  description: {
296
- story: "Only 3 badges passed. The first row (`slice(0, 3)`) is full, the second row (`slice(3)`) renders empty. Verifies no layout breakage with fewer items."
244
+ story: "Only 3 badges passed. The first row (`slice(0, 3)`) is full, the second row (`slice(3)`) renders empty. Verifies no layout breakage."
297
245
  }
298
246
  }
299
247
  }
@@ -320,7 +268,7 @@ export const SingleBadge = {
320
268
  parameters: {
321
269
  docs: {
322
270
  description: {
323
- story: "Minimum edge case: a single badge. Both row slices receive at most one item. Ensures no crash or layout breakage."
271
+ story: "Minimum edge case: a single badge. Ensures no crash or layout breakage."
324
272
  }
325
273
  }
326
274
  }
@@ -339,7 +287,7 @@ export const AllDarkBadges = {
339
287
  parameters: {
340
288
  docs: {
341
289
  description: {
342
- story: "All badges set to `type: 'dark'`. Verifies the `.bd-awards-badge--dark` CSS class is applied to every badge in `_renderBadge`."
290
+ story: "All badges set to `type: 'dark'`. Verifies the `.bd-awards-badge--dark` CSS class is applied to every badge."
343
291
  }
344
292
  }
345
293
  }
@@ -365,36 +313,6 @@ export const AllRedBadges = {
365
313
  };
366
314
 
367
315
 
368
- // ─── Custom features ──────────────────────────────────────────────────────────
369
-
370
- export const CustomFeatures = {
371
- name: "Custom Feature Cards",
372
- args: {
373
- ...defaultArgs,
374
- features: [
375
- {
376
- icon : "brain",
377
- title : "AI-Powered Threat Detection",
378
- description: "Our next-generation AI engine analyses over 500 billion security events per day, identifying and neutralising threats before they can cause harm to your system."
379
- },
380
- {
381
- icon : "chart",
382
- title : "Real-Time Protection",
383
- description: "Continuous monitoring of all active processes, network connections, and file system changes ensures that threats are caught the moment they appear — with zero performance impact."
384
- }
385
- ]
386
- },
387
- render,
388
- parameters: {
389
- docs: {
390
- description: {
391
- story: "Custom feature card content. The `icon` field accepts `'brain'` (maps to `/assets/machine-learning.svg`) or `'chart'` (maps to `/assets/analysis.svg`)."
392
- }
393
- }
394
- }
395
- };
396
-
397
-
398
316
  // ─── Small mobile ─────────────────────────────────────────────────────────────
399
317
 
400
318
  export const SmallMobile = {
@@ -429,7 +347,7 @@ export const Playground = {
429
347
  parameters: {
430
348
  docs: {
431
349
  description: {
432
- story: "Fully interactive story. Use the Controls panel to modify every prop in real time: text content, badge arrays, and feature cards."
350
+ story: "Fully interactive story. Use the Controls panel to modify every prop in real time: text content and badge arrays."
433
351
  }
434
352
  }
435
353
  }
@@ -154,7 +154,7 @@ export const awardsCSS = css`
154
154
  align-items: stretch;
155
155
  gap: var(--spacing-32);
156
156
  width: 100%;
157
- max-width: 1228px;
157
+ max-width: 940px;
158
158
  margin: 0 auto;
159
159
  padding: 0 var(--spacing-24);
160
160
  box-sizing: border-box;
@@ -17,13 +17,17 @@ export default css`
17
17
  --cs-bar-track-border: var(--color-neutral-200);
18
18
  --cs-bar-height: var(--dimension-48px);
19
19
  --cs-gap: var(--spacing-32);
20
- /* Figma layout width; between --content-width-max (1200) and --grid-container-max-xxl (1320) */
21
20
  --cs-grid-max: 1228px;
22
21
  }
23
22
 
24
- :host(compare-card),
23
+ :host(compare-card) {
24
+ display: block;
25
+ height: 100%;
26
+ }
27
+
25
28
  :host(compare-bar) {
26
29
  display: block;
30
+ width: 100%;
27
31
  }
28
32
 
29
33
 
@@ -32,15 +36,15 @@ export default css`
32
36
  ──────────────────────────────────────────────────────────────── */
33
37
 
34
38
  .cs-sr-only {
35
- position: absolute;
36
- width: 1px;
37
- height: 1px;
38
- padding: 0;
39
- margin: -1px;
40
- overflow: hidden;
41
- clip: rect(0, 0, 0, 0);
39
+ position: absolute;
40
+ width: 1px;
41
+ height: 1px;
42
+ padding: 0;
43
+ margin: -1px;
44
+ overflow: hidden;
45
+ clip: rect(0, 0, 0, 0);
42
46
  white-space: nowrap;
43
- border: 0;
47
+ border: 0;
44
48
  }
45
49
 
46
50
  .cs-section {
@@ -70,6 +74,7 @@ export default css`
70
74
  max-width: var(--cs-grid-max);
71
75
  margin: 0 auto;
72
76
  justify-content: center;
77
+ align-items: stretch;
73
78
  }
74
79
 
75
80
  @media (max-width: 768px) {
@@ -80,7 +85,7 @@ export default css`
80
85
 
81
86
 
82
87
  /* ──────────────────────────────────────────────────────────────
83
- 3. compare-card (nearest radii: 20px Figma → --radius-3xl 24px)
88
+ 3. compare-card
84
89
  ──────────────────────────────────────────────────────────────── */
85
90
 
86
91
  .card {
@@ -99,6 +104,7 @@ export default css`
99
104
  flex-direction: column;
100
105
  align-items: flex-start;
101
106
  gap: var(--spacing-8);
107
+ flex: 1 1 auto;
102
108
  }
103
109
 
104
110
  .card-icon-wrap {
@@ -109,10 +115,10 @@ export default css`
109
115
 
110
116
  .card-icon-wrap bd-img {
111
117
  display: block;
112
- width: 100%;
113
- height: 100%;
118
+ width: 100%;
119
+ height: 100%;
114
120
  }
115
-
121
+
116
122
  .card-text-wrap {
117
123
  display: flex;
118
124
  flex-direction: column;
@@ -133,22 +139,20 @@ export default css`
133
139
  flex-direction: column;
134
140
  gap: var(--spacing-8);
135
141
  align-self: stretch;
142
+ flex: 0 0 auto;
136
143
  }
137
144
 
138
145
  .card-footnote {
139
146
  margin: 0;
140
147
  color: var(--color-neutral-900);
148
+ flex: 0 0 auto;
141
149
  }
142
150
 
143
151
 
144
152
  /* ──────────────────────────────────────────────────────────────
145
- 4. compare-bar (10px radius Figma → --radius-xl 12px)
153
+ 4. compare-bar
146
154
  ──────────────────────────────────────────────────────────────── */
147
155
 
148
- :host(compare-bar) {
149
- width: 100%;
150
- }
151
-
152
156
  .bar-track {
153
157
  position: relative;
154
158
  height: var(--cs-bar-height);
@@ -172,7 +176,7 @@ export default css`
172
176
  box-sizing: border-box;
173
177
  background: transparent;
174
178
  transition: width calc(var(--transition-duration-slower) + 300ms)
175
- cubic-bezier(0.22, 1, 0.36, 1);
179
+ cubic-bezier(0.22, 1, 0.36, 1);
176
180
  min-width: 0;
177
181
  }
178
182
 
@@ -239,29 +243,28 @@ export default css`
239
243
  ══════════════════════════════════════════════════════════════ */
240
244
 
241
245
  :host(bd-compare-section.theme-green) {
242
- --cs-bar-primary-bg: var(--color-green-500);
246
+ --cs-bar-primary-bg: var(--color-green-500);
243
247
  --cs-bar-primary-fg: var(--color-neutral-0);
244
248
  --cs-icon-color: var(--color-green-500);
245
249
  --cs-icon-filter: invert(35%) sepia(72%) saturate(500%) hue-rotate(100deg) brightness(95%) contrast(95%);
246
250
  }
247
251
 
248
252
  :host(bd-compare-section.theme-red) {
249
- --cs-bar-primary-bg: var(--color-red-500);
253
+ --cs-bar-primary-bg: var(--color-red-500);
250
254
  --cs-bar-primary-fg: var(--color-neutral-0);
251
255
  --cs-icon-color: var(--color-red-500);
252
256
  --cs-icon-filter: invert(22%) sepia(90%) saturate(700%) hue-rotate(345deg) brightness(95%) contrast(95%);
253
257
  }
254
258
 
255
- /* No violet in DS; keep marketing accent */
256
259
  :host(bd-compare-section.theme-purple) {
257
- --cs-bar-primary-bg: #7c3aed;
260
+ --cs-bar-primary-bg: #7c3aed;
258
261
  --cs-bar-primary-fg: var(--color-neutral-0);
259
262
  --cs-icon-color: #7c3aed;
260
263
  --cs-icon-filter: invert(25%) sepia(80%) saturate(800%) hue-rotate(255deg) brightness(90%) contrast(95%);
261
264
  }
262
265
 
263
266
  :host(bd-compare-section.theme-dark) {
264
- --cs-bar-primary-bg: var(--color-blue-400);
267
+ --cs-bar-primary-bg: var(--color-blue-400);
265
268
  --cs-bar-primary-fg: var(--color-neutral-0);
266
269
  --cs-icon-color: var(--color-blue-400);
267
270
  --cs-icon-filter: invert(65%) sepia(50%) saturate(500%) hue-rotate(195deg) brightness(105%) contrast(95%);
@@ -280,4 +283,4 @@ export default css`
280
283
  --cs-bar-height: var(--dimension-40px);
281
284
  --cs-gap: var(--spacing-24);
282
285
  }
283
- `;
286
+ `;
@@ -101,7 +101,7 @@ export const Default = {
101
101
  >
102
102
  <compare-card
103
103
  title="Best Protection"
104
- description="Best Protection against e-Threats (On a scale of 0 to 6, with 6 as the highest level of protection)"
104
+ description="Best Protection against e-Threats (On a scale of 0 to 6, with 6 as the highest level of protection)Best Protection against e-Threats (On a scale of 0 to 6, with 6 as the highest level of protection)Best Protection against e-Threats (On a scale of 0 to 6, with 6 as the highest level of protection)"
105
105
  footnote="Overall Score. July 2011 – February 2025. Source AV TEST.org"
106
106
  footnote-href="${AV_TEST_URL}"
107
107
  icon-src="${SHIELD_ICON}"