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

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.
@@ -27,10 +27,29 @@ 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, DocCheckIcon, EvalBar, expanderBtnStyle, FinanceMode, Slider, TermChips } from './FinanceCalculator';
31
31
 
32
32
  const eur = (n: number): string => n.toLocaleString('de-DE');
33
33
 
34
+ /** Figma "check with open arc" glyph (modern lucide circle-check-big; the bundled lucide
35
+ * 0.288 only ships the closed-circle variant, so the two paths are inlined). */
36
+ const ArcCheckIcon: React.FunctionComponent<{ size?: number; color?: string }> = ({ size = 20, color = 'currentColor' }) => (
37
+ <svg
38
+ width={size}
39
+ height={size}
40
+ viewBox="0 0 24 24"
41
+ fill="none"
42
+ stroke={color}
43
+ strokeWidth={2.4}
44
+ strokeLinecap="round"
45
+ strokeLinejoin="round"
46
+ style={{ flex: 'none' }}
47
+ >
48
+ <path d="M21.801 10A10 10 0 1 1 17 3.335" />
49
+ <path d="m9 11 3 3L22 4" />
50
+ </svg>
51
+ );
52
+
34
53
  /** mileage → 'X.XXX km' (de-DE grouping). */
35
54
  function fmtKm(n: number): string {
36
55
  return `${n.toLocaleString('de-DE')} km`;
@@ -102,6 +121,30 @@ export const VdpHeaderSummary: React.FunctionComponent<VdpHeaderCardProps> = ({
102
121
  );
103
122
  };
104
123
 
124
+ /** the 6-fact icon grid (km/EZ · Getriebe/Kraftstoff · Hand/Karosserie) — reused by the
125
+ * mobile highlights card, which repeats the vehicle identity per the Figma master
126
+ * ("Vehicle Highlights / Card" carries its own title + facts above the pills). */
127
+ export const VdpFactsGrid: React.FunctionComponent<VdpHeaderCardProps> = ({ vehicle, t }) => {
128
+ const facts: ReadonlyArray<readonly [string, string]> = [
129
+ ['gauge', fmtKm(vehicle.km)],
130
+ ['calendar', vehicle.reg],
131
+ ['cog', vehicle.gear],
132
+ ['fuel', stripParen(vehicle.fuel)],
133
+ ['user', t('autobahn:header.owners', { owners: vehicle.owners, count: vehicle.owners })],
134
+ ...(vehicle.body ? ([['car-front', vehicle.body]] as const) : []),
135
+ ];
136
+ return (
137
+ <div className="vdp-meta" style={{ marginTop: 0 }}>
138
+ {facts.map(([ic, v]) => (
139
+ <span key={ic}>
140
+ <Icon name={ic} size={16} color="var(--md-on-surface-variant)" stroke={1.8} />
141
+ {v}
142
+ </span>
143
+ ))}
144
+ </div>
145
+ );
146
+ };
147
+
105
148
  /** hand-picked highlights — the accent rows of the header. ALL highlights are shown
106
149
  * (review round 9: "all highlights shown", no 6-chip cap); the Figma pill colors live
107
150
  * on .vdp-highlights .chip in the theme. */
@@ -362,9 +405,9 @@ export const Co2Label: React.FunctionComponent<Co2LabelProps> = ({ vehicle, t })
362
405
  [t('autobahn:eco.stat.co2WeightedCombined'), p.co2w],
363
406
  ]
364
407
  : [
408
+ // Figma: two stats only — the class letter lives on the scale, not as a stat row
365
409
  [t('autobahn:eco.stat.consumptionCombined'), vehicle.cons],
366
410
  [t('autobahn:eco.stat.co2Emissions'), vehicle.co2],
367
- [t('autobahn:eco.stat.co2Class'), vehicle.co2class],
368
411
  ];
369
412
  return (
370
413
  <div className="mcard" style={{ padding: 22 }}>
@@ -381,6 +424,10 @@ export const Co2Label: React.FunctionComponent<Co2LabelProps> = ({ vehicle, t })
381
424
  </div>
382
425
  ))}
383
426
  </div>
427
+ {/* scale caption (Figma) — replaces the old CO₂-Klasse stat row + EnVKV footer line */}
428
+ <div className="md-label-sm" style={{ margin: '0 0 8px' }}>
429
+ {t('autobahn:eco.co2ClassScaleLabel')}
430
+ </div>
384
431
  <div className="co2-scale">
