@bytebrand/fe-ui-core-autobahn 1.0.43 → 1.0.44

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.
@@ -488,10 +488,10 @@
488
488
  "immediately": "Sofort verfügbar"
489
489
  },
490
490
  "tier": {
491
- "top": "Top Preis",
491
+ "top": "Top-Preis",
492
492
  "good": "Guter Preis",
493
- "fair": "Fairer Preis",
494
- "raised": "Erhöht",
493
+ "fair": "Fairer-Preis",
494
+ "raised": "Erhöhter Preis",
495
495
  "high": "Hoher Preis"
496
496
  },
497
497
  "sort": {
@@ -528,13 +528,13 @@
528
528
  }
529
529
  },
530
530
  "eco": {
531
- "heading": "Umwelt & Verbrauch",
531
+ "heading": "Umwelt & Verbrauch*",
532
532
  "stat": {
533
533
  "powerConsumptionWeighted": "Stromverbrauch (gew.)",
534
534
  "fuelConsumptionWeighted": "Kraftstoff (gew.)",
535
535
  "fuelConsumptionDischarged": "Kraftstoff (entladen)",
536
536
  "co2WeightedCombined": "CO₂ (gew., komb.)",
537
- "consumptionCombined": "Verbrauch komb. (WLTP)",
537
+ "consumptionCombined": "Verbrauch (WLTP-kombiniert)",
538
538
  "co2Emissions": "CO₂-Emissionen (WLTP)",
539
539
  "co2Class": "CO₂-Klasse (WLTP)"
540
540
  },
@@ -544,7 +544,8 @@
544
544
  "energyClassWltpSuffix": "Energieeffizienzklasse nach WLTP · Angaben gem. Pkw-EnVKV*",
545
545
  "energyClass": "Energieeffizienzklasse nach WLTP (kombiniert) · Angaben gem. Pkw-EnVKV*",
546
546
  "weightedShort": "gew. komb.",
547
- "dischargedShort": "entladen komb."
547
+ "dischargedShort": "entladen komb.",
548
+ "co2ClassScaleLabel": "CO₂-Klasse (WLTP-kombiniert)"
548
549
  },
549
550
  "description": {
550
551
  "heading": "Fahrzeugbeschreibung",
@@ -570,7 +571,7 @@
570
571
  },
571
572
  "marketValue": {
572
573
  "heading": "Marktwert-Einordnung",
573
- "belowAverage": "{{rating}} — {{amount}} € unter Marktdurchschnitt"
574
+ "belowAverage": "{{rating}} — {{amount}} € unter Marktwert"
574
575
  },
575
576
  "priceMarket": {
576
577
  "heading": "Preis & Marktwert",
@@ -680,11 +681,19 @@
680
681
  "ratePerMonth": "{{amount}} € /mtl.",
681
682
  "totalAmount": "Gesamtbetrag",
682
683
  "totalRepayment": "Gesamtrückzahlungsbetrag",
683
- "legalLeasing": "Unverbindliches Leasingbeispiel (Kilometerleasing). Bonität vorausgesetzt. Dieses freibleibende Angebot wird vermittelt durch die auto.de GmbH, Max-Planck-Str. 19, 06796 Sandersdorf-Brehna, die als ungebundener Vermittler nicht beratend tätig ist. Irrtümer vorbehalten. Preise ggf. inkl. MwSt.",
684
- "legalFinancing": "Repräsentatives Berechnungsbeispiel gem. § 17a Abs. 4 PAngV, das 2/3 aller Kunden — Bonität vorausgesetzt — erhalten. Für Ihre Finanzierungswünsche stellen wir eine Bonitätsanfrage. Dieses freibleibende Angebot der Santander Consumer Bank AG, Santander-Platz 1, 41061 Mönchengladbach, wird vermittelt durch die auto.de GmbH, Max-Planck-Str. 19, 06796 Sandersdorf-Brehna, die als ungebundener Vermittler nicht beratend tätig ist. Irrtümer vorbehalten. Preise ggf. inkl. MwSt.",
684
+ "legalLeasing": "Für Ihre Finanzierungswünsche stellen wir eine Bonitätsanfrage. Bonität vorausgesetzt, ist dies ein repräsentatives Berechnungsbeispiel gem. der Angaben, welches 2/3 aller Kunden, im Sinne des § 17a Abs. 4 PangV, erhalten. Dieses freibleibende Angebot der Santander Consumer Bank AG, Santander-Platz 1, 41061 Mönchengladbach wird vermittelt durch die auto.de GmbH, Max-Planck-Str. 19, 06796 Sandersdorf-Brehna, die als ungebundener Vermittler nicht beratend tätig ist. Irrtümer vorbehalten. Preise ggf. inkl. MwSt.",
685
+ "legalFinancing": "Für Ihre Finanzierungswünsche stellen wir eine Bonitätsanfrage. Bonität vorausgesetzt, ist dies ein repräsentatives Berechnungsbeispiel gem. der Angaben, welches 2/3 aller Kunden, im Sinne des § 17a Abs. 4 PangV, erhalten. Dieses freibleibende Angebot der Santander Consumer Bank AG, Santander-Platz 1, 41061 Mönchengladbach wird vermittelt durch die auto.de GmbH, Max-Planck-Str. 19, 06796 Sandersdorf-Brehna, die als ungebundener Vermittler nicht beratend tätig ist. Irrtümer vorbehalten. Preise ggf. inkl. MwSt.",
685
686
  "sheetTitleFinancing": "Kredit-Rate anpassen",
686
687
  "sheetTitleLeasing": "Leasing-Rate anpassen",
687
- "sheetTitleCash": "Kaufen - Details"
688
+ "sheetTitleCash": "Kaufen - Details",
689
+ "exampleVehiclePrice": "Fahrzeugpreis",
690
+ "examplePriceSuffix": "ggf. inkl. anderer Services",
691
+ "exampleApr": "Effektiver Jahreszins",
692
+ "exampleNominal": "Sollzinssatz p. a. (gebunden für die gesamte Vertragslaufzeit)",
693
+ "exampleNet": "Nettodarlehensbetrag",
694
+ "exampleDown": "Anzahlung",
695
+ "exampleFinal": "Schlussrate",
696
+ "exampleGross": "Gesamtrückzahlungsbetrag"
688
697
  },
689
698
  "ratings": {
690
699
  "showLess": "Weniger anzeigen",
@@ -491,7 +491,7 @@
491
491
  "top": "Top price",
492
492
  "good": "Good price",
493
493
  "fair": "Fair price",
494
- "raised": "Increased",
494
+ "raised": "Increased price",
495
495
  "high": "High price"
496
496
  },
497
497
  "sort": {
@@ -528,13 +528,13 @@
528
528
  }
529
529
  },
