@repobit/dex-system-design 0.24.18 → 0.24.20

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 (29) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/package.json +4 -4
  3. package/src/components/ai-guardian-demo.stories.js +454 -33
  4. package/src/components/bento-grid/bento-grid.css.js +12 -1
  5. package/src/components/bento-grid/bento-grid.js +59 -1
  6. package/src/components/bento-grid/bento-grid.stories.js +45 -0
  7. package/src/components/cards/card.css.js +56 -0
  8. package/src/components/cards/card.js +64 -11
  9. package/src/components/cards/card.stories.js +65 -2
  10. package/src/components/comparison-panel/comparison-panel.css.js +52 -1
  11. package/src/components/comparison-panel/comparison-panel.js +6 -1
  12. package/src/components/compatibility-table/compatibility-table.css.js +29 -13
  13. package/src/components/compatibility-table/compatibility-table.js +13 -7
  14. package/src/components/cta-banner/cta-banner.css.js +11 -0
  15. package/src/components/cta-banner/cta-banner.js +7 -0
  16. package/src/components/footer/footer-lp.css.js +10 -21
  17. package/src/components/footer/footer-lp.js +3 -18
  18. package/src/components/footer/footer-nav-menu.css.js +0 -2
  19. package/src/components/footer/footer.css.js +18 -35
  20. package/src/components/footer/footer.js +5 -1
  21. package/src/components/header/header-code.css.js +208 -10
  22. package/src/components/header/header-code.js +148 -5
  23. package/src/components/header/header-code.stories.js +83 -27
  24. package/src/components/page-switcher/page-switcher.js +1 -1
  25. package/src/components/paragraph/paragraph.js +6 -0
  26. package/src/components/quick-answer/quick-answer.css.js +2 -0
  27. package/src/components/quick-answer/quick-answer.js +1 -1
  28. package/src/components/terminal/terminal-message.css.js +153 -13
  29. package/src/components/terminal/terminal-message.js +37 -11
@@ -29,6 +29,29 @@ function resolveBreakpoint(width, maxColumns, minCardWidth) {
29
29
  return { bp, columns, gap };
30
30
  }
31
31
 