385
432
  {CO2_CLASSES.map((c, i) => (
386
433
  <div key={c} className="co2-col">
@@ -407,17 +454,8 @@ export const Co2Label: React.FunctionComponent<Co2LabelProps> = ({ vehicle, t })
407
454
  </div>
408
455
  ))}
409
456
  </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
- )}
457
+ {/* no EnVKV footer line — Figma drops it (the * on the heading refers to the page-footer
458
+ disclaimer instead); the scale caption above carries the WLTP-kombiniert wording */}
421
459
  </div>
422
460
  );
423
461
  };
@@ -617,28 +655,12 @@ export const PriceHistory: React.FunctionComponent<PriceHistoryProps> = ({ vehic
617
655
  <div className="md-title-sm" style={{ marginBottom: 12 }}>
618
656
  {t('autobahn:marketValue.heading')}
619
657
  </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>
658
+ {/* segmented tier bar (Figma — discrete colors, not a gradient), continuous marker;
659
+ ring color follows the car's actual rating tone */}
660
+ <EvalBar
661
+ tier={{ top: 0, good: 1, fair: 2, raised: 3, high: 4 }[vehicle.rating as 'top' | 'good' | 'fair' | 'raised' | 'high'] ?? 2}
662
+ leftPct={mpos * 100}
663
+ />
642
664
  <div style={{ display: 'flex', justifyContent: 'space-between', marginTop: 8 }}>
643
665
  <span className="md-label-sm">{eur(vehicle.marketLow!)} €</span>
644
666
  <span className="md-label-sm">{eur(vehicle.marketHigh!)} €</span>
@@ -650,13 +672,13 @@ export const PriceHistory: React.FunctionComponent<PriceHistoryProps> = ({ vehic
650
672
  alignItems: 'center',
651
673
  gap: 8,
652
674
  marginTop: 12,
653
- padding: '10px 12px',
675
+ padding: '13px 14px',
654
676
  borderRadius: 'var(--md-r-sm)',
655
677
  background: 'var(--md-good-container)',
656
678
  }}
657
679
  >
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)' }}>
680
+ <ArcCheckIcon size={20} color="var(--md-good)" />
681
+ <span style={{ fontSize: 14, fontWeight: 700, color: 'var(--md-good)' }}>
660
682
  {t('autobahn:marketValue.belowAverage', { rating: t('autobahn:tier.' + vehicle.rating), amount: eur(delta) })}
661
683
  </span>
662
684
  </div>
@@ -720,8 +742,41 @@ const RATING_TIERS: ReadonlyArray<readonly [keyof PriceRatingBands, string]> = [
720
742
  ['highPrice', 'var(--md-error)'],
721
743
  ];
722
744
 
745
+ /** collapsible sub-card header (blue icon + bold title + chevron) — Figma round 12. */
746
+ const BreakdownCardHead: React.FunctionComponent<{
747
+ icon: string;
748
+ title: string;
749
+ open: boolean;
750
+ onToggle: () => void;
751
+ }> = ({ icon, title, open, onToggle }) => (
752
+ <button
753
+ type="button"
754
+ onClick={onToggle}
755
+ style={{
756
+ display: 'flex',
757
+ alignItems: 'center',
758
+ gap: 10,
759
+ width: '100%',
760
+ background: 'none',
761
+ border: 0,
762
+ padding: 0,
763
+ cursor: 'pointer',
764
+ font: 'inherit',
765
+ textAlign: 'left',
766
+ }}
767
+ >
768
+ <Icon name={icon} size={19} color="var(--md-primary)" stroke={2} style={{ flex: 'none' }} />
769
+ <span className="md-title-sm" style={{ flex: 1, minWidth: 0, fontSize: 15 }}>
770
+ {title}
771
+ </span>
772
+ <Icon name={open ? 'chevron-up' : 'chevron-down'} size={18} color="var(--md-primary)" style={{ flex: 'none' }} />
773
+ </button>
774
+ );
775
+
723
776
  const PriceRatingBreakdown: React.FunctionComponent<{ vehicle: VehicleDetail; t: TranslationFunction }> = ({ vehicle, t }) => {
724
777
  const [open, setOpen] = useState(false);
778
+ const [criteriaOpen, setCriteriaOpen] = useState(true);
779
+ const [categoriesOpen, setCategoriesOpen] = useState(true);
725
780
  const bands = vehicle.priceRatingBands;
726
781
  if (!bands) return null;
727
782
 
@@ -752,11 +807,15 @@ const PriceRatingBreakdown: React.FunctionComponent<{ vehicle: VehicleDetail; t:
752
807
  </button>
753
808
  {open && (
754
809
  <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 }}>
810
+ <div className="mcard mcard--filled" style={{ padding: 16, flexDirection: 'column' }}>
811
+ <BreakdownCardHead
812
+ icon="list"
813
+ title={t('autobahn:priceMarket.criteriaTitle')}
814
+ open={criteriaOpen}
815
+ onToggle={() => setCriteriaOpen((o) => !o)}
816
+ />
817
+ {criteriaOpen && (
818
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: 14, marginTop: 14 }}>
760
819
  {criteria.map(([ic, label, value]) => (
761
820
  <div key={label} style={{ display: 'flex', alignItems: 'flex-start', gap: 9, minWidth: '44%', flex: '1 1 44%' }}>
762
821
  <Icon name={ic} size={17} color="var(--md-primary)" stroke={1.8} style={{ marginTop: 1, flex: 'none' }} />
@@ -771,14 +830,18 @@ const PriceRatingBreakdown: React.FunctionComponent<{ vehicle: VehicleDetail; t:
771
830
  </div>
772
831
  ))}
773
832
  </div>
833
+ )}
774
834
  </div>
775
835
 
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' }}>
836
+ <div className="mcard mcard--filled" style={{ padding: 16, flexDirection: 'column' }}>
837
+ <BreakdownCardHead
838
+ icon="bar-chart-3"
839
+ title={t('autobahn:priceMarket.categoriesTitle')}
840
+ open={categoriesOpen}
841
+ onToggle={() => setCategoriesOpen((o) => !o)}
842
+ />
843
+ {categoriesOpen && (
844
+ <div style={{ display: 'flex', flexDirection: 'column', marginTop: 8 }}>
782
845
  {RATING_TIERS.map(([key, color], i) => (
783
846
  <div
784
847
  key={key}
@@ -824,6 +887,7 @@ const PriceRatingBreakdown: React.FunctionComponent<{ vehicle: VehicleDetail; t:
824
887
  </p>
825
888
  </div>
826
889
  </div>
890
+ )}
827
891
  </div>
828
892
  </div>
829
893
  )}
@@ -1198,22 +1262,15 @@ export const VdpBottomBar: React.FunctionComponent<VdpBottomBarProps> = ({
1198
1262
  className="fin-eval-pill"
1199
1263
  style={{ color: TIER_COLORS[evalTier], background: `color-mix(in srgb, ${TIER_COLORS[evalTier]} 16%, transparent)` }}
1200
1264
  >
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
1265
  {t('autobahn:tier.' + TIER_KEYS[evalTier])}
1208
1266
  </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>
1267
+ <EvalBar tier={evalTier} />
1212
1268
  </div>
1213
1269
  )}
1214
1270
  </div>
1215
1271
  <div className="bb-cta-row">
1216
- <Button variant="filled" icon="file-check" style={{ flex: 1, minWidth: 0 }} onClick={onSecure}>
1272
+ <Button variant="filled" style={{ flex: 1, minWidth: 0 }} onClick={onSecure}>
1273
+ <DocCheckIcon />
1217
1274
  {t('autobahn:bottom.secureOffer')}
1218
1275
  </Button>
1219
1276
  {/* padding:0 — icon-only button (same fix as the sheet's phone button) */}
@@ -1308,6 +1365,8 @@ export interface FinanceDetailsModalProps {
1308
1365
  monthly: number;
1309
1366
  /** struck old rate (oldMonthlyInstallment, isStrikeShown-gated) — red line-through next to the rate. */
1310
1367
  oldMonthly?: number;
1368
+ /** struck list price (common.highestPrice, isStrikeShown-gated) — cash-tab strike (Figma). */
1369
+ listPrice?: number;
1311
1370
  apr: number;
1312
1371
  down: number;
1313
1372
  term: number;
@@ -1374,6 +1433,7 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
1374
1433
  price,
1375
1434
  monthly,
1376
1435
  oldMonthly,
1436
+ listPrice,
1377
1437
  apr,
1378
1438
  down,
1379
1439
  term,
@@ -1514,6 +1574,20 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
1514
1574
  </div>
1515
1575
  ));
1516
1576
 
1577
+ // mobile sheet footnote — the Figma representative example: real figures inline
1578
+ // ("³ Fahrzeugpreis: … €; Effektiver Jahreszins: … %; …") followed by the PangV sentence.
1579
+ // Only REAL config figures are included; absent ones are simply omitted.
1580
+ const exampleParts: string[] = [
1581
+ `${t('autobahn:modal.exampleVehiclePrice')}: ${eur(price)} € ${t('autobahn:modal.examplePriceSuffix')}`,
1582
+ ...(apr > 0 ? [`${t('autobahn:modal.exampleApr')}: ${pct(apr)}`] : []),
1583
+ ...(nominalRate != null && nominalRate > 0 ? [`${t('autobahn:modal.exampleNominal')}: ${pct(nominalRate)}`] : []),
1584
+ `${t('autobahn:modal.exampleNet')}: ${eur(netto)} €`,
1585
+ `${t('autobahn:modal.exampleDown')}: ${eur(down)} €`,
1586
+ ...(finalInstallment != null && finalInstallment > 0 ? [`${t('autobahn:modal.exampleFinal')}: ${eur(finalInstallment)} €`] : []),
1587
+ ...(grossAmount != null && grossAmount > 0 ? [`${t('autobahn:modal.exampleGross')}: ${eur(grossAmount)} €`] : []),
1588
+ ];
1589
+ const exampleLine = `³ ${exampleParts.join('; ')};`;
1590
+
1517
1591
  return (
1518
1592
  <div className="fin-modal-scrim" onClick={onClose}>
1519
1593
  <div className="fin-modal" role="dialog" aria-modal="true" aria-label={title} ref={sheetRef} onClick={(e) => e.stopPropagation()}>
@@ -1625,16 +1699,27 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
1625
1699
 
1626
1700
  <div className="fin-price-grid">
1627
1701
  <div className="fin-rate-main">
1628
- <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
1702
+ <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
1629
1703
  <span className="num" style={{ fontSize: 34, fontWeight: 800, color: 'var(--md-primary)', letterSpacing: '-1px' }}>
1630
1704
  {isCash ? eur(price) : eur(Math.round(monthly))} €
1631
1705
  </span>
1632
- {!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>
1706
+ {/* Figma Kaufen sheet: struck list price top-right beside the big figure */}
1707
+ {isCash && listPrice != null && listPrice > price && (
1708
+ <span className="fin-rate-unit" style={{ alignSelf: 'flex-start', paddingTop: 2 }}>
1709
+ <s className="num fin-rate-old">{eur(listPrice)} €</s>
1636
1710
  </span>
1637
1711
  )}
1712
+ {!isCash && (
1713
+ <>
1714
+ <span className="md-label-sm" style={{ fontWeight: 600, alignSelf: 'flex-end', paddingBottom: 5 }}>
1715
+ {t('autobahn:finance.exampleLabel')}
1716
+ </span>
1717
+ <span className="fin-rate-unit">
1718
+ {oldMonthly != null && <s className="num fin-rate-old">{eur(Math.round(oldMonthly))} €</s>}
1719
+ <span>{t('autobahn:finance.perMonth')}</span>
1720
+ </span>
1721
+ </>
1722
+ )}
1638
1723
  </div>
1639
1724
  </div>
1640
1725
  <div>
@@ -1643,24 +1728,12 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
1643
1728
  className="fin-eval-pill"
1644
1729
  style={{ color: TIER_COLORS[evalTier], background: `color-mix(in srgb, ${TIER_COLORS[evalTier]} 16%, transparent)` }}
1645
1730
  >
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
1731
  {evalTierLabel}
1653
1732
  </span>
1654
1733
  )}
1655
1734
  </div>
1656
1735
  <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>
1736
+ <div>{ratingLabel && <EvalBar tier={evalTier} />}</div>
1664
1737
  </div>
1665
1738
 
1666
1739
  {isCash ? (
@@ -1675,14 +1748,15 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
1675
1748
  {addServicesCost != null && (
1676
1749
  <ToggleRow
1677
1750
  label={t('autobahn:finance.addServicesToggle')}
1678
- price={t('autobahn:finance.addServicesOnce', { value: eur(addServicesCost) })}
1751
+ price={t('autobahn:finance.addServicesOnce', { value: eur(Math.round(addServicesCost)) })}
1679
1752
  checked={!!withAddServices}
1680
1753
  disabled={addServicesDisabled}
1681
1754
  onChange={(checked) => onToggleAddServices && onToggleAddServices(checked)}
1682
1755
  />
1683
1756
  )}
1684
1757
  <div style={{ display: 'flex', gap: 10 }}>
1685
- <Button variant="filled" size="lg" icon="shield-check" style={{ flex: 1 }} onClick={onSecure}>
1758
+ <Button variant="filled" size="lg" style={{ flex: 1 }} onClick={onSecure}>
1759
+ <DocCheckIcon />
1686
1760
  {t('autobahn:finance.secureOffer')}
1687
1761
  </Button>
1688
1762
  {/* padding:0 — mbtn--lg's 30px side padding left no room for the icon at width 52 */}
@@ -1703,7 +1777,6 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
1703
1777
  {eur(down)} €
1704
1778
  </span>
1705
1779
  </div>
1706
- <span className="fin-val-box num">{eur(down)} €</span>
1707
1780
  <Slider
1708
1781
  min={downMin as number}
1709
1782
  max={downMax as number}
@@ -1711,6 +1784,7 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
1711
1784
  value={down}
1712
1785
  onChange={onDownChange as (n: number) => void}
1713
1786
  fmt={(v) => `${eur(Number(v))} €`}
1787
+ bubble
1714
1788
  />
1715
1789
  </div>
1716
1790
  {isLease && annualMileage != null && onAnnualMileageChange && (
@@ -1724,7 +1798,6 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
1724
1798
  {eur(annualMileage)} km
1725
1799
  </span>
1726
1800
  </div>
1727
- <span className="fin-val-box num">{eur(annualMileage)} km</span>
1728
1801
  <Slider
1729
1802
  min={annualMileageMin}
1730
1803
  max={annualMileageMax}
@@ -1732,6 +1805,7 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
1732
1805
  value={annualMileage}
1733
1806
  onChange={onAnnualMileageChange}
1734
1807
  fmt={(v) => `${eur(Number(v))} km`}
1808
+ bubble
1735
1809
  />
1736
1810
  </div>
1737
1811
  )}
@@ -1755,7 +1829,7 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
1755
1829
  {addServicesCost != null && onToggleAddServices && (
1756
1830
  <ToggleRow
1757
1831
  label={t('autobahn:finance.addServicesToggle')}
1758
- price={t('autobahn:finance.addServicesPerMonth', { value: eur(addServicesCost) })}
1832
+ price={t('autobahn:finance.addServicesPerMonth', { value: eur(Math.round(addServicesCost)) })}
1759
1833
  checked={!!withAddServices}
1760
1834
  disabled={addServicesDisabled}
1761
1835
  onChange={onToggleAddServices}
@@ -1763,7 +1837,8 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
1763
1837
  )}
1764
1838
 
1765
1839
  <div style={{ display: 'flex', gap: 10 }}>
1766
- <Button variant="filled" size="lg" icon="shield-check" style={{ flex: 1 }} onClick={onSecure}>
1840
+ <Button variant="filled" size="lg" style={{ flex: 1 }} onClick={onSecure}>
1841
+ <DocCheckIcon />
1767
1842
  {t('autobahn:finance.requestRate')}
1768
1843
  </Button>
1769
1844
  {/* padding:0 — mbtn--lg's 30px side padding left no room for the icon at width 52 */}
@@ -1777,11 +1852,13 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
1777
1852
  {isLease ? t('autobahn:modal.leasingDetails') : t('autobahn:modal.financingDetails')}
1778
1853
  </span>
1779
1854
  </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. */}
1855
+ {/* mobile mockup shows NO row-by-row breakdown table (round 9 review) — the Figma
1856
+ footnote: real figures inline, then the PangV sentence (round 12). */}
1782
1857
  {showBreakdown && (
1783
1858
  <div>
1784
1859
  <p className="md-label-sm" style={{ margin: 0, lineHeight: 1.55 }}>
1860
+ {exampleLine}
1861
+ <br />
1785
1862
  {isLease ? t('autobahn:modal.legalLeasing') : t('autobahn:modal.legalFinancing')}
1786
1863
  </p>
1787
1864
  </div>
@@ -1840,11 +1917,13 @@ export const ReviewsTeaser: React.FunctionComponent<ReviewsTeaserProps> = ({ cou
1840
1917
  }}
1841
1918
  >
1842
1919
  <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>
1920
+ <span style={{ display: 'inline-flex', alignItems: 'center', gap: 9 }}>
1921
+ <Stars value={average} size={19} />
1922
+ <span className="num" style={{ fontWeight: 800, fontSize: 20, color: 'var(--fig-navy)' }}>
1923
+ {average.toLocaleString('de-DE', { minimumFractionDigits: 1, maximumFractionDigits: 1 })}/5
1924
+ </span>
1846
1925
  </span>
1847
- <span className="md-label-sm" style={{ display: 'block', marginTop: 2 }}>
1926
+ <span style={{ display: 'block', marginTop: 4, fontSize: 14.5, color: 'var(--fig-navy)' }}>
1848
1927
  {t('autobahn:reviews.teaserCount', { count })}
1849
1928
  </span>
1850
1929
  </span>
@@ -2036,14 +2115,14 @@ export const VdpEnquiry: React.FunctionComponent<VdpEnquiryProps> = ({
2036
2115
  const emailOk = /.+@.+\..+/.test(email);
2037
2116
  const phoneDigits = (areaCode + phoneNum).replace(/\D/g, '');
2038
2117
  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.
2118
+ // Round 13 (explicit spec): PHONE is always required (the * sits on the country-code field),
2119
+ // e-mail is always optional (validated only when filled); names + consent always required.
2041
2120
  const valid =
2042
2121
  firstName.trim().length > 0 &&
2043
2122
  lastName.trim().length > 0 &&
2044
- emailOk &&
2045
2123
  consent &&
2046
- (pref !== 'phone' || phoneOk);
2124
+ phoneOk &&
2125
+ (email.trim().length === 0 || emailOk);
2047
2126
 
2048
2127
  const submit = async (): Promise<void> => {
2049
2128
  if (!valid || sending) return;
@@ -2159,7 +2238,8 @@ export const VdpEnquiry: React.FunctionComponent<VdpEnquiryProps> = ({
2159
2238
  <input
2160
2239
  style={enquiryInput}
2161
2240
  type="email"
2162
- placeholder={t('autobahn:enquiry.form.emailPlaceholder')}
2241
+ // e-mail is always optional (round 13) — no required-*
2242
+ placeholder={t('autobahn:enquiry.form.emailPlaceholder').replace(/\*$/, '')}
2163
2243
  value={email}
2164
2244
  onChange={(e) => setEmail(e.target.value)}
2165
2245
  aria-label={t('autobahn:enquiry.contactMethod.email')}
@@ -2167,7 +2247,8 @@ export const VdpEnquiry: React.FunctionComponent<VdpEnquiryProps> = ({
2167
2247
  </div>
2168
2248
  {/* Land (+49 fixed) · Vorwahl · Telefonnummer — outlined float-label row (Figma) */}
2169
2249
  <div className="enq-phone-row" style={{ marginTop: 16 }}>
2170
- <FloatField label={t('autobahn:enquiry.form.countryLabel')}>
2250
+ {/* phone is always required — the * sits on the country-code field (round 13) */}
2251
+ <FloatField label={t('autobahn:enquiry.form.countryLabel') + '*'}>
2171
2252
  <span className="enq-country num">
2172
2253
  +49
2173
2254
  <Icon name="chevron-down" size={14} color="var(--md-on-surface-variant)" />
@@ -2202,7 +2283,7 @@ export const VdpEnquiry: React.FunctionComponent<VdpEnquiryProps> = ({
2202
2283
  {/* Nutzungsbedingungen sentence + revocable-consent checkbox (Figma round 9) */}
2203
2284
  <p className="md-body-sm" style={{ margin: '14px 0 0' }}>
2204
2285
  {t('autobahn:enquiry.form.termsPrefix')}{' '}
2205
- <a href="/nutzungsbedingungen/" target="_blank" rel="noopener noreferrer" style={{ color: 'inherit' }}>
2286
+ <a href="/nutzungsbedingungen/" target="_blank" rel="noopener noreferrer" style={{ color: 'inherit', textDecoration: 'underline' }}>
2206
2287
  {t('autobahn:enquiry.form.termsLink')}
2207
2288
  </a>{' '}
2208
2289
  {t('autobahn:enquiry.form.termsSuffix')}
@@ -2286,8 +2367,11 @@ export const HowItWorks: React.FunctionComponent<HowItWorksProps> = ({ steps, t
2286
2367
  <Icon name={STEP_ICONS[i] || 'star'} size={24} color="#fff" stroke={1.8} />
2287
2368
  <span className="hiw-num">{i + 1}</span>
2288
2369
  </div>
2289
- <div className="hiw-title">{title}</div>
2290
- <p className="hiw-desc">{desc}</p>
2370
+ {/* title stacked ABOVE the description (Figma) — one text column beside the circle */}
2371
+ <div className="hiw-body">
2372
+ <div className="hiw-title">{title}</div>
2373
+ <p className="hiw-desc">{desc}</p>
2374
+ </div>
2291
2375
  </div>
2292
2376
  ))}
2293
2377
  </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,