530
530
  "eco": {
531
- "heading": "Environment & consumption",
531
+ "heading": "Environment & consumption*",
532
532
  "stat": {
533
533
  "powerConsumptionWeighted": "Power consumption (weighted)",
534
534
  "fuelConsumptionWeighted": "Fuel (weighted)",
535
535
  "fuelConsumptionDischarged": "Fuel (discharged)",
536
536
  "co2WeightedCombined": "CO₂ (weighted, combined)",
537
- "consumptionCombined": "Consumption comb. (WLTP)",
537
+ "consumptionCombined": "Consumption (WLTP combined)",
538
538
  "co2Emissions": "CO₂ emissions (WLTP)",
539
539
  "co2Class": "CO₂ class (WLTP)"
540
540
  },
@@ -544,7 +544,8 @@
544
544
  "energyClassWltpSuffix": "Energy efficiency class per WLTP · figures pursuant to the German Pkw-EnVKV*",
545
545
  "energyClass": "Energy efficiency class per WLTP (combined) · figures pursuant to the German Pkw-EnVKV*",
546
546
  "weightedShort": "wtd. comb.",
547
- "dischargedShort": "depleted comb."
547
+ "dischargedShort": "depleted comb.",
548
+ "co2ClassScaleLabel": "CO₂ class (WLTP combined)"
548
549
  },
549
550
  "description": {
550
551
  "heading": "Vehicle description",
@@ -570,7 +571,7 @@
570
571
  },
571
572
  "marketValue": {
572
573
  "heading": "Market-value positioning",
573
- "belowAverage": "{{rating}} — {{amount}} € below the market average"
574
+ "belowAverage": "{{rating}} — {{amount}} € below market value"
574
575
  },
575
576
  "priceMarket": {
576
577
  "heading": "Price & market value",
@@ -680,11 +681,19 @@
680
681
  "ratePerMonth": "{{amount}} € /mo.",
681
682
  "totalAmount": "Total amount",
682
683
  "totalRepayment": "Total repayment amount",
683
- "legalLeasing": "Non-binding leasing example (mileage leasing). Subject to creditworthiness. This non-binding offer is brokered by auto.de GmbH, Max-Planck-Str. 19, 06796 Sandersdorf-Brehna, which acts as a non-tied intermediary and does not provide advice. Errors excepted. Prices incl. VAT where applicable.",
684
- "legalFinancing": "Representative calculation example pursuant to § 17a (4) PAngV, obtained by 2/3 of all customers — subject to creditworthiness. For your financing requests we will submit a credit inquiry. This non-binding offer from Santander Consumer Bank AG, Santander-Platz 1, 41061 Mönchengladbach, is brokered by auto.de GmbH, Max-Planck-Str. 19, 06796 Sandersdorf-Brehna, which acts as a non-tied intermediary and does not provide advice. Errors excepted. Prices incl. VAT where applicable.",
684
+ "legalLeasing": "We run a credit check for your financing request. Subject to creditworthiness, this is a representative example per § 17a (4) PangV received by 2/3 of all customers. This non-binding offer of Santander Consumer Bank AG, Santander-Platz 1, 41061 Mönchengladbach is brokered by auto.de GmbH, Max-Planck-Str. 19, 06796 Sandersdorf-Brehna, acting as an untied intermediary without advising. Errors excepted. Prices may incl. VAT.",
685
+ "legalFinancing": "We run a credit check for your financing request. Subject to creditworthiness, this is a representative example per § 17a (4) PangV received by 2/3 of all customers. This non-binding offer of Santander Consumer Bank AG, Santander-Platz 1, 41061 Mönchengladbach is brokered by auto.de GmbH, Max-Planck-Str. 19, 06796 Sandersdorf-Brehna, acting as an untied intermediary without advising. Errors excepted. Prices may incl. VAT.",
685
686
  "sheetTitleFinancing": "Adjust credit rate",
686
687
  "sheetTitleLeasing": "Adjust leasing rate",
687
- "sheetTitleCash": "Purchase - Details"
688
+ "sheetTitleCash": "Purchase - Details",
689
+ "exampleVehiclePrice": "Vehicle price",
690
+ "examplePriceSuffix": "possibly incl. other services",
691
+ "exampleApr": "Effective annual rate",
692
+ "exampleNominal": "Nominal rate p.a. (fixed for the entire contract term)",
693
+ "exampleNet": "Net loan amount",
694
+ "exampleDown": "Down payment",
695
+ "exampleFinal": "Final installment",
696
+ "exampleGross": "Total repayment amount"
688
697
  },
689
698
  "ratings": {
690
699
  "showLess": "Show less",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bytebrand/fe-ui-core-autobahn",
3
- "version": "1.0.43",
3
+ "version": "1.0.44",
4
4
  "description": "auto.de md3 (Autobahn) design system — UI primitives, cards, chrome, VDP modules, theme CSS and locales. Ships raw TypeScript source (like fe-ui-core before it); each consumer's bundler compiles it.",
5
5
  "main": "index.ts",
6
6
  "license": "UNLICENSED",
package/theme/md3.css CHANGED
@@ -1195,8 +1195,15 @@
1195
1195
  border-radius: var(--md-r-full); font-size: 14px; font-weight: 700; line-height: 1; white-space: nowrap; }
1196
1196
  .md3 .fin-eval-pill .lucide { flex: none; }
1197
1197
  /* gradient spectrum bar + circle thumb — replaces opacity-segment design */