32
+ // Same breakpoint logic as resolveBreakpoint, but allows more than 2
33
+ // columns (up to maxColumns) since the "equal" variant is a plain grid
34
+ // with no wide/narrow pairing to worry about.
35
+ function resolveEqualColumns(width, maxColumns, minCardWidth) {
36
+ const bp =
37
+ width >= BREAKPOINT_WIDTHS.xl
38
+ ? "xl" :
39
+ width >= BREAKPOINT_WIDTHS.lg
40
+ ? "lg" :
41
+ width >= BREAKPOINT_WIDTHS.md ? "md" : "sm";
42
+
43
+ let columns = bp === "sm" ? 1 : bp === "md" ? Math.min(2, maxColumns) : Math.max(1, maxColumns);
44
+ const gap = DEFAULT_GAP[bp];
45
+
46
+ while (columns > 1) {
47
+ const cardWidth = (width - gap * (columns - 1)) / columns;
48
+ if (cardWidth >= minCardWidth) break;
49
+ columns -= 1;
50
+ }
51
+
52
+ return { bp, columns, gap };
53
+ }
54
+
32
55
  class BentoGrid extends LitElement {
33
56
  static properties = {
34
57
  eyebrow : { type: String },
@@ -37,7 +60,20 @@ class BentoGrid extends LitElement {
37
60
  maxColumns : { type: Number, attribute: "max-columns" },
38
61
  minCardWidth: { type: Number, attribute: "min-card-width" },
39
62
  // Fixed wide/narrow ratio applied to every pair, e.g. 0.7 = 70/30.
63
+ // Only used when variant="weighted" (the default).
40
64
  wideShare : { type: Number, attribute: "wide-share" },
65
+ // "weighted" (default) — pairs of cards alternate wide/narrow, per
66
+ // _applyWeights below. "equal" — every card gets the same width, laid
67
+ // out in a plain N-column grid (see equalColumns) with no pairing.
68
+ variant : { type: String, reflect: true },
69
+ // Column count for variant="equal" at the widest breakpoint (xl/lg);
70
+ // narrows to min(2, this) at md, and 1 at sm, same responsive pattern
71
+ // as the weighted variant. Ignored when variant="weighted".
72
+ equalColumns: { type: Number, attribute: "equal-columns" },
73
+ // "none" (default, transparent) | "tint" (light blue wash behind the
74
+ // whole grid, including header text) — a section-level background, not
75
+ // per-card (bd-bento-item already has its own `tone="tint"` for that).
76
+ background : { type: String, reflect: true },
41
77
  _columns : { state: true },
42
78
  _gap : { state: true }
43
79
  };
@@ -52,6 +88,9 @@ class BentoGrid extends LitElement {
52
88
  this.maxColumns = 2;
53
89
  this.minCardWidth = 260;
54
90
  this.wideShare = 0.7;
91
+ this.variant = "weighted";
92
+ this.equalColumns = 3;
93
+ this.background = "none";
55
94
  this._columns = 2;
56
95
  this._gap = 24;
57
96
  this._resizeObserver = null;
@@ -124,13 +163,32 @@ class BentoGrid extends LitElement {
124
163
  }
125
164
  }
126
165
 
166
+ // Every item gets the same span (TOTAL_UNITS / columns) — no pairing, no
167
+ // "featured" full-width exception. Rounds if TOTAL_UNITS doesn't divide
168
+ // evenly by columns; for common column counts (1, 2, 3, 4, 6, 8, 12, 24)
169
+ // against TOTAL_UNITS=24 this is always exact.
170
+ _applyEqualSpans(columns) {
171
+ const span = Math.round(TOTAL_UNITS / Math.max(1, columns));
172
+ this._items.forEach((item) => item.style.setProperty("--bd-bento-span", String(span)));
173
+ }
174
+
127
175
  _relayout() {
128
176
  const width = this.clientWidth || 960;
177
+ this.style.setProperty("--bd-bento-total-units", String(TOTAL_UNITS));
178
+
179
+ if (this.variant === "equal") {
180
+ const { columns, gap } = resolveEqualColumns(width, this.equalColumns, this.minCardWidth);
181
+ this._columns = columns;
182
+ this._gap = gap;
183
+ this.style.setProperty("--bd-bento-gap", `${gap}px`);
184
+ this._applyEqualSpans(columns);
185
+ return;
186
+ }
187
+
129
188
  const { columns, gap } = resolveBreakpoint(width, this.maxColumns, this.minCardWidth);
130
189
  this._columns = columns;
131
190
  this._gap = gap;
132
191
  this.style.setProperty("--bd-bento-gap", `${gap}px`);
133
- this.style.setProperty("--bd-bento-total-units", String(TOTAL_UNITS));
134
192
 
135
193
  if (columns <= 1) {
136
194
  this._items.forEach((item) => item.style.setProperty("--bd-bento-span", String(TOTAL_UNITS)));
@@ -437,4 +437,49 @@ export const TwentySevenItems = {
437
437
  }
438
438
  }
439
439
  }
440
+ };
441
+ export const EqualColumns = {
442
+ name : "Equal Columns — Protection",
443
+ render: () => html`
444
+ <bd-bento-grid
445
+ variant="equal"
446
+ equal-columns="3"
447
+ eyebrow="Protection"
448
+ title="What does VPN for AI Agents protect against?"
449
+ description="Each protection is structural: every request the agent routes through Bitdefender's MCP tools gets it automatically, with no setting to remember."
450
+ >
451
+ <bd-bento-item>
452
+ <span slot="icon" style="color: var(--color-blue-500, #006DFF); font-weight: 700; font-family: var(--typography-fontFamily-mono, monospace); font-size: 14px; letter-spacing: 0.05em;">01</span>
453
+ <bd-h as="h5" font-weight="700" style="margin: 0 0 8px;">IP masking</bd-h>
454
+ <bd-p color="var(--color-neutral-600)">When your agent reaches a site, it sees the VPN server's exit IP. Not your home address or IP-based location.</bd-p>
455
+ </bd-bento-item>
456
+ <bd-bento-item>
457
+ <span slot="icon" style="color: var(--color-blue-500, #006DFF); font-weight: 700; font-family: var(--typography-fontFamily-mono, monospace); font-size: 14px; letter-spacing: 0.05em;">02</span>
458
+ <bd-h as="h5" font-weight="700" style="margin: 0 0 8px;">Per-prompt encryption</bd-h>
459
+ <bd-p color="var(--color-neutral-600)">Every request opens its own encrypted tunnel from your device to Bitdefender, so whoever runs the Wi-Fi you're on can't read what the agent sends.</bd-p>
460
+ </bd-bento-item>
461
+ <bd-bento-item>
462
+ <span slot="icon" style="color: var(--color-blue-500, #006DFF); font-weight: 700; font-family: var(--typography-fontFamily-mono, monospace); font-size: 14px; letter-spacing: 0.05em;">03</span>
463
+ <bd-h as="h5" font-weight="700" style="margin: 0 0 8px;">Session isolation</bd-h>
464
+ <bd-p color="var(--color-neutral-600)">Each tunnel closes the moment the prompt ends. If a site sees two of your agent's requests, it has no network-level way to tell they came from the same prompt.</bd-p>
465
+ </bd-bento-item>
466
+ <bd-bento-item>
467
+ <span slot="icon" style="color: var(--color-blue-500, #006DFF); font-weight: 700; font-family: var(--typography-fontFamily-mono, monospace); font-size: 14px; letter-spacing: 0.05em;">04</span>
468
+ <bd-h as="h5" font-weight="700" style="margin: 0 0 8px;">Clean-IP browsing</bd-h>
469
+ <bd-p color="var(--color-neutral-600)">Every request exits on the VPN server's IP rather than the same home address every time, useful for QA and monitoring work that needs a neutral, non-residential vantage point.</bd-p>
470
+ </bd-bento-item>
471
+ <bd-bento-item>
472
+ <span slot="icon" style="color: var(--color-blue-500, #006DFF); font-weight: 700; font-family: var(--typography-fontFamily-mono, monospace); font-size: 14px; letter-spacing: 0.05em;">05</span>
473
+ <bd-h as="h5" font-weight="700" style="margin: 0 0 8px;">Regional routing</bd-h>
474
+ <bd-p color="var(--color-neutral-600)">Need to see a page the way a visitor from a specific country would? Route that one request through a Bitdefender server there, just for that prompt.</bd-p>
475
+ </bd-bento-item>
476
+ </bd-bento-grid>
477
+ `,
478
+ parameters: {
479
+ docs: {
480
+ description: {
481
+ story: "`variant=\"equal\"` gives every card the same width in a plain N-column grid (`equal-columns`), with no wide/narrow pairing — unlike the default `\"weighted\"` variant."
482
+ }
483
+ }
484
+ }
440
485
  };
@@ -50,6 +50,12 @@ export default css`
50
50
  gap: var(--spacing-8);
51
51
  }
52
52
 
53
+ /* Outline-variant sections get extra top padding above the header,
54
+ per the reference design. */
55
+ :host([variant="outline"]) .bd-header-light-carousel-s {
56
+ padding-top: 80px;
57
+ }
58
+
53
59
  .bd-light-carousel-s {
54
60
  display: flex;
55
61
  align-items: center;
@@ -95,6 +101,56 @@ export default css`
95
101
  background: #E4F2FF !important;
96
102
  }
