@bytebrand/fe-ui-core-autobahn 1.0.29 → 1.0.30

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.
@@ -61,6 +61,9 @@ export const VdpHeaderSummary: React.FunctionComponent<VdpHeaderCardProps> = ({
61
61
  ['cog', vehicle.gear],
62
62
  ['fuel', stripParen(vehicle.fuel)],
63
63
  ['user', t('autobahn:header.owners', { owners: vehicle.owners, count: vehicle.owners })],
64
+ // body type closes the fact grid (mockup: km|EZ / Getriebe|Kraftstoff / Hand|Karosserie);
65
+ // the compact phone dot-line hides everything from "Hand" on via CSS (nth-child(n+5)).
66
+ ...(vehicle.body ? ([['car-front', vehicle.body]] as const) : []),
64
67
  ];
65
68
  return (
66
69
  <>
@@ -99,10 +102,12 @@ export const VdpHeaderSummary: React.FunctionComponent<VdpHeaderCardProps> = ({
99
102
  );
100
103
  };
101
104
 
102
- /** hand-picked highlights — the single accent row of the header. */
105
+ /** hand-picked highlights — the accent rows of the header. ALL highlights are shown
106
+ * (review round 9: "all highlights shown", no 6-chip cap); the Figma pill colors live
107
+ * on .vdp-highlights .chip in the theme. */
103
108
  export const VdpHeaderHighlights: React.FunctionComponent<VdpHeaderCardProps> = ({ vehicle }) => (
104
109
  <div className="vdp-highlights" style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 8 }}>
105
- {vehicle.highlights.slice(0, 6).map((f) => (
110
+ {vehicle.highlights.map((f) => (
106
111
  <span key={f} className="chip chip--filter chip--selected" style={{ cursor: 'default' }}>
107
112
  <Icon name="check" size={14} stroke={2.4} />
108
113
  {f}
@@ -297,7 +302,7 @@ export const EquipmentList: React.FunctionComponent<EquipmentListProps> = ({ gro
297
302
  <div className="eq-cols">
298
303
  {entries.map(([g, items]) => (
299
304
  <div key={g} style={{ breakInside: 'avoid', marginBottom: 18 }}>
300
- <div className="md-title-sm" style={{ color: 'var(--md-primary)', marginBottom: 8 }}>
305
+ <div className="md-title-sm" style={{ color: 'var(--md-link)', marginBottom: 8 }}>
301
306
  {g}
302
307
  </div>
303
308
  <ul style={{ listStyle: 'none', margin: 0, padding: 0, display: 'flex', flexDirection: 'column', gap: 7 }}>
@@ -341,10 +346,14 @@ export interface Co2LabelProps {
341
346
  t: TranslationFunction;
342
347
  }
343
348
 
344
- /** CO₂ / energy-efficiency label (EU-style stepped colour band). Handles PHEV. */
349
+ /** CO₂ / energy-efficiency label (EU-style stepped colour band, Figma pastel scale).
350
+ * Handles PHEV with BOTH class markers (gew. komb. + entladen komb.) on the scale. */
345
351
  export const Co2Label: React.FunctionComponent<Co2LabelProps> = ({ vehicle, t }) => {
346
352
  const p = vehicle.phev;
347
353
  const idx = Math.max(0, CO2_CLASSES.indexOf((p ? p.co2classW : vehicle.co2class) as (typeof CO2_CLASSES)[number]));
354
+ // discharged-class marker (PHEV only); -1 when absent or same as the weighted class.
355
+ const idxD = p ? CO2_CLASSES.indexOf(p.co2classD as (typeof CO2_CLASSES)[number]) : -1;
356
+ const hasMarks = !!p && (idx >= 0 || idxD >= 0);
348
357
  const stats: ReadonlyArray<readonly [string, string]> = p
349
358
  ? [
350
359
  [t('autobahn:eco.stat.powerConsumptionWeighted'), p.consEl],
@@ -374,8 +383,27 @@ export const Co2Label: React.FunctionComponent<Co2LabelProps> = ({ vehicle, t })
374
383
  </div>
375
384
  <div className="co2-scale">
376
385
  {CO2_CLASSES.map((c, i) => (
377
- <div key={c} className={'co2-seg' + (i === idx ? ' co2-seg--on' : '')} style={{ background: CO2_COLORS[i] }}>
378
- {c}
386
+ <div key={c} className="co2-col">
387
+ {/* PHEV: tiny class markers above the weighted + discharged segments (mockup) */}
388
+ {hasMarks && (
389
+ <span className="co2-mark">
390
+ {i === idx
391
+ ? t('autobahn:eco.weightedShort')
392
+ : i === idxD && idxD !== idx
393
+ ? t('autobahn:eco.dischargedShort')
394
+ : ' '}
395
+ </span>
396
+ )}
397
+ <div
398
+ className={
399
+ 'co2-seg' +
400
+ (i === idx ? ' co2-seg--on' : '') +
401
+ (i === idxD && idxD !== idx ? ' co2-seg--on2' : '')
402
+ }
403
+ style={{ ['--seg' as string]: CO2_COLORS[i] } as React.CSSProperties}
404
+ >
405
+ {c}
406
+ </div>
379
407
  </div>
380
408
  ))}
381
409
  </div>
@@ -450,8 +478,8 @@ export const VehicleDescription: React.FunctionComponent<VehicleDescriptionProps
450
478
  // Trust strip
451
479
  // ---------------------------------------------------------------------------
452
480
 
453
- /** [kebab-case lucide icon, label]. */
454
- export type TrustItem = readonly [icon: string, label: string];
481
+ /** [kebab-case lucide icon, label, optional subline (mobile Figma tiles)]. */
482
+ export type TrustItem = readonly [icon: string, label: string, sub?: string];
455
483
 
456
484
  export interface TrustStripProps {
457
485
  /** trust badges; defaults to auto.de's four standard promises. */
@@ -460,21 +488,25 @@ export interface TrustStripProps {
460
488
  t: TranslationFunction;
461
489
  }
462
490
 
463
- /** auto.de's four standard promises — [icon, i18n label key]. */
464
- const DEFAULT_TRUST: ReadonlyArray<TrustItem> = [
465
- ['shield-check', 'autobahn:trust.warranty'],
466
- ['undo-2', 'autobahn:trust.return'],
467
- ['truck', 'autobahn:trust.delivery'],
468
- ['badge-check', 'autobahn:trust.inspection'],
491
+ /** auto.de's four standard promises — [icon, i18n label key, i18n subline key]. */
492
+ const DEFAULT_TRUST: ReadonlyArray<readonly [string, string, string]> = [
493
+ ['shield-check', 'autobahn:trust.warranty', 'autobahn:trust.warrantySub'],
494
+ ['undo-2', 'autobahn:trust.return', 'autobahn:trust.returnSub'],
495
+ ['truck', 'autobahn:trust.delivery', 'autobahn:trust.deliverySub'],
496
+ ['badge-check', 'autobahn:trust.inspection', 'autobahn:trust.inspectionSub'],
469
497
  ];
470
498
 
471
- /** Horizontal trust-signal strip. */
499
+ /** Trust-signal strip: quiet one-liner on desktop, Figma 2×2 tile card on mobile
500
+ * (the .trust-sub sublines only surface in the mobile tile layout via CSS). */
472
501
  export const TrustStrip: React.FunctionComponent<TrustStripProps> = ({ items, t }) => (
473
502
  <div className="trust-strip">
474
- {(items ?? DEFAULT_TRUST.map(([ic, key]) => [ic, t(key)] as TrustItem)).map(([ic, label]) => (
503
+ {(items ?? DEFAULT_TRUST.map(([ic, key, subKey]) => [ic, t(key), t(subKey)] as TrustItem)).map(([ic, label, sub]) => (
475
504
  <div key={label}>
476
505
  <Icon name={ic} size={17} color="var(--md-primary)" stroke={2} style={{ flex: 'none' }} />
477
- {label}
506
+ <span style={{ minWidth: 0 }}>
507
+ {label}
508
+ {sub && <span className="trust-sub">{sub}</span>}
509
+ </span>
478
510
  </div>
479
511
  ))}
480
512
  </div>
@@ -930,32 +962,111 @@ export interface VdpBottomBarProps {
930
962
  * after scrolling past the main offer card.
931
963
  */
932
964
  show: boolean;
965
+ /** active payment mode — drives the tabs + the displayed figure (rate vs price). */
966
+ mode?: FinanceMode;
967
+ /** mode tab tapped; when omitted the tabs row is not rendered. */
968
+ onModeChange?: (mode: FinanceMode) => void;
969
+ /** monthly rate for the CURRENT mode (fin/leasing differ); falls back to vehicle.monthly. */
970
+ monthly?: number;
971
+ /** price-rating label — drives the eval pill + gradient bar (same as the offer card). */
972
+ ratingLabel?: string;
933
973
  /** primary CTA click. */
934
974
  onSecure?: () => void;
975
+ /** phone CTA click. */
976
+ onCall?: () => void;
977
+ /** opens the "Rate anpassen" sheet; when omitted the button is not rendered. */
978
+ onOpenAdjust?: () => void;
935
979
  /** i18n function (drilled from the page). */
936
980
  t: TranslationFunction;
937
981
  }
938
982
 
939
- /** Mobile sticky bottom CTA bar — rate + Kaufpreis + "Angebot sichern". */
940
- export const VdpBottomBar: React.FunctionComponent<VdpBottomBarProps> = ({ vehicle, show, onSecure, t }) => (
941
- <div className={'vdp-bottom-bar show-mobile' + (show ? ' vdp-bottom-bar--on' : '')}>
942
- <div style={{ flex: 1, minWidth: 0 }}>
943
- <div style={{ display: 'flex', alignItems: 'baseline', gap: 5, whiteSpace: 'nowrap' }}>
944
- <span className="md-label-sm">{t('autobahn:bottom.from')}</span>
945
- <span className="num" style={{ fontWeight: 700, fontSize: 22, color: 'var(--md-primary)', letterSpacing: '-.5px' }}>
946
- {vehicle.monthly} €
947
- </span>
948
- <span className="md-label-sm">{t('autobahn:bottom.perMonth')}</span>
983
+ /** Mobile sticky bottom bar (Figma "Sticky Footer"): mode tabs, rate + price rating,
984
+ * then Angebot anfragen · phone · Rate anpassen. No struck old rate — there is no
985
+ * real previous-rate source (needs-data discipline), so nothing is fabricated. */
986
+ export const VdpBottomBar: React.FunctionComponent<VdpBottomBarProps> = ({
987
+ vehicle,
988
+ show,
989
+ mode = 'fin',
990
+ onModeChange,
991
+ monthly,
992
+ ratingLabel,
993
+ onSecure,
994
+ onCall,
995
+ onOpenAdjust,
996
+ t,
997
+ }) => {
998
+ const isCash = mode === 'cash';
999
+ const rate = monthly != null ? monthly : vehicle.monthly;
1000
+ const tierIdx = TIER_LABELS.indexOf(ratingLabel as (typeof TIER_LABELS)[number]);
1001
+ const evalTier = tierIdx >= 0 ? tierIdx : 1;
1002
+ return (
1003
+ <div className={'vdp-bottom-bar show-mobile' + (show ? ' vdp-bottom-bar--on' : '')}>
1004
+ {onModeChange && (
1005
+ <div className="fin-tabs bb-tabs" role="tablist">
1006
+ {(
1007
+ [
1008
+ ['fin', 'autobahn:finance.mode.financing'],
1009
+ ['leasing', 'autobahn:finance.mode.leasing'],
1010
+ ['cash', 'autobahn:card.price.buy'],
1011
+ ] as Array<[FinanceMode, string]>
1012
+ ).map(([k, labelKey]) => (
1013
+ <button
1014
+ key={k}
1015
+ type="button"
1016
+ role="tab"
1017
+ aria-selected={mode === k}
1018
+ className={'fin-tab' + (mode === k ? ' on' : '')}
1019
+ onClick={() => onModeChange(k)}
1020
+ >
1021
+ {t(labelKey)}
1022
+ </button>
1023
+ ))}
1024
+ </div>
1025
+ )}
1026
+ <div className="bb-price-row">
1027
+ <div style={{ display: 'flex', alignItems: 'baseline', gap: 5, whiteSpace: 'nowrap', minWidth: 0 }}>
1028
+ <span className="num" style={{ fontWeight: 800, fontSize: 30, color: 'var(--md-primary)', letterSpacing: '-1px' }}>
1029
+ {isCash ? eur(vehicle.price) : eur(Math.round(rate))} €
1030
+ </span>
1031
+ <span className="md-label-sm" style={{ fontWeight: 600 }}>
1032
+ {isCash ? t('autobahn:finance.fixedPrice') : `${t('autobahn:finance.exampleLabel')} ${t('autobahn:bottom.perMonth')}`}
1033
+ </span>
1034
+ </div>
1035
+ {ratingLabel && (
1036
+ <div className="bb-eval">
1037
+ <span
1038
+ className="fin-eval-pill"
1039
+ style={{ color: TIER_COLORS[evalTier], background: `color-mix(in srgb, ${TIER_COLORS[evalTier]} 16%, transparent)` }}
1040
+ >
1041
+ <Icon
1042
+ name={evalTier <= 1 ? 'check' : evalTier === 2 ? 'minus' : 'trending-up'}
1043
+ size={13}
1044
+ stroke={2.6}
1045
+ color={TIER_COLORS[evalTier]}
1046
+ />
1047
+ {t('autobahn:tier.' + TIER_KEYS[evalTier])}
1048
+ </span>
1049
+ <div className="fin-eval-bar">
1050
+ <div className="fin-eval-thumb" style={{ left: `${(evalTier + 0.5) * 20}%`, borderColor: TIER_COLORS[evalTier] }} />
1051
+ </div>
1052
+ </div>
1053
+ )}
949
1054
  </div>
950
- <div className="md-label-sm" style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
951
- {t('autobahn:bottom.purchasePrice', { price: eur(vehicle.price) })}
1055
+ <div className="bb-cta-row">
1056
+ <Button variant="filled" icon="file-check" style={{ flex: 1, minWidth: 0 }} onClick={onSecure}>
1057
+ {t('autobahn:bottom.secureOffer')}
1058
+ </Button>
1059
+ {/* padding:0 — icon-only button (same fix as the sheet's phone button) */}
1060
+ {onCall && <Button variant="outlined" icon="phone" style={{ width: 46, flex: 'none', padding: 0 }} onClick={onCall} />}
1061
+ {onOpenAdjust && !isCash && (
1062
+ <Button variant="outlined" icon="sliders-horizontal" className="bb-adjust" style={{ flex: 'none' }} onClick={onOpenAdjust}>
1063
+ {t('autobahn:finance.adjustRate')}
1064
+ </Button>
1065
+ )}
952
1066
  </div>
953
1067
  </div>
954
- <Button variant="filled" icon="shield-check" style={{ flex: 'none' }} onClick={onSecure}>
955
- {t('autobahn:bottom.secureOffer')}
956
- </Button>
957
- </div>
958
- );
1068
+ );
1069
+ };
959
1070
 
960
1071
  // ---------------------------------------------------------------------------
961
1072
  // 130-Punkte-Check block
@@ -1483,6 +1594,12 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
1483
1594
  export interface CustomerRatingsProps {
1484
1595
  /** the loaded reviews (REAL — from the WP json-quotes source). */
1485
1596
  reviews: VdpReview[];
1597
+ /**
1598
+ * Overall auto.de Google rating to display instead of the average of the few
1599
+ * loaded reviews (which skews to 5,0 — the review feed only carries 5-star
1600
+ * quotes). The page passes the real aggregate (4,6 per Google, review round 9).
1601
+ */
1602
+ average?: number;
1486
1603
  /** i18n function (drilled from the page). */
1487
1604
  t: TranslationFunction;
1488
1605
  }
@@ -1495,11 +1612,11 @@ export interface CustomerRatingsProps {
1495
1612
  * the WordPress json-quotes feed); the page loads them and passes them in. When no
1496
1613
  * reviews load, the page renders nothing (no fabricated content).
1497
1614
  */
1498
- export const CustomerRatings: React.FunctionComponent<CustomerRatingsProps> = ({ reviews, t }) => {
1615
+ export const CustomerRatings: React.FunctionComponent<CustomerRatingsProps> = ({ reviews, average, t }) => {
1499
1616
  const [showAll, setShowAll] = useState(false);
1500
1617
  if (!reviews || !reviews.length) return null;
1501
1618
  const count = reviews.length;
1502
- const avg = reviews.reduce((s, r) => s + (r.rating || 0), 0) / count;
1619
+ const avg = average != null ? average : reviews.reduce((s, r) => s + (r.rating || 0), 0) / count;
1503
1620
  const shown = showAll ? reviews : reviews.slice(0, 4);
1504
1621
  return (
1505
1622
  <section className="mcard" id="vdp-bewertungen" style={{ padding: 22, marginTop: 44 }}>
@@ -1608,15 +1725,13 @@ const enquiryInput: React.CSSProperties = {
1608
1725
  fontSize: 14,
1609
1726
  };
1610
1727
 
1611
- /** field with a small floating-style label above the input. */
1612
- const EnquiryField: React.FunctionComponent<{
1728
+ /** outlined field with the label floating ON the top border (Figma phone/message fields). */
1729
+ const FloatField: React.FunctionComponent<{
1613
1730
  label: string;
1614
1731
  children: React.ReactNode;
1615
1732
  }> = ({ label, children }) => (
1616
- <label style={{ display: 'flex', flexDirection: 'column', gap: 5 }}>
1617
- <span className="md-label-sm" style={{ fontWeight: 600 }}>
1618
- {label}
1619
- </span>
1733
+ <label className="enq-float">
1734
+ <span className="enq-float-label">{label}</span>
1620
1735
  {children}
1621
1736
  </label>
1622
1737
  );
@@ -1640,9 +1755,12 @@ export const VdpEnquiry: React.FunctionComponent<VdpEnquiryProps> = ({
1640
1755
  onSubmit,
1641
1756
  t,
1642
1757
  }) => {
1643
- const [name, setName] = useState('');
1758
+ const [firstName, setFirstName] = useState('');
1759
+ const [lastName, setLastName] = useState('');
1644
1760
  const [email, setEmail] = useState('');
1645
- const [phone, setPhone] = useState('');
1761
+ const [areaCode, setAreaCode] = useState('');
1762
+ const [phoneNum, setPhoneNum] = useState('');
1763
+ const [consent, setConsent] = useState(false);
1646
1764
  const [pref, setPref] = useState<'email' | 'phone'>('email');
1647
1765
  const [message, setMessage] = useState(
1648
1766
  autoDeId
@@ -1654,9 +1772,16 @@ export const VdpEnquiry: React.FunctionComponent<VdpEnquiryProps> = ({
1654
1772
  const [error, setError] = useState('');
1655
1773
 
1656
1774
  const emailOk = /.+@.+\..+/.test(email);
1657
- const phoneOk = phone.replace(/\D/g, '').length >= 6;
1658
- // the preferred channel is required; the other stays optional.
1659
- const valid = name.trim().length > 1 && (pref === 'email' ? emailOk : phoneOk);
1775
+ const phoneDigits = (areaCode + phoneNum).replace(/\D/g, '');
1776
+ const phoneOk = phoneDigits.length >= 6;
1777
+ // Figma round 9: Vorname/Nachname/E-Mail are always required (*), phone additionally
1778
+ // when it's the preferred channel; the consent checkbox gates the send.
1779
+ const valid =
1780
+ firstName.trim().length > 0 &&
1781
+ lastName.trim().length > 0 &&
1782
+ emailOk &&
1783
+ consent &&
1784
+ (pref !== 'phone' || phoneOk);
1660
1785
 
1661
1786
  const submit = async (): Promise<void> => {
1662
1787
  if (!valid || sending) return;
@@ -1665,8 +1790,14 @@ export const VdpEnquiry: React.FunctionComponent<VdpEnquiryProps> = ({
1665
1790
  const note = t('autobahn:enquiry.preferredContact', {
1666
1791
  channel: pref === 'email' ? t('autobahn:enquiry.contactMethod.email') : t('autobahn:enquiry.contactMethod.phone'),
1667
1792
  });
1793
+ const phone = phoneDigits ? `+49 ${areaCode.trim()} ${phoneNum.trim()}`.replace(/\s+/g, ' ').trim() : '';
1668
1794
  try {
1669
- await onSubmit({ name: name.trim(), email: email.trim(), phone: phone.trim(), message: message.trim() + note });
1795
+ await onSubmit({
1796
+ name: `${firstName.trim()} ${lastName.trim()}`,
1797
+ email: email.trim(),
1798
+ phone,
1799
+ message: message.trim() + note,
1800
+ });
1670
1801
  setSent(true);
1671
1802
  } catch (e) {
1672
1803
  setError(t('autobahn:enquiry.form.sendError'));
@@ -1744,38 +1875,103 @@ export const VdpEnquiry: React.FunctionComponent<VdpEnquiryProps> = ({
1744
1875
  </button>
1745
1876
  </div>
1746
1877
  </div>
1747
- <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(190px, 1fr))', gap: 12 }}>
1748
- <EnquiryField label={t('autobahn:enquiry.form.nameLabel')}>
1749
- <input style={enquiryInput} placeholder={t('autobahn:enquiry.form.namePlaceholder')} value={name} onChange={(e) => setName(e.target.value)} aria-label={t('autobahn:enquiry.form.nameAria')} />
1750
- </EnquiryField>
1751
- <EnquiryField label={pref === 'email' ? t('autobahn:enquiry.form.emailLabelRequired') : t('autobahn:enquiry.form.emailLabel')}>
1752
- <input style={enquiryInput} type="email" placeholder={t('autobahn:enquiry.form.emailPlaceholder')} value={email} onChange={(e) => setEmail(e.target.value)} aria-label={t('autobahn:enquiry.contactMethod.email')} />
1753
- </EnquiryField>
1754
- <EnquiryField label={pref === 'phone' ? t('autobahn:enquiry.form.phoneLabelRequired') : t('autobahn:enquiry.form.phoneLabel')}>
1755
- <input style={enquiryInput} type="tel" placeholder={t('autobahn:enquiry.form.phonePlaceholder')} value={phone} onChange={(e) => setPhone(e.target.value)} aria-label={t('autobahn:enquiry.contactMethod.phone')} />
1756
- </EnquiryField>
1878
+ {/* Vorname* / Nachname* — two required fields (Figma round 9), placeholder-labeled */}
1879
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))', gap: 12 }}>
1880
+ <input
1881
+ style={enquiryInput}
1882
+ placeholder={t('autobahn:enquiry.form.firstNamePlaceholder')}
1883
+ value={firstName}
1884
+ onChange={(e) => setFirstName(e.target.value)}
1885
+ aria-label={t('autobahn:enquiry.form.firstNameAria')}
1886
+ />
1887
+ <input
1888
+ style={enquiryInput}
1889
+ placeholder={t('autobahn:enquiry.form.lastNamePlaceholder')}
1890
+ value={lastName}
1891
+ onChange={(e) => setLastName(e.target.value)}
1892
+ aria-label={t('autobahn:enquiry.form.lastNameAria')}
1893
+ />
1757
1894
  </div>
1758
1895
  <div style={{ marginTop: 12 }}>
1759
- <EnquiryField label={t('autobahn:enquiry.form.messageLabel')}>
1896
+ <input
1897
+ style={enquiryInput}
1898
+ type="email"
1899
+ placeholder={t('autobahn:enquiry.form.emailPlaceholder')}
1900
+ value={email}
1901
+ onChange={(e) => setEmail(e.target.value)}
1902
+ aria-label={t('autobahn:enquiry.contactMethod.email')}
1903
+ />
1904
+ </div>
1905
+ {/* Land (+49 fixed) · Vorwahl · Telefonnummer — outlined float-label row (Figma) */}
1906
+ <div className="enq-phone-row" style={{ marginTop: 16 }}>
1907
+ <FloatField label={t('autobahn:enquiry.form.countryLabel')}>
1908
+ <span className="enq-country num">
1909
+ +49
1910
+ <Icon name="chevron-down" size={14} color="var(--md-on-surface-variant)" />
1911
+ </span>
1912
+ </FloatField>
1913
+ <FloatField label={t('autobahn:enquiry.form.areaCodeLabel')}>
1914
+ <input
1915
+ type="tel"
1916
+ value={areaCode}
1917
+ onChange={(e) => setAreaCode(e.target.value)}
1918
+ aria-label={t('autobahn:enquiry.form.areaCodeLabel')}
1919
+ />
1920
+ </FloatField>
1921
+ <FloatField label={t('autobahn:enquiry.form.phoneNumberLabel')}>
1922
+ <input
1923
+ type="tel"
1924
+ value={phoneNum}
1925
+ onChange={(e) => setPhoneNum(e.target.value)}
1926
+ aria-label={t('autobahn:enquiry.form.phoneNumberLabel')}
1927
+ />
1928
+ </FloatField>
1929
+ </div>
1930
+ <div style={{ marginTop: 16 }}>
1931
+ <FloatField label={t('autobahn:enquiry.form.messageLabel')}>
1760
1932
  <textarea
1761
- style={{ ...enquiryInput, height: 92, padding: '10px 14px', resize: 'vertical' }}
1762
1933
  value={message}
1763
1934
  onChange={(e) => setMessage(e.target.value)}
1764
1935
  aria-label={t('autobahn:enquiry.form.messageAria')}
1765
1936
  />
1766
- </EnquiryField>
1937
+ </FloatField>
1767
1938
  </div>
1939
+ {/* Nutzungsbedingungen sentence + revocable-consent checkbox (Figma round 9) */}
1940
+ <p className="md-body-sm" style={{ margin: '14px 0 0' }}>
1941
+ {t('autobahn:enquiry.form.termsPrefix')}{' '}
1942
+ <a href="/nutzungsbedingungen/" target="_blank" rel="noopener noreferrer" style={{ color: 'inherit' }}>
1943
+ {t('autobahn:enquiry.form.termsLink')}
1944
+ </a>{' '}
1945
+ {t('autobahn:enquiry.form.termsSuffix')}
1946
+ </p>
1947
+ <label className="enq-consent">
1948
+ <input type="checkbox" checked={consent} onChange={(e) => setConsent(e.target.checked)} />
1949
+ <span>
1950
+ {t('autobahn:enquiry.form.consentPart1')}{' '}
1951
+ <a href="/datenschutz/" target="_blank" rel="noopener noreferrer">
1952
+ {t('autobahn:enquiry.form.consentLinkHere')}
1953
+ </a>{' '}
1954
+ {t('autobahn:enquiry.form.consentPart2')}{' '}
1955
+ <a href="/datenschutz/" target="_blank" rel="noopener noreferrer">
1956
+ {t('autobahn:enquiry.form.consentLinkPrivacy')}
1957
+ </a>
1958
+ {t('autobahn:enquiry.form.consentPart3')}
1959
+ </span>
1960
+ </label>
1768
1961
  {error && (
1769
1962
  <div className="md-label-sm" style={{ color: 'var(--md-error)', marginTop: 8 }}>
1770
1963
  {error}
1771
1964
  </div>
1772
1965
  )}
1773
- <div style={{ display: 'flex', alignItems: 'center', gap: 14, marginTop: 16, flexWrap: 'wrap' }}>
1774
- <Button variant="filled" size="lg" icon="send" disabled={!valid || sending} onClick={submit}>
1966
+ <div style={{ marginTop: 16 }}>
1967
+ <Button variant="filled" block size="lg" icon="send" disabled={!valid || sending} onClick={submit}>
1775
1968
  {sending ? t('autobahn:enquiry.form.sending') : t('autobahn:enquiry.sendButton')}
1776
1969
  </Button>
1777
- <span className="md-label-sm" style={{ display: 'inline-flex', alignItems: 'center', gap: 5 }}>
1778
- <Icon name="lock" size={13} />
1970
+ <span
1971
+ className="md-label-sm"
1972
+ style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 5, marginTop: 10, color: 'var(--md-good)' }}
1973
+ >
1974
+ <Icon name="lock" size={13} color="var(--md-good)" />
1779
1975
  {t('autobahn:enquiry.security')}
1780
1976
  </span>
1781
1977
  </div>
@@ -1816,7 +2012,8 @@ export const HowItWorks: React.FunctionComponent<HowItWorksProps> = ({ steps, t
1816
2012
  steps ?? DEFAULT_STEPS.map(([titleKey, descKey]) => [t(titleKey), t(descKey)] as HowItWorksStep);
1817
2013
  return (
1818
2014
  <section className="hiw-section" style={{ marginTop: 44 }}>
1819
- <h2 className="md-headline-sm" style={{ marginBottom: 28, textAlign: 'center' }}>
2015
+ {/* alignment is breakpoint-owned: centered on desktop, left on the mobile card (CSS) */}
2016
+ <h2 className="md-headline-sm hiw-heading" style={{ marginBottom: 28 }}>
1820
2017
  {t('autobahn:howItWorks.title')}
1821
2018
  </h2>
1822
2019
  <div className="steps-4 hiw-grid">