1198
- .md3 .fin-eval-bar { position: relative; width: 100%; height: 9px; border-radius: 5px;
1199
- background: linear-gradient(to right, #0F7B43, #72C02C, #FFC107, #FF6B35, #C62828); }
1198
+ /* segmented evaluation bar (Figma round 12: discrete tier segments, not a gradient) —
1199
+ the gradient background below is dead once .fin-eval-seg children render on top */
1200
+ .md3 .fin-eval-seg { flex: 1; height: 100%; border-radius: 3px; }
1201
+ .md3 .fin-eval-bar { display: flex; gap: 3px; overflow: visible; }
1202
+ .md3 .slider-bubble { position: absolute; top: 0; transform: translateX(-50%); z-index: 1;
1203
+ padding: 4px 10px; border: 1px solid var(--md-outline-variant); border-radius: 8px;
1204
+ background: var(--md-sc-lowest); box-shadow: var(--md-e1); font-size: 13px; font-weight: 700;
1205
+ white-space: nowrap; }
1206
+ .md3 .fin-eval-bar { position: relative; width: 100%; height: 9px; border-radius: 5px; }
1200
1207
  .md3 .fin-eval-thumb { position: absolute; top: 50%; transform: translate(-50%, -50%);
1201
1208
  width: 17px; height: 17px; border-radius: 50%; background: #fff;
1202
1209
  border: 3px solid currentColor; box-shadow: 0 1px 4px rgba(0,0,0,.3); pointer-events: none; }
@@ -1630,6 +1637,32 @@
1630
1637
  .md3 .bb-cta-row .mbtn--outlined:not(.bb-adjust) { box-shadow: inset 0 0 0 1.5px var(--md-primary); }
1631
1638
  /* mobile page is WHITE like the Figma master — beats the .vdp-tint inline gray */
1632
1639
  .md3 .vdp-tint { background: var(--md-sc-lowest) !important; }
1640
+ /* ---- round 12 (9-screenshot review) ---- */
1641
+ /* Laufzeit chips = one-row segmented control (Figma), unit suffix hidden */
1642
+ .md3 .fin-sheet-terms .term-chips { display: flex; flex-wrap: nowrap; gap: 0;
1643
+ border: 1px solid var(--fig-border); border-radius: 10px; overflow: hidden; }
1644
+ .md3 .fin-sheet-terms .term-chip { flex: 1 1 0; min-width: 0; height: 46px; border: 0;
1645
+ border-left: 1px solid var(--fig-border); border-radius: 0; padding: 0; font-size: 14px; }
1646
+ .md3 .fin-sheet-terms .term-chip:first-child { border-left: 0; }
1647
+ .md3 .fin-sheet-terms .term-chip .tc-unit { display: none; }
1648
+ /* sheet: no divider under the header; one under the rate row instead (Figma) */
1649
+ .md3 .fin-modal-head { border-bottom: 0; padding-bottom: 8px; }
1650
+ .md3 .fin-sheet-adjust { border-top: 1px solid var(--fig-tile-border); padding-top: 16px; }
1651
+ /* sheet cash rows (Zahlung/Garantie/Lieferung) — Figma sizes */
1652
+ .md3 .fin-modal .calcrow { padding: 13px 0; }
1653
+ .md3 .fin-modal .calcrow-label { font-size: 14px; color: var(--fig-gray); }
1654
+ .md3 .fin-modal .calcrow-value { font-size: 15px !important; color: var(--fig-navy); font-weight: 700 !important; }
1655
+ /* condition tiles: Figma value size + roomier gap to the pills above */
1656
+ .md3 .zustand-tile .md-title-sm { font-size: 16.5px; }
1657
+ .md3 .vdp-header-extras .zustand-grid { margin-top: 10px; }
1658
+ /* enquiry: aside without background (Figma), blue-bordered active contact chip,
1659
+ underlined legal links */
1660
+ .md3 .enquiry-aside { background: transparent; border-bottom: 0; }
1661
+ .md3 .enquiry-form .chip--selected { box-shadow: inset 0 0 0 1.5px var(--md-primary);
1662
+ background: var(--fig-chip-bg); color: var(--md-primary); }
1663
+ .md3 .enq-consent a { text-decoration: underline; }
1664
+ /* HowItWorks: text column beside the circle (title above description) */
1665
+ .md3 .hiw-body { display: flex; flex-direction: column; min-width: 0; }
1633
1666
  /* phone hero rate: 44px — with the € folded into the big span (round 11) 52px pushed the
1634
1667
  stacked /mtl. unit onto a second line; 44px fits "143 €" + stack beside the rating column */
1635
1668
  .md3 .fin-rate-main .num { font-size: 44px !important; letter-spacing: -1.2px !important; }
@@ -30,36 +30,67 @@ export interface SliderProps {
30
30
  onChange: (value: number) => void;
31
31
  /** formats the min/max end labels (and is reused by callers for the value). */
32
32
  fmt?: (v: number) => React.ReactNode;
33
+ /** render the current value in a floating bubble above the thumb (Figma sheet sliders). */
34
+ bubble?: boolean;
33
35
  }
34
36
 
35
37
  /**
36
38
  * Range slider with a filled track and min/max end labels. The track fill is a
37
39
  * linear-gradient driven by the value's position; the design's `.mslider` class
38
- * styles the thumb/rail.
40
+ * styles the thumb/rail. With `bubble` the current value floats above the thumb
41
+ * (mockup "Rate anpassen" sliders) instead of the caller printing it elsewhere.
39
42
  */
40
- export const Slider: React.FunctionComponent<SliderProps> = ({ min, max, step = 1, value, onChange, fmt = (v) => v }) => {
43
+ export const Slider: React.FunctionComponent<SliderProps> = ({ min, max, step = 1, value, onChange, fmt = (v) => v, bubble }) => {
41
44
  const span = max - min;
42
45
  const pct = span > 0 ? ((value - min) / span) * 100 : 0;
43
46
  return (
44
47
  <div>
45
- <input
46
- type="range"
47
- className="mslider"
48
- min={min}
49
- max={max}
50
- step={step}
51
- value={value}
52
- onChange={(e) => onChange(Number(e.target.value))}
53
- style={{ background: `linear-gradient(90deg, var(--md-primary) ${pct}%, var(--md-sc-highest) ${pct}%)` }}
54
- />
55
- <div style={{ display: 'flex', justifyContent: 'space-between', marginTop: 4 }}>
56
- <span className="md-label-sm">{fmt(min)}</span>
57
- <span className="md-label-sm">{fmt(max)}</span>
48
+ <div style={{ position: 'relative', paddingTop: bubble ? 38 : 0 }}>
49
+ {bubble && (
50
+ // thumb is 22px wide: interpolate the bubble anchor between half-thumb insets so it
51
+ // tracks the thumb center (a plain pct% drifts at the rail ends)
52
+ <span className="slider-bubble num" style={{ left: `calc(11px + (100% - 22px) * ${pct / 100})` }}>
53
+ {fmt(value)}
54
+ </span>
55
+ )}
56
+ <input
57
+ type="range"
58
+ className="mslider"
59
+ min={min}
60
+ max={max}
61
+ step={step}
62
+ value={value}
63
+ onChange={(e) => onChange(Number(e.target.value))}
64
+ style={{ background: `linear-gradient(90deg, var(--md-primary) ${pct}%, var(--md-sc-highest) ${pct}%)` }}
65
+ />
58
66
  </div>
67
+ {!bubble && (
68
+ <div style={{ display: 'flex', justifyContent: 'space-between', marginTop: 4 }}>
69
+ <span className="md-label-sm">{fmt(min)}</span>
70
+ <span className="md-label-sm">{fmt(max)}</span>
71
+ </div>
72
+ )}
59
73
  </div>
60
74
  );
61
75
  };
62
76
 
77
+ /**
78
+ * Segmented price-evaluation bar (Figma: 5 discrete tier segments, NOT a gradient) with a
79
+ * white ring marker. `leftPct` overrides the marker position (the Marktwert gauge positions
80
+ * it continuously); default = centered on the tier segment.
81
+ */
82
+ export const EvalBar: React.FunctionComponent<{ tier: number; leftPct?: number }> = ({ tier, leftPct }) => (
83
+ <div className="fin-eval-bar">
84
+ {TIER_COLORS.map((c, i) => (
85
+ <span key={i} className="fin-eval-seg" style={{ background: c }} />
86
+ ))}
87
+ <div
88
+ className="fin-eval-thumb"
89
+ style={{ left: `${leftPct != null ? leftPct : (tier + 0.5) * 20}%`, borderColor: TIER_COLORS[Math.max(0, Math.min(4, tier))] }}
90
+ />
91
+ </div>
92
+ );
93
+
63
94
  export interface TermChipsProps {
64
95
  /** the selectable term values (months), e.g. [12,24,36,48,60,72]. */
65
96
  options: number[];
@@ -97,6 +128,7 @@ export interface CalcRowProps {
97
128
  /** One label/value row inside the "Rate anpassen" / "Details" expander. */
98
129
  export const CalcRow: React.FunctionComponent<CalcRowProps> = ({ label, last, children }) => (
99
130
  <div
131
+ className="calcrow"
100
132
  style={{
101
133
  display: 'flex',
102
134
  justifyContent: 'space-between',
@@ -106,8 +138,8 @@ export const CalcRow: React.FunctionComponent<CalcRowProps> = ({ label, last, ch
106
138
  borderBottom: last ? 0 : '1px solid var(--md-outline-variant)',
107
139
  }}
108
140
  >
109
- <span className="md-label-sm">{label}</span>
110
- <span className="num" style={{ fontSize: 13.5, fontWeight: 600 }}>
141
+ <span className="md-label-sm calcrow-label">{label}</span>
142
+ <span className="num calcrow-value" style={{ fontSize: 13.5, fontWeight: 600 }}>
111
143
  {children}
112
144
  </span>
113
145
  </div>
@@ -361,7 +393,7 @@ const FinanceCalculator: React.FunctionComponent<FinanceCalculatorProps> = ({
361
393
  </div>
362
394
  )}
363
395
  </div>
364
- {/* col 2 row 1: price-rating pill */}
396
+ {/* col 2 row 1: price-rating pill (Figma: bold colored label, NO icon) */}
365
397
  <div>
366
398
  {ratingLabel && (
367
399
  <span
@@ -369,26 +401,14 @@ const FinanceCalculator: React.FunctionComponent<FinanceCalculatorProps> = ({
369
401
  style={{ color: TIER_COLORS[evalTier], background: `color-mix(in srgb, ${TIER_COLORS[evalTier]} 16%, transparent)` }}
370
402
  aria-label={t('autobahn:finance.priceRatingAria', { rating: evalTierLabel })}
371
403
  >
372
- <Icon
373
- name={evalTier <= 1 ? 'check' : evalTier === 2 ? 'minus' : 'trending-up'}
374
- size={14}
375
- stroke={2.6}
376
- color={TIER_COLORS[evalTier]}
377
- />
378
404
  {evalTierLabel}
379
405
  </span>
380
406
  )}
381
407
  </div>
382
408
  {/* col 1 row 2: condition line */}
383
409
  <div className="fin-cond">{condLine}</div>
384
- {/* col 2 row 2: gradient evaluation bar */}
385
- <div>
386
- {ratingLabel && (
387
- <div className="fin-eval-bar">
388
- <div className="fin-eval-thumb" style={{ left: `${(evalTier + 0.5) * 20}%`, borderColor: TIER_COLORS[evalTier] }} />
389
- </div>
390
- )}
391
- </div>
410
+ {/* col 2 row 2: segmented evaluation bar (Figma: discrete tiers, not a gradient) */}
411
+ <div>{ratingLabel && <EvalBar tier={evalTier} />}</div>
392
412
  </div>
393
413
 
394
414
  {/* ============================== DESKTOP (≥601px) — unchanged ============================== */}
@@ -27,7 +27,7 @@ import { TranslationFunction } from '../types';
27
27
  import { Button, Icon, Spec, StatChip, Stars, Switch } from '../ui';
28
28
  import { CO2_CLASSES, CO2_COLORS, TIER_LABELS, TIER_KEYS, TIER_COLORS } from '../constants';
29
29
  import { defaultVdpSpecs, EquipmentGroups, PriceRatingBands, VehicleDetail, VdpSpec, VdpReview } from './types';
30
- import { CalcRow, expanderBtnStyle, FinanceMode, Slider, TermChips } from './FinanceCalculator';
30
+ import { CalcRow, EvalBar, expanderBtnStyle, FinanceMode, Slider, TermChips } from './FinanceCalculator';
31
31
 
32
32
  const eur = (n: number): string => n.toLocaleString('de-DE');
33
33
 
@@ -102,6 +102,30 @@ export const VdpHeaderSummary: React.FunctionComponent<VdpHeaderCardProps> = ({
102
102
  );
103
103
  };
104
104
 
105
+ /** the 6-fact icon grid (km/EZ · Getriebe/Kraftstoff · Hand/Karosserie) — reused by the
106
+ * mobile highlights card, which repeats the vehicle identity per the Figma master
107
+ * ("Vehicle Highlights / Card" carries its own title + facts above the pills). */
108
+ export const VdpFactsGrid: React.FunctionComponent<VdpHeaderCardProps> = ({ vehicle, t }) => {
109
+ const facts: ReadonlyArray<readonly [string, string]> = [
110
+ ['gauge', fmtKm(vehicle.km)],
111
+ ['calendar', vehicle.reg],
112
+ ['cog', vehicle.gear],
113
+ ['fuel', stripParen(vehicle.fuel)],
114
+ ['user', t('autobahn:header.owners', { owners: vehicle.owners, count: vehicle.owners })],
115
+ ...(vehicle.body ? ([['car-front', vehicle.body]] as const) : []),
116
+ ];
117
+ return (
118
+ <div className="vdp-meta" style={{ marginTop: 0 }}>
119
+ {facts.map(([ic, v]) => (
120
+ <span key={ic}>
121
+ <Icon name={ic} size={16} color="var(--md-on-surface-variant)" stroke={1.8} />
122
+ {v}
123
+ </span>
124
+ ))}
125
+ </div>
126
+ );
127
+ };
128
+
105
129
  /** hand-picked highlights — the accent rows of the header. ALL highlights are shown
106
130
  * (review round 9: "all highlights shown", no 6-chip cap); the Figma pill colors live
107
131
  * on .vdp-highlights .chip in the theme. */
@@ -362,9 +386,9 @@ export const Co2Label: React.FunctionComponent<Co2LabelProps> = ({ vehicle, t })
362
386
  [t('autobahn:eco.stat.co2WeightedCombined'), p.co2w],
363
387
  ]
364
388
  : [
389
+ // Figma: two stats only — the class letter lives on the scale, not as a stat row
365
390
  [t('autobahn:eco.stat.consumptionCombined'), vehicle.cons],
366
391
  [t('autobahn:eco.stat.co2Emissions'), vehicle.co2],
367
- [t('autobahn:eco.stat.co2Class'), vehicle.co2class],
368
392
  ];
369
393
  return (
370
394
  <div className="mcard" style={{ padding: 22 }}>
@@ -381,6 +405,10 @@ export const Co2Label: React.FunctionComponent<Co2LabelProps> = ({ vehicle, t })
381
405
  </div>
382
406
  ))}
383
407
  </div>
408
+ {/* scale caption (Figma) — replaces the old CO₂-Klasse stat row + EnVKV footer line */}
409
+ <div className="md-label-sm" style={{ margin: '0 0 8px' }}>
410
+ {t('autobahn:eco.co2ClassScaleLabel')}
411
+ </div>
384
412
  <div className="co2-scale">
385
413
  {CO2_CLASSES.map((c, i) => (
386
414
  <div key={c} className="co2-col">
@@ -407,17 +435,8 @@ export const Co2Label: React.FunctionComponent<Co2LabelProps> = ({ vehicle, t })
407
435
  </div>
408
436
  ))}
409
437
  </div>
410
- {p ? (
411
- <p className="md-label-sm" style={{ margin: '10px 0 0' }}>
412
- {t('autobahn:eco.co2ClassPrefix')} <b style={{ color: 'var(--md-on-surface)' }}>{p.co2classW}</b>{' '}
413
- {t('autobahn:eco.weightedCombined')} · <b style={{ color: 'var(--md-on-surface)' }}>{p.co2classD}</b>{' '}
414
- {t('autobahn:eco.dischargedCombined')} · {t('autobahn:eco.energyClassWltpSuffix')}
415
- </p>
416
- ) : (
417
- <p className="md-label-sm" style={{ margin: '10px 0 0' }}>
418
- {t('autobahn:eco.energyClass')}
419
- </p>
420
- )}
438
+ {/* no EnVKV footer line — Figma drops it (the * on the heading refers to the page-footer
439
+ disclaimer instead); the scale caption above carries the WLTP-kombiniert wording */}
421
440
  </div>
422
441
  );
423
442
  };
@@ -617,28 +636,12 @@ export const PriceHistory: React.FunctionComponent<PriceHistoryProps> = ({ vehic
617
636
  <div className="md-title-sm" style={{ marginBottom: 12 }}>
618
637
  {t('autobahn:marketValue.heading')}
619
638
  </div>
620
- <div
621
- style={{
622
- position: 'relative',
623
- height: 10,
624
- borderRadius: 'var(--md-r-full)',
625
- background: 'linear-gradient(90deg,#0F7B43,#C7B53A 55%,#D4502B)',
626
- }}
627
- >
628
- <span
629
- style={{
630
- position: 'absolute',
631
- top: '50%',
632
- left: `calc(${mpos * 100}% - 9px)`,
633
- transform: 'translateY(-50%)',
634
- width: 18,
635
- height: 18,
636
- borderRadius: '50%',
637
- background: '#fff',
638
- boxShadow: 'var(--md-e2), 0 0 0 2px var(--md-primary)',
639
- }}
640
- />
641
- </div>
639
+ {/* segmented tier bar (Figma — discrete colors, not a gradient), continuous marker;
640
+ ring color follows the car's actual rating tone */}
641
+ <EvalBar
642
+ tier={{ top: 0, good: 1, fair: 2, raised: 3, high: 4 }[vehicle.rating as 'top' | 'good' | 'fair' | 'raised' | 'high'] ?? 2}
643
+ leftPct={mpos * 100}
644
+ />
642
645
  <div style={{ display: 'flex', justifyContent: 'space-between', marginTop: 8 }}>
643
646
  <span className="md-label-sm">{eur(vehicle.marketLow!)} €</span>
644
647
  <span className="md-label-sm">{eur(vehicle.marketHigh!)} €</span>
@@ -650,13 +653,13 @@ export const PriceHistory: React.FunctionComponent<PriceHistoryProps> = ({ vehic
650
653
  alignItems: 'center',
651
654
  gap: 8,
652
655
  marginTop: 12,
653
- padding: '10px 12px',
656
+ padding: '13px 14px',
654
657
  borderRadius: 'var(--md-r-sm)',
655
658
  background: 'var(--md-good-container)',
656
659
  }}
657
660
  >
658
- <Icon name="badge-check" size={18} color="var(--md-on-good-container)" />
659
- <span style={{ fontSize: 13, fontWeight: 600, color: 'var(--md-on-good-container)' }}>
661
+ <Icon name="check-circle" size={18} color="var(--md-good)" />
662
+ <span style={{ fontSize: 14, fontWeight: 700, color: 'var(--md-good)' }}>
660
663
  {t('autobahn:marketValue.belowAverage', { rating: t('autobahn:tier.' + vehicle.rating), amount: eur(delta) })}
661
664
  </span>
662
665
  </div>
@@ -720,8 +723,41 @@ const RATING_TIERS: ReadonlyArray<readonly [keyof PriceRatingBands, string]> = [
720
723
  ['highPrice', 'var(--md-error)'],
721
724
  ];
722
725
 
726
+ /** collapsible sub-card header (blue icon + bold title + chevron) — Figma round 12. */
727
+ const BreakdownCardHead: React.FunctionComponent<{
728
+ icon: string;
729
+ title: string;
730
+ open: boolean;
731
+ onToggle: () => void;
732
+ }> = ({ icon, title, open, onToggle }) => (
733
+ <button
734
+ type="button"
735
+ onClick={onToggle}
736
+ style={{
737
+ display: 'flex',
738
+ alignItems: 'center',
739
+ gap: 10,
740
+ width: '100%',
741
+ background: 'none',
742
+ border: 0,
743
+ padding: 0,
744
+ cursor: 'pointer',
745
+ font: 'inherit',
746
+ textAlign: 'left',
747
+ }}
748
+ >
749
+ <Icon name={icon} size={19} color="var(--md-primary)" stroke={2} style={{ flex: 'none' }} />
750
+ <span className="md-title-sm" style={{ flex: 1, minWidth: 0, fontSize: 15 }}>
751
+ {title}
752
+ </span>
753
+ <Icon name={open ? 'chevron-up' : 'chevron-down'} size={18} color="var(--md-primary)" style={{ flex: 'none' }} />
754
+ </button>
755
+ );
756
+
723
757
  const PriceRatingBreakdown: React.FunctionComponent<{ vehicle: VehicleDetail; t: TranslationFunction }> = ({ vehicle, t }) => {
724
758
  const [open, setOpen] = useState(false);
759
+ const [criteriaOpen, setCriteriaOpen] = useState(true);
760
+ const [categoriesOpen, setCategoriesOpen] = useState(true);
725
761
  const bands = vehicle.priceRatingBands;
726
762
  if (!bands) return null;
727
763
 
@@ -752,11 +788,15 @@ const PriceRatingBreakdown: React.FunctionComponent<{ vehicle: VehicleDetail; t:
752
788
  </button>
753
789
  {open && (
754
790
  <div style={{ marginTop: 14, display: 'flex', flexDirection: 'column', gap: 18 }}>
755
- <div className="mcard mcard--filled" style={{ padding: 16 }}>
756
- <div className="md-title-sm" style={{ marginBottom: 12 }}>
757
- {t('autobahn:priceMarket.criteriaTitle')}
758
- </div>
759
- <div style={{ display: 'flex', flexWrap: 'wrap', gap: 14 }}>
791
+ <div className="mcard mcard--filled" style={{ padding: 16, flexDirection: 'column' }}>
792
+ <BreakdownCardHead
793
+ icon="list"
794
+ title={t('autobahn:priceMarket.criteriaTitle')}
795
+ open={criteriaOpen}
796
+ onToggle={() => setCriteriaOpen((o) => !o)}
797
+ />
798
+ {criteriaOpen && (
799
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: 14, marginTop: 14 }}>
760
800
  {criteria.map(([ic, label, value]) => (
761
801
  <div key={label} style={{ display: 'flex', alignItems: 'flex-start', gap: 9, minWidth: '44%', flex: '1 1 44%' }}>
762
802
  <Icon name={ic} size={17} color="var(--md-primary)" stroke={1.8} style={{ marginTop: 1, flex: 'none' }} />
@@ -771,14 +811,18 @@ const PriceRatingBreakdown: React.FunctionComponent<{ vehicle: VehicleDetail; t:
771
811
  </div>
772
812
  ))}
773
813
  </div>
814
+ )}
774
815
  </div>
775
816
 
776
- <div className="mcard mcard--filled" style={{ padding: 16 }}>
777
- <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
778
- <Icon name="bar-chart-3" size={18} color="var(--md-primary)" />
779
- <div className="md-title-sm">{t('autobahn:priceMarket.categoriesTitle')}</div>
780
- </div>
781
- <div style={{ display: 'flex', flexDirection: 'column' }}>
817
+ <div className="mcard mcard--filled" style={{ padding: 16, flexDirection: 'column' }}>
818
+ <BreakdownCardHead
819
+ icon="bar-chart-3"
820
+ title={t('autobahn:priceMarket.categoriesTitle')}
821
+ open={categoriesOpen}
822
+ onToggle={() => setCategoriesOpen((o) => !o)}
823
+ />
824
+ {categoriesOpen && (
825
+ <div style={{ display: 'flex', flexDirection: 'column', marginTop: 8 }}>
782
826
  {RATING_TIERS.map(([key, color], i) => (
783
827
  <div
784
828
  key={key}
@@ -824,6 +868,7 @@ const PriceRatingBreakdown: React.FunctionComponent<{ vehicle: VehicleDetail; t:
824
868
  </p>
825
869
  </div>
826
870
  </div>
871
+ )}
827
872
  </div>
828
873
  </div>
829
874
  )}
@@ -1198,17 +1243,9 @@ export const VdpBottomBar: React.FunctionComponent<VdpBottomBarProps> = ({
1198
1243
  className="fin-eval-pill"
1199
1244
  style={{ color: TIER_COLORS[evalTier], background: `color-mix(in srgb, ${TIER_COLORS[evalTier]} 16%, transparent)` }}
1200
1245
  >
1201
- <Icon
1202
- name={evalTier <= 1 ? 'check' : evalTier === 2 ? 'minus' : 'trending-up'}
1203
- size={13}
1204
- stroke={2.6}
1205
- color={TIER_COLORS[evalTier]}
1206
- />
1207
1246
  {t('autobahn:tier.' + TIER_KEYS[evalTier])}
1208
1247
  </span>
1209
- <div className="fin-eval-bar">
1210
- <div className="fin-eval-thumb" style={{ left: `${(evalTier + 0.5) * 20}%`, borderColor: TIER_COLORS[evalTier] }} />
1211
- </div>
1248
+ <EvalBar tier={evalTier} />
1212
1249
  </div>
1213
1250
  )}
1214
1251
  </div>
@@ -1308,6 +1345,8 @@ export interface FinanceDetailsModalProps {
1308
1345
  monthly: number;
1309
1346
  /** struck old rate (oldMonthlyInstallment, isStrikeShown-gated) — red line-through next to the rate. */
1310
1347
  oldMonthly?: number;
1348
+ /** struck list price (common.highestPrice, isStrikeShown-gated) — cash-tab strike (Figma). */
1349
+ listPrice?: number;
1311
1350
  apr: number;
1312
1351
  down: number;
1313
1352
  term: number;
@@ -1374,6 +1413,7 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
1374
1413
  price,
1375
1414
  monthly,
1376
1415
  oldMonthly,
1416
+ listPrice,
1377
1417
  apr,
1378
1418
  down,
1379
1419
  term,
@@ -1514,6 +1554,20 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
1514
1554
  </div>
1515
1555
  ));
1516
1556
 
1557
+ // mobile sheet footnote — the Figma representative example: real figures inline
1558
+ // ("³ Fahrzeugpreis: … €; Effektiver Jahreszins: … %; …") followed by the PangV sentence.
1559
+ // Only REAL config figures are included; absent ones are simply omitted.
1560
+ const exampleParts: string[] = [
1561
+ `${t('autobahn:modal.exampleVehiclePrice')}: ${eur(price)} € ${t('autobahn:modal.examplePriceSuffix')}`,
1562
+ ...(apr > 0 ? [`${t('autobahn:modal.exampleApr')}: ${pct(apr)}`] : []),
1563
+ ...(nominalRate != null && nominalRate > 0 ? [`${t('autobahn:modal.exampleNominal')}: ${pct(nominalRate)}`] : []),
1564
+ `${t('autobahn:modal.exampleNet')}: ${eur(netto)} €`,
1565
+ `${t('autobahn:modal.exampleDown')}: ${eur(down)} €`,
1566
+ ...(finalInstallment != null && finalInstallment > 0 ? [`${t('autobahn:modal.exampleFinal')}: ${eur(finalInstallment)} €`] : []),
1567
+ ...(grossAmount != null && grossAmount > 0 ? [`${t('autobahn:modal.exampleGross')}: ${eur(grossAmount)} €`] : []),
1568
+ ];
1569
+ const exampleLine = `³ ${exampleParts.join('; ')};`;
1570
+
1517
1571
  return (
1518
1572
  <div className="fin-modal-scrim" onClick={onClose}>
1519
1573
  <div className="fin-modal" role="dialog" aria-modal="true" aria-label={title} ref={sheetRef} onClick={(e) => e.stopPropagation()}>
@@ -1625,15 +1679,24 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
1625
1679
 
1626
1680
  <div className="fin-price-grid">
1627
1681
  <div className="fin-rate-main">
1628
- <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
1682
+ {/* Figma Kaufen sheet: struck list price on its own line ABOVE the big figure */}
1683
+ {isCash && listPrice != null && listPrice > price && (
1684
+ <s className="num fin-rate-old" style={{ display: 'block' }}>{eur(listPrice)} €</s>
1685
+ )}
1686
+ <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
1629
1687
  <span className="num" style={{ fontSize: 34, fontWeight: 800, color: 'var(--md-primary)', letterSpacing: '-1px' }}>
1630
1688
  {isCash ? eur(price) : eur(Math.round(monthly))} €
1631
1689
  </span>
1632
1690
  {!isCash && (
1633
- <span className="fin-rate-unit">
1634
- {oldMonthly != null && <s className="num fin-rate-old">{eur(Math.round(oldMonthly))} €</s>}
1635
- <span>{t('autobahn:finance.perMonth')}</span>
1636
- </span>
1691
+ <>
1692
+ <span className="md-label-sm" style={{ fontWeight: 600, alignSelf: 'flex-end', paddingBottom: 5 }}>
1693
+ {t('autobahn:finance.exampleLabel')}
1694
+ </span>
1695
+ <span className="fin-rate-unit">
1696
+ {oldMonthly != null && <s className="num fin-rate-old">{eur(Math.round(oldMonthly))} €</s>}
1697
+ <span>{t('autobahn:finance.perMonth')}</span>
1698
+ </span>
1699
+ </>
1637
1700
  )}
1638
1701
  </div>
1639
1702
  </div>
@@ -1643,24 +1706,12 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
1643
1706
  className="fin-eval-pill"
1644
1707
  style={{ color: TIER_COLORS[evalTier], background: `color-mix(in srgb, ${TIER_COLORS[evalTier]} 16%, transparent)` }}
1645
1708
  >
1646
- <Icon
1647
- name={evalTier <= 1 ? 'check' : evalTier === 2 ? 'minus' : 'trending-up'}
1648
- size={14}
1649
- stroke={2.6}
1650
- color={TIER_COLORS[evalTier]}
1651
- />
1652
1709
  {evalTierLabel}
1653
1710
  </span>
1654
1711
  )}
1655
1712
  </div>
1656
1713
  <div />
1657
- <div>
1658
- {ratingLabel && (
1659
- <div className="fin-eval-bar">
1660
- <div className="fin-eval-thumb" style={{ left: `${(evalTier + 0.5) * 20}%`, borderColor: TIER_COLORS[evalTier] }} />
1661
- </div>
1662
- )}
1663
- </div>
1714
+ <div>{ratingLabel && <EvalBar tier={evalTier} />}</div>
1664
1715
  </div>
1665
1716
 
1666
1717
  {isCash ? (
@@ -1675,14 +1726,14 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
1675
1726
  {addServicesCost != null && (
1676
1727
  <ToggleRow
1677
1728
  label={t('autobahn:finance.addServicesToggle')}
1678
- price={t('autobahn:finance.addServicesOnce', { value: eur(addServicesCost) })}
1729
+ price={t('autobahn:finance.addServicesOnce', { value: eur(Math.round(addServicesCost)) })}
1679
1730
  checked={!!withAddServices}
1680
1731
  disabled={addServicesDisabled}
1681
1732
  onChange={(checked) => onToggleAddServices && onToggleAddServices(checked)}
1682
1733
  />
1683
1734
  )}
1684
1735
  <div style={{ display: 'flex', gap: 10 }}>
1685
- <Button variant="filled" size="lg" icon="shield-check" style={{ flex: 1 }} onClick={onSecure}>
1736
+ <Button variant="filled" size="lg" icon="file-check" style={{ flex: 1 }} onClick={onSecure}>
1686
1737
  {t('autobahn:finance.secureOffer')}
1687
1738
  </Button>
1688
1739
  {/* padding:0 — mbtn--lg's 30px side padding left no room for the icon at width 52 */}
@@ -1703,7 +1754,6 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
1703
1754
  {eur(down)} €
1704
1755
  </span>
1705
1756
  </div>
1706
- <span className="fin-val-box num">{eur(down)} €</span>
1707
1757
  <Slider
1708
1758
  min={downMin as number}
1709
1759
  max={downMax as number}
@@ -1711,6 +1761,7 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
1711
1761
  value={down}
1712
1762
  onChange={onDownChange as (n: number) => void}
1713
1763
  fmt={(v) => `${eur(Number(v))} €`}
1764
+ bubble
1714
1765
  />
1715
1766
  </div>
1716
1767
  {isLease && annualMileage != null && onAnnualMileageChange && (
@@ -1724,7 +1775,6 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
1724
1775
  {eur(annualMileage)} km
1725
1776
  </span>
1726
1777
  </div>
1727
- <span className="fin-val-box num">{eur(annualMileage)} km</span>
1728
1778
  <Slider
1729
1779
  min={annualMileageMin}
1730
1780
  max={annualMileageMax}
@@ -1732,6 +1782,7 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
1732
1782
  value={annualMileage}
1733
1783
  onChange={onAnnualMileageChange}
1734
1784
  fmt={(v) => `${eur(Number(v))} km`}
1785
+ bubble
1735
1786
  />
1736
1787
  </div>
1737
1788
  )}
@@ -1755,7 +1806,7 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
1755
1806
  {addServicesCost != null && onToggleAddServices && (
1756
1807
  <ToggleRow
1757
1808
  label={t('autobahn:finance.addServicesToggle')}
1758
- price={t('autobahn:finance.addServicesPerMonth', { value: eur(addServicesCost) })}
1809
+ price={t('autobahn:finance.addServicesPerMonth', { value: eur(Math.round(addServicesCost)) })}
1759
1810
  checked={!!withAddServices}
1760
1811
  disabled={addServicesDisabled}
1761
1812
  onChange={onToggleAddServices}
@@ -1763,7 +1814,7 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
1763
1814
  )}
1764
1815
 
1765
1816
  <div style={{ display: 'flex', gap: 10 }}>
1766
- <Button variant="filled" size="lg" icon="shield-check" style={{ flex: 1 }} onClick={onSecure}>
1817
+ <Button variant="filled" size="lg" icon="file-check" style={{ flex: 1 }} onClick={onSecure}>
1767
1818
  {t('autobahn:finance.requestRate')}
1768
1819
  </Button>
1769
1820
  {/* padding:0 — mbtn--lg's 30px side padding left no room for the icon at width 52 */}
@@ -1777,11 +1828,13 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
1777
1828
  {isLease ? t('autobahn:modal.leasingDetails') : t('autobahn:modal.financingDetails')}
1778
1829
  </span>
1779
1830
  </button>
1780
- {/* mobile mockup shows NO row-by-row breakdown table (round 9 review) — only the
1781
- legally required representative example; the table stays desktop-only. */}
1831
+ {/* mobile mockup shows NO row-by-row breakdown table (round 9 review) — the Figma
1832
+ footnote: real figures inline, then the PangV sentence (round 12). */}
1782
1833
  {showBreakdown && (
1783
1834
  <div>
1784
1835
  <p className="md-label-sm" style={{ margin: 0, lineHeight: 1.55 }}>
1836
+ {exampleLine}
1837
+ <br />
1785
1838
  {isLease ? t('autobahn:modal.legalLeasing') : t('autobahn:modal.legalFinancing')}
1786
1839
  </p>
1787
1840
  </div>
@@ -1840,11 +1893,13 @@ export const ReviewsTeaser: React.FunctionComponent<ReviewsTeaserProps> = ({ cou
1840
1893
  }}
1841
1894
  >
1842
1895
  <span style={{ minWidth: 0 }}>
1843
- <span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
1844
- <Stars value={average} size={16} />
1845
- <span className="num md-title-sm">{average.toLocaleString('de-DE', { minimumFractionDigits: 1, maximumFractionDigits: 1 })}/5</span>
1896
+ <span style={{ display: 'inline-flex', alignItems: 'center', gap: 9 }}>
1897
+ <Stars value={average} size={19} />
1898
+ <span className="num" style={{ fontWeight: 800, fontSize: 20, color: 'var(--fig-navy)' }}>
1899
+ {average.toLocaleString('de-DE', { minimumFractionDigits: 1, maximumFractionDigits: 1 })}/5
1900
+ </span>
1846
1901
  </span>
1847
- <span className="md-label-sm" style={{ display: 'block', marginTop: 2 }}>
1902
+ <span style={{ display: 'block', marginTop: 4, fontSize: 14.5, color: 'var(--fig-navy)' }}>
1848
1903
  {t('autobahn:reviews.teaserCount', { count })}
1849
1904
  </span>
1850
1905
  </span>
@@ -2036,14 +2091,13 @@ export const VdpEnquiry: React.FunctionComponent<VdpEnquiryProps> = ({
2036
2091
  const emailOk = /.+@.+\..+/.test(email);
2037
2092
  const phoneDigits = (areaCode + phoneNum).replace(/\D/g, '');
2038
2093
  const phoneOk = phoneDigits.length >= 6;
2039
- // Figma round 9: Vorname/Nachname/E-Mail are always required (*), phone additionally
2040
- // when it's the preferred channel; the consent checkbox gates the send.
2094
+ // Figma round 12: the required-* follows the chosen contact channel — E-Mail* when email is
2095
+ // preferred, Telefonnummer* when phone is; names + consent are always required.
2041
2096
  const valid =
2042
2097
  firstName.trim().length > 0 &&
2043
2098
  lastName.trim().length > 0 &&
2044
- emailOk &&
2045
2099
  consent &&
2046
- (pref !== 'phone' || phoneOk);
2100
+ (pref === 'phone' ? phoneOk : emailOk);
2047
2101
 
2048
2102
  const submit = async (): Promise<void> => {
2049
2103
  if (!valid || sending) return;
@@ -2159,7 +2213,8 @@ export const VdpEnquiry: React.FunctionComponent<VdpEnquiryProps> = ({
2159
2213
  <input
2160
2214
  style={enquiryInput}
2161
2215
  type="email"
2162
- placeholder={t('autobahn:enquiry.form.emailPlaceholder')}
2216
+ // the required-* follows the preferred channel (Figma round 12)
2217
+ placeholder={pref === 'email' ? t('autobahn:enquiry.form.emailPlaceholder') : t('autobahn:enquiry.form.emailPlaceholder').replace(/\*$/, '')}
2163
2218
  value={email}
2164
2219
  onChange={(e) => setEmail(e.target.value)}
2165
2220
  aria-label={t('autobahn:enquiry.contactMethod.email')}
@@ -2181,7 +2236,7 @@ export const VdpEnquiry: React.FunctionComponent<VdpEnquiryProps> = ({
2181
2236
  aria-label={t('autobahn:enquiry.form.areaCodeLabel')}
2182
2237
  />
2183
2238
  </FloatField>
2184
- <FloatField label={t('autobahn:enquiry.form.phoneNumberLabel')}>
2239
+ <FloatField label={t('autobahn:enquiry.form.phoneNumberLabel') + (pref === 'phone' ? '*' : '')}>
2185
2240
  <input
2186
2241
  type="tel"
2187
2242
  value={phoneNum}
@@ -2202,7 +2257,7 @@ export const VdpEnquiry: React.FunctionComponent<VdpEnquiryProps> = ({
2202
2257
  {/* Nutzungsbedingungen sentence + revocable-consent checkbox (Figma round 9) */}
2203
2258
  <p className="md-body-sm" style={{ margin: '14px 0 0' }}>
2204
2259
  {t('autobahn:enquiry.form.termsPrefix')}{' '}
2205
- <a href="/nutzungsbedingungen/" target="_blank" rel="noopener noreferrer" style={{ color: 'inherit' }}>
2260
+ <a href="/nutzungsbedingungen/" target="_blank" rel="noopener noreferrer" style={{ color: 'inherit', textDecoration: 'underline' }}>
2206
2261
  {t('autobahn:enquiry.form.termsLink')}
2207
2262
  </a>{' '}
2208
2263
  {t('autobahn:enquiry.form.termsSuffix')}
@@ -2286,8 +2341,11 @@ export const HowItWorks: React.FunctionComponent<HowItWorksProps> = ({ steps, t
2286
2341
  <Icon name={STEP_ICONS[i] || 'star'} size={24} color="#fff" stroke={1.8} />
2287
2342
  <span className="hiw-num">{i + 1}</span>
2288
2343
  </div>
2289
- <div className="hiw-title">{title}</div>
2290
- <p className="hiw-desc">{desc}</p>
2344
+ {/* title stacked ABOVE the description (Figma) — one text column beside the circle */}
2345
+ <div className="hiw-body">
2346
+ <div className="hiw-title">{title}</div>
2347
+ <p className="hiw-desc">{desc}</p>
2348
+ </div>
2291
2349
  </div>
2292
2350
  ))}
2293
2351
  </div>
package/vdp/index.ts CHANGED
@@ -24,6 +24,7 @@ export type {
24
24
  export {
25
25
  VdpHeaderCard,
26
26
  VdpHeaderSummary,
27
+ VdpFactsGrid,
27
28
  VdpHeaderHighlights,
28
29
  VdpHeaderCondition,
29
30
  VdpTrustRow,