97
103
 
104
+ /* === OUTLINE VARIANT — bordered, tinted, fully centered card. Set
105
+ variant="outline" on bd-card-section (propagates to items) or on an
106
+ individual bd-card-item to opt it in/out per-card. === */
107
+ .bd-light-box-outline {
108
+ background-color: var(--color-neutral-25);
109
+ border: 2px solid var(--color-blue-500);
110
+ border-radius: var(--radius-2xl, 1rem);
111
+ padding: 40px;
112
+ align-items: center !important;
113
+ justify-content: center !important;
114
+ text-align: center !important;
115
+ }
116
+
117
+ .bd-light-box-outline .bd-light-box-header-s {
118
+ align-items: center;
119
+ justify-content: center;
120
+ width: 100%;
121
+ }
122
+
123
+ .bd-light-box-outline .bd-light-title-badge-wrapper-s {
124
+ justify-content: center;
125
+ }
126
+
127
+ .bd-light-box-outline .bd-card-title-group {
128
+ align-items: center;
129
+ text-align: center;
130
+ margin-top: 60px;
131
+ }
132
+
133
+ .bd-light-box-outline .bd-card-title-group bd-h {
134
+ color: var(--color-blue-500, #006dff);
135
+ text-align: center;
136
+ }
137
+
138
+ .bd-light-box-outline .bd-card-item-subtitle,
139
+ .bd-light-box-outline .bd-card-content-wrapper p,
140
+ .bd-light-box-outline .bd-card-content-wrapper ::slotted(*) {
141
+ color: var(--color-neutral-700);
142
+ text-align: center;
143
+ }
144
+
145
+ .bd-light-box-outline .bd-card-content-wrapper {
146
+ align-items: center;
147
+ text-align: center;
148
+ }
149
+
150
+ .bd-light-box-outline .bd-card-cta-wrapper {
151
+ justify-content: center;
152
+ }
153
+
98
154
  .bd-light-box-center {
99
155
  align-items: center !important;
100
156
  justify-content: center !important;
@@ -9,6 +9,13 @@ class Card extends LitElement {
9
9
  title : { type: String },
10
10
  subtitle : { type: String },
11
11
  bgBlue : { type: Boolean, reflect: true, attribute: "bg-blue" },
12
+ // "" (default — existing grey-box carousel look) | "outline" — bordered,
13
+ // centered cards with a light-blue tint, blue title and centered icon,
14
+ // matching the reference design. Propagated automatically to every
15
+ // slotted bd-card-item (see _onSlotChange) so callers only set it once,
16
+ // on the section, unless a specific item needs to opt out by setting
17
+ // its own variant attribute explicitly.
18
+ variant : { type: String, reflect: true },
12
19
  _itemCount: { state: true }
13
20
  };
14
21
 
@@ -17,6 +24,7 @@ class Card extends LitElement {
17
24
  this.title = "";
18
25
  this.subtitle = "";
19
26
  this.bgBlue = false;
27
+ this.variant = "";
20
28
  this._itemCount = 0;
21
29
  this._resizeObserver = null;
22
30
  }
@@ -47,8 +55,26 @@ class Card extends LitElement {
47
55
  this.style.setProperty("--bd-card-width", `${cardW}px`);
48
56
  }
49
57
 
58
+ // Propagates this.variant onto every slotted bd-card-item that didn't
59
+ // itself declare a variant attribute in its ORIGINAL markup. We can't use
60
+ // item.hasAttribute("variant") here — CardItem's own `variant` property
61
+ // has reflect:true, so Lit reflects its default "" onto the attribute on
62
+ // first render, meaning hasAttribute("variant") becomes true for EVERY
63
+ // item almost immediately, even ones the author never touched. Instead
64
+ // we rely on item._authorSetVariant, captured in CardItem's constructor
65
+ // (before Lit's own reflection has a chance to run) — see below.
66
+ _propagateVariant() {
67
+ if (!this.variant) return;
68
+ this.querySelectorAll("bd-card-item").forEach((item) => {
69
+ if (!item._authorSetVariant) {
70
+ item.variant = this.variant;
71
+ }
72
+ });
73
+ }
74
+
50
75
  _onSlotChange = () => {
51
76
  this._itemCount = this.querySelectorAll("bd-card-item").length;
77
+ this._propagateVariant();
52
78
  this._updateCardWidth();
53
79
  this.requestUpdate();
54
80
  this._equalizeHeights();
@@ -160,6 +186,7 @@ class Card extends LitElement {
160
186
  }
161
187
 
162
188
  await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)));
189
+ this._propagateVariant();
163
190
  this._updateCardWidth();
164
191
  this._equalizeHeights();
165
192
 
@@ -208,38 +235,61 @@ class CardItem extends LitElement {
208
235
  icon : { type: String },
209
236
  align : { type: String },
210
237
  bgBlue : { type: Boolean, reflect: true, attribute: "bg-blue" },
211
- ctaAlign: { type: String, attribute: "cta-align" }
238
+ ctaAlign: { type: String, attribute: "cta-align" },
239
+ // "" (default) | "outline" — see Card.variant above. Reflected so
240
+ // card.css.js can target it with :host([variant="outline"]).
241
+ variant : { type: String, reflect: true }
212
242
  };
213
243
 
214
244
  constructor() {
215
245
  super();
246
+ // Captured BEFORE Lit's own property-to-attribute reflection runs (that
247
+ // happens on first update/render, not here). At this point the DOM
248
+ // parser has already set any attribute the author wrote in markup, so
249
+ // this reliably answers "did the author explicitly set variant on this
250
+ // element", independent of whatever default value we assign below.
251
+ this._authorSetVariant = this.hasAttribute("variant");
252
+
216
253
  this.title = "";
217
254
  this.subtitle = "";
218
255
  this.icon = "";
219
256
  this.align = "start";
220
257
  this.bgBlue = false;
221
258
  this.ctaAlign = "start";
259
+ this.variant = "";
222
260
  }
223
261
 
224
262
  _renderIcon() {
225
263
  const iconEl = this.querySelector("[slot='icon']");
264
+ const isOutline = this.variant === "outline";
265
+ const iconW = isOutline ? 65 : 48;
266
+ const iconH = isOutline ? 75 : 48;
226
267
 
227
268
  if (iconEl) {
228
269
  const clone = iconEl.cloneNode(true);
229
270
 
230
- clone.style.maxWidth = "48px";
231
- clone.style.maxHeight = "48px";
232
- clone.style.width = "48px";
233
- clone.style.height = "48px";
271
+ // The icon components (bd-globe-icon etc.) size their inner <svg>
272
+ // from their own width/height attributes, not from CSS on the host —
273
+ // setting only clone.style.width/height left the SVG's actual
274
+ // width="48" height="48" attributes untouched, so nothing visibly
275
+ // changed. Overwrite the attributes themselves for anything that
276
+ // looks like an icon component (custom element) or a raw <svg>.
277
+ clone.setAttribute("width", iconW);
278
+ clone.setAttribute("height", iconH);
279
+
280
+ clone.style.maxWidth = `${iconW}px`;
281
+ clone.style.maxHeight = `${iconH}px`;
282
+ clone.style.width = `${iconW}px`;
283
+ clone.style.height = `${iconH}px`;
234
284
  clone.style.objectFit = "contain";
235
285
  clone.style.display = "block";
236
286
 
237
287
  const innerImg = clone.querySelector("img");
238
288
  if (innerImg) {
239
- innerImg.style.maxWidth = "48px";
240
- innerImg.style.maxHeight = "48px";
241
- innerImg.style.width = "48px";
242
- innerImg.style.height = "48px";
289
+ innerImg.style.maxWidth = `${iconW}px`;
290
+ innerImg.style.maxHeight = `${iconH}px`;
291
+ innerImg.style.width = `${iconW}px`;
292
+ innerImg.style.height = `${iconH}px`;
243
293
  innerImg.style.objectFit = "contain";
244
294
  innerImg.style.display = "block";
245
295
  innerImg.removeAttribute("width");
@@ -254,6 +304,7 @@ class CardItem extends LitElement {
254
304
  src="${this.icon}"
255
305
  alt="icon"
256
306
  class="bd-light-icon-s"
307
+ style="width: ${iconW}px; height: ${iconH}px;"
257
308
  />`;
258
309
  }
259
310
 
@@ -272,7 +323,8 @@ class CardItem extends LitElement {
272
323
  }
273
324
 
274
325
  render() {
275
- const isCentered = this.align === "center";
326
+ const isOutline = this.variant === "outline";
327
+ const isCentered = this.align === "center" || isOutline;
276
328
  const hasContent = this._hasContent();
277
329
  const hasCtaSlot = !!this.querySelector("[slot='cta']");
278
330
  const ctaJustify = this.ctaAlign === "center"
@@ -285,7 +337,8 @@ class CardItem extends LitElement {
285
337
  <div
286
338
  class="bd-light-box-s
287
339
  ${isCentered ? "bd-light-box-center" : ""}
288
- ${this.bgBlue ? "bd-light-box-blue" : ""}"
340
+ ${this.bgBlue ? "bd-light-box-blue" : ""}
341
+ ${isOutline ? "bd-light-box-outline" : ""}"
289
342
  >
290
343
  <div class="bd-light-box-header-s ${isCentered ? "bd-light-box-header-center" : ""}">
291
344
  ${this._renderIcon()}
@@ -27,6 +27,13 @@ Grid columns are calculated automatically:
27
27
  | 7–8 | 4 | 4+3, 4+4 |
28
28
  | 9 | 3 | 3+3+3 |
29
29
  | 10+ | 4 | 4+4+... |
30
+
31
+ ### Variants
32
+ \`variant="outline"\` on \`bd-card-section\` switches every slotted \`bd-card-item\`
33
+ to a bordered, light-blue-tinted, centered card style (blue title, centered
34
+ icon/text) — set it once on the section, it propagates to all items
35
+ automatically. Leaving \`variant\` unset keeps the existing grey-box look.
36
+ An individual \`bd-card-item\` can opt out with \`variant=""\` explicitly.
30
37
  `
31
38
  }
32
39
  }
@@ -324,6 +331,54 @@ export const MobileView = {
324
331
  </bd-card-section>
325
332
  ` };
326
333
 
334
+ // ─── Outline Variant ────────────────────────────────────────────────────────
335
+
336
+ export const OutlineVariant = {
337
+ name : 'Outline Variant — Trusted For The AI Era',
338
+ render: () => html`
339
+ <bd-card-section
340
+ title="Trusted For The AI Era"
341
+ subtitle="Pioneering AI in Cybersecurity since 2008."
342
+ variant="outline"
343
+ >
344
+ <bd-card-item title="Transparency">
345
+ <bd-individual-icon slot="icon" width="48" height="48" color="#006DFF"></bd-individual-icon>
346
+ <bd-p>We provide clear information about how our AI makes security decisions and what data it uses, balancing technical transparency with usability.</bd-p>
347
+ </bd-card-item>
348
+ <bd-card-item title="Privacy by Design">
349
+ <bd-globe-icon slot="icon" width="48" height="48" color="#006DFF"></bd-globe-icon>
350
+ <bd-p>We implement privacy-preserving techniques like federated learning and differential privacy to minimize data exposure while maintaining AI effectiveness.</bd-p>
351
+ </bd-card-item>
352
+ <bd-card-item title="Human Guided AI Protection">
353
+ <bd-family-icon slot="icon" width="48" height="48" color="#006DFF"></bd-family-icon>
354
+ <bd-p>While our AI is powerful, we augment it with the deep expertise of Bitdefender Labs.</bd-p>
355
+ </bd-card-item>
356
+ </bd-card-section>
357
+ `,
358
+ parameters: { docs: { description: { story: '`variant="outline"` pe `bd-card-section` — se propagă automat la toate cardurile: contur albastru subțire, fundal albastru foarte deschis, titlu albastru, conținut centrat.' } } }
359
+ };
360
+
361
+ export const OutlineVariantMixed = {
362
+ name : 'Outline Variant — Mixed With Default',
363
+ render: () => html`
364
+ <bd-card-section title="Mixed Variants" variant="outline">
365
+ <bd-card-item title="Outline Card One">
366
+ <bd-individual-icon slot="icon" width="48" height="48" color="#006DFF"></bd-individual-icon>
367
+ <bd-p>This card inherits the outline variant from the section.</bd-p>
368
+ </bd-card-item>
369
+ <bd-card-item title="Opted-out Card" variant="">
370
+ <bd-globe-icon slot="icon" width="32" height="32" color="#006DFF"></bd-globe-icon>
371
+ <bd-p>This card explicitly sets variant="" to opt out and keep the default grey-box look.</bd-p>
372
+ </bd-card-item>
373
+ <bd-card-item title="Outline Card Two">
374
+ <bd-machine-learning-icon slot="icon" width="48" height="48" color="#006DFF"></bd-machine-learning-icon>
375
+ <bd-p>This card also inherits the outline variant.</bd-p>
376
+ </bd-card-item>
377
+ </bd-card-section>
378
+ `,
379
+ parameters: { docs: { description: { story: 'Un card poate seta `variant=""` explicit ca să rămână pe stilul default, chiar dacă secțiunea are `variant="outline"`.' } } }
380
+ };
381
+
327
382
  // ─── Playground ───────────────────────────────────────────────────────────────
328
383
 
329
384
  export const Playground = {
@@ -331,13 +386,21 @@ export const Playground = {
331
386
  args: {
332
387
  sectionTitle : 'Why Bitdefender',
333
388
  sectionSubtitle: '',
389
+ sectionVariant : '',
334
390
  itemTitle : 'AI Protection',
335
391
  align : 'start',
336
392
  bgBlue : false,
337
393
  ctaAlign : 'start'
338
394
  },
395
+ argTypes: {
396
+ sectionVariant: {
397
+ control : { type: 'select' },
398
+ options : ['', 'outline'],
399
+ description: 'bd-card-section variant — propagates to all items'
400
+ }
401
+ },
339
402
  render: (args) => html`
340
- <bd-card-section title="${args.sectionTitle}" subtitle="${args.sectionSubtitle}">
403
+ <bd-card-section title="${args.sectionTitle}" subtitle="${args.sectionSubtitle}" variant="${args.sectionVariant}">
341
404
  <bd-card-item
342
405
  title="${args.itemTitle}"
343
406
  align="${args.align}"
@@ -360,5 +423,5 @@ export const Playground = {
360
423
  </bd-card-item>
361
424
  </bd-card-section>
362
425
  `,
363
- parameters: { docs: { description: { story: 'Playground interactiv.' } } }
426
+ parameters: { docs: { description: { story: 'Playground interactiv — folosește `sectionVariant` din Controls ca să testezi `variant="outline"`.' } } }
364
427
  };
@@ -4,7 +4,50 @@ export default css`
4
4
  :host {
5
5
  display: block;
6
6
  font-family: var(--typography-fontFamily-sans);
7
- background: var(--color-blue-50);
7
+ background: var(--color-blue-50);
8
+ }
9
+
10
+ /* === DARK VARIANT === */
11
+ :host([variant="dark"]) {
12
+ background: linear-gradient(160deg, var(--color-neutral-800) 0%, #0b1626 100%);
13
+ }
14
+
15
+ :host([variant="dark"]) .bd-comparisonpanel-title {
16
+ color: var(--color-neutral-0, #fff);
17
+ }
18
+
19
+ :host([variant="dark"]) .bd-comparisonpanel-description {
20
+ color: rgba(255, 255, 255, 0.55);
21
+ }
22
+
23
+ :host([variant="dark"]) .bd-comparisonpanel-eyebrow {
24
+ color: var(--color-blue-400, #60a5fa);
25
+ }
26
+
27
+ :host([variant="dark"]) .bd-comparisonpanel-column {
28
+ background: rgba(255, 255, 255, 0.03);
29
+ border-color: rgba(255, 255, 255, 0.1);
30
+ }
31
+
32
+ :host([variant="dark"]) .bd-comparisonpanel-column-title {
33
+ color: rgba(255, 255, 255, 0.5);
34
+ font-family: var(--typography-fontFamily-mono, monospace);
35
+ text-transform: uppercase;
36
+ letter-spacing: 0.08em;
37
+ font-weight: var(--typography-fontWeight-bold, 700);
38
+ font-size: var(--typography-fontSize-xs, 12px);
39
+ }
40
+
41
+ :host([variant="dark"]) .bd-comparisonpanel-text {
42
+ color: rgba(255, 255, 255, 0.85);
43
+ }
44
+
45
+ :host([variant="dark"]) .bd-comparisonpanel-icon--positive {
46
+ background: var(--color-green-600, #16a34a);
47
+ }
48
+
49
+ :host([variant="dark"]) .bd-comparisonpanel-icon--negative {
50
+ background: var(--color-red-500, #ef4444);
8
51
  }
9
52
 
10
53
  .bd-comparisonpanel-wrapper {
@@ -14,6 +57,14 @@ export default css`
14
57
  box-sizing: border-box;
15
58
  }
16
59
 
60
+ /* Small eyebrow label above the title, e.g. "Clarity" — optional, only
61
+ rendered when the eyebrow prop/slot is used. */
62
+ .bd-comparisonpanel-eyebrow {
63
+ margin: 0 0 var(--spacing-8);
64
+ color: var(--color-blue-500);
65
+ font-weight: var(--typography-fontWeight-bold, 700);
66
+ }
67
+
17
68
  .bd-comparisonpanel-title {
18
69
  margin: 0 0 var(--spacing-16);
19
70
  color: var(--color-neutral-900);
@@ -11,7 +11,11 @@ class ComparisonPanel extends LitElement {
11
11
  positiveTitle: { type: String, attribute: "positive-title" },
12
12
  negativeTitle: { type: String, attribute: "negative-title" },
13
13
  positiveItems: { type: Array }, // [String]
14
- negativeItems: { type: Array } // [String]
14
+ negativeItems: { type: Array }, // [String]
15
+ // "light" (default) — white section, bordered white cards, sans-serif
16
+ // column titles. "dark" — near-black gradient section, dark bordered
17
+ // cards, mono uppercase column titles (e.g. "VPN FOR AI AGENTS IS").
18
+ variant : { type: String, reflect: true }
15
19
  };
16
20
 
17
21
  static styles = [tokens, comparisonPanelCSS];
@@ -24,6 +28,7 @@ class ComparisonPanel extends LitElement {
24
28
  this.negativeTitle = "";
25
29
  this.positiveItems = [];
26
30
  this.negativeItems = [];
31
+ this.variant = "light";
27
32
  }
28
33
 
29
34
  _renderColumn(kind, colTitle, items, slotName) {
@@ -9,7 +9,27 @@ export default css`
9
9
  :host([background="blue"]) {
10
10
  background: var(--color-blue-50);
11
11
  }
12
+ /* variant="light" + background="bordered": renders its own card border
13
+ and hover lift, so callers don't need a manual wrapper <div> just to
14
+ get the bordered-card look (e.g. side-by-side requirement cards). */
15
+ :host([variant="light"][background="bordered"]) {
16
+ display: block;
17
+ border: 1px solid var(--color-neutral-100, #e5e7eb);
18
+ border-radius: var(--radius-xl, 12px);
19
+ padding: var(--spacing-24, 24px);
20
+ box-sizing: border-box;
21
+ transition: transform 0.25s ease, box-shadow 0.25s ease, border-color 0.25s ease;
22
+ }
23
+
24
+ :host([variant="light"][background="bordered"]:hover) {
25
+ transform: translateY(-3px);
26
+ box-shadow: 0 14px 40px #00000012;
27
+ border-color: var(--color-neutral-200);
28
+ }
12
29
 
30
+ :host([variant="light"][background="bordered"]) .bd-compattable-wrapper {
31
+ padding: 0;
32
+ }
13
33
  .bd-compattable-wrapper {
14
34
  max-width: var(--layout-ensemble-max-width, 1200px);
15
35
  margin: 0 auto;
@@ -31,7 +51,6 @@ export default css`
31
51
  max-width: 72ch;
32
52
  }
33
53
 
34
- /* === TABLE CONTAINER (default / outcome / risk) === */
35
54
  .bd-compattable-table-container {
36
55
  display: table;
37
56
  width: 100%;
@@ -42,7 +61,6 @@ export default css`
42
61
  box-sizing: border-box;
43
62
  }
44
63
 
45
- /* === HEADER ROW === */
46
64
  .bd-compattable-header {
47
65
  display: grid;
48
66
  grid-template-columns: 1.4fr 1.2fr 2.4fr;
@@ -74,7 +92,6 @@ export default css`
74
92
  color: var(--color-neutral-600);
75
93
  }
76
94
 
77
- /* === BODY === */
78
95
  .bd-compattable-body {
79
96
  display: flex;
80
97
  flex-direction: column;
@@ -141,15 +158,15 @@ export default css`
141
158
  .bd-compattable-cell--riskcoverage {
142
159
  color: var(--color-neutral-900);
143
160
  }
144
-
145
- /* === LIGHT VARIANT === */
146
- .bd-compattable-lightgrid {
147
- display: grid;
148
- grid-template-columns: 1fr 1fr;
149
- column-gap: var(--spacing-48);
150
- row-gap: 0;
151
- }
152
-
161
+ .bd-compattable-lightgrid {
162
+ display: grid;
163
+ grid-template-columns: repeat(var(--bd-compattable-light-columns, 2), 1fr);
164
+ column-gap: var(--spacing-48);
165
+ row-gap: 0;
166
+ }
167
+ .bd-compattable-lightrow:last-of-type {
168
+ border-bottom: none;
169
+ }
153
170
  .bd-compattable-lightrow {
154
171
  display: flex;
155
172
  align-items: baseline;
@@ -171,7 +188,6 @@ export default css`
171
188
  text-align: right;
172
189
  }
173
190
 
174
- /* === RESPONSIVE === */
175
191
  @media (max-width: 767px) {
176
192
  .bd-compattable-header {
177
193
  display: none;