@bytebrand/fe-ui-core-autobahn 1.0.155 → 1.0.157

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cards/CarCard.tsx CHANGED
@@ -157,8 +157,6 @@ const CarCard: React.FunctionComponent<CarCardProps> = ({
157
157
  // active that's already 3, so NEU is dropped rather than causing a 4th to overflow.
158
158
  const activeDealChips = [showDrop || showSave, showPct, !!car.zeroDown].filter(Boolean).length;
159
159
  const showNeu = asGoodAsNew && car.isNew && activeDealChips < 3;
160
- // tabs the price block can switch to (PriceTabs shows Leasing only when the car has a lease rate)
161
- const paymentModes: PaymentMode[] = car.lease != null ? ['financing', 'leasing', 'buy'] : ['financing', 'buy'];
162
160
  return (
163
161
  <article
164
162
  className="mcard mcard--interactive cc-card"
@@ -279,46 +277,18 @@ const CarCard: React.FunctionComponent<CarCardProps> = ({
279
277
  <div className="cc-price" style={{ marginTop: 0, borderTop: '1px solid var(--md-outline-variant)' }}>
280
278
  <PriceTabs car={car} mode={mode} onModeChange={setMode} t={t} />
281
279
  <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8, marginTop: 10 }}>
282
- {/* every tab's hero stacked in one grid cell (.cc-hero-stack), only the active one
283
- visible, bottom-aligned — Kaufen's struck list price sits on its own line above
284
- the price, so without this the row (and the card) grew/shrank by that line on
285
- every tab switch. Same trick as .cc-terms-stack below. */}
286
- <div className="cc-hero-stack">
287
- {paymentModes.map((m) => (
288
- <div
289
- key={m}
290
- className={'cc-hero-slot' + (m === mode ? '' : ' cc-hero-slot--hidden')}
291
- aria-hidden={m === mode ? undefined : true}
292
- >
293
- <PriceHero car={car} mode={m} t={t} alwaysShowUnit />
294
- </div>
295
- ))}
296
- </div>
280
+ <PriceHero car={car} mode={mode} t={t} alwaysShowUnit />
297
281
  <PriceEvalCol car={car} t={t} />
298
282
  </div>
299
283
  {/* same fallback CardCtaRow already uses below: pages that haven't wired a real
300
- "Rate anpassen" sheet yet still get a working "Details" link (opens the car).
301
- Every tab's terms line is rendered into the same grid cell (.cc-terms-stack) and
302
- only the active one is visible — the cell is as tall as the tallest line, so the
303
- card keeps one height across Finanzierung/Leasing/Kaufen (Leasing's longer line
304
- wraps to 2 lines on narrow phones). No change to the visible line itself. */}
305
- <div className="cc-terms-stack">
306
- {paymentModes.map((m) => (
307
- <div
308
- key={m}
309
- className={'cc-terms-slot' + (m === mode ? '' : ' cc-terms-slot--hidden')}
310
- aria-hidden={m === mode ? undefined : true}
311
- >
312
- <PriceTerms
313
- car={car}
314
- mode={m}
315
- onOpenRateAdjust={onOpenRateAdjust || (() => onOpen(car))}
316
- onShowDetails={onShowFinancingDetails ? () => onShowFinancingDetails(car, m) : undefined}
317
- t={t}
318
- />
319
- </div>
320
- ))}
321
- </div>
284
+ "Rate anpassen" sheet yet still get a working "Details" link (opens the car). */}
285
+ <PriceTerms
286
+ car={car}
287
+ mode={mode}
288
+ onOpenRateAdjust={onOpenRateAdjust || (() => onOpen(car))}
289
+ onShowDetails={onShowFinancingDetails ? () => onShowFinancingDetails(car, mode) : undefined}
290
+ t={t}
291
+ />
322
292
  </div>
323
293
  {/* Mobile-only row (CardCtaRow itself carries the show-mobile class) — always
324
294
  rendered, with real fallbacks the page can override once it wires its own
@@ -710,12 +710,7 @@ export const PriceTerms: React.FunctionComponent<{
710
710
  onShowDetails?: () => void;
711
711
  t: TranslationFunction;
712
712
  }> = ({ car, mode, onOpenRateAdjust, onShowDetails, t }) => {
713
- // Kaufen (buy): a cash purchase has no financing terms/details link — used to `return null`
714
- // here, which collapsed .cc-terms to 0 height on that tab while Financing/Leasing rendered a
715
- // real line, jumping the price row (and everything below it: rating pill, CTA row) up/down on
716
- // every tab switch. Render an empty, still-present .cc-terms div instead — same fix as the VDP
717
- // FinanceCalculator's .fin-cond (md3.css), which reserves its line's height the same way.
718
- if (mode === 'buy') return <div className="cc-terms" />;
713
+ if (mode === 'buy') return null;
719
714
  const finFirstInstallment =
720
715
  car.finFirstInstallment != null ? car.finFirstInstallment.toLocaleString('de-DE') : _finFirstInstallment;
721
716
  const finMonths = car.finMonths ?? _finMonths;
package/constants/app.ts CHANGED
@@ -40,17 +40,7 @@ export const LOCAL_STORAGE_KEY = {
40
40
  i18n_KEY: 'i18nLanguage'
41
41
  };
42
42
 
43
- // ADV-284: evaluated at import time — with site storage blocked, reading localStorage throws a
44
- // SecurityError that took down the whole bundle. Blocked storage behaves like "nothing saved".
45
- const readStoredLang = (): string | null => {
46
- try {
47
- return localStorage.getItem(LOCAL_STORAGE_KEY.i18n_KEY);
48
- } catch (e) {
49
- return null;
50
- }
51
- };
52
-
53
- export const DEFAULT_LANG: string = typeof window !== 'undefined' ? readStoredLang() : 'de';
43
+ export const DEFAULT_LANG: string = typeof window !== 'undefined' ? localStorage.getItem(LOCAL_STORAGE_KEY.i18n_KEY) : 'de';
54
44
 
55
45
  // Default support contact phone
56
46
  export const CONTACT_PHONE: string = '+498004030182';
package/constants/core.ts CHANGED
@@ -35,18 +35,7 @@ export const LOCAL_STORAGE_KEY = {
35
35
  i18n_KEY: 'i18nLanguage'
36
36
  };
37
37
 
38
- // ADV-284: evaluated at import time — with site storage blocked, merely reading
39
- // window.localStorage throws a SecurityError, which took down the whole bundle (landing
40
- // page never rendered). Treat blocked storage like "no language saved".
41
- const readStoredLang = (): string | null => {
42
- try {
43
- return typeof window !== 'undefined' ? window.localStorage.getItem(LOCAL_STORAGE_KEY.i18n_KEY) : null;
44
- } catch (e) {
45
- return null;
46
- }
47
- };
48
-
49
- export const DEFAULT_LANG: string = readStoredLang() ?? 'de';
38
+ export const DEFAULT_LANG: string = (typeof window !== 'undefined' ? window.localStorage.getItem(LOCAL_STORAGE_KEY.i18n_KEY) : null) ?? 'de';
50
39
 
51
40
  export const ROW_STYLES = {
52
41
  marginBottom: 10
@@ -88,7 +88,7 @@
88
88
  "detailsShow": "Details anzeigen",
89
89
  "paymentLabel": "Zahlung",
90
90
  "paymentValue": "Überweisung",
91
- "warrantyLabel": "Garantie",
91
+ "warrantyLabel": "Zusatzgarantie (Multipart)",
92
92
  "warrantyValue": "1 Jahr inklusive",
93
93
  "deliveryLabel": "Lieferung",
94
94
  "deliveryValue": "ca. 7–14 Tage · oder Abholung",
@@ -701,7 +701,7 @@
701
701
  },
702
702
  "modal": {
703
703
  "leasingDetails": "Leasingdetails",
704
- "financingDetails": "Details",
704
+ "financingDetails": "Finanzierungsdetails",
705
705
  "close": "Schließen",
706
706
  "vehiclePrice": "Fahrzeugpreis ggf. inkl. MwSt.",
707
707
  "leasingAmount": "Leasingbetrag",
@@ -88,7 +88,7 @@
88
88
  "detailsShow": "Show details",
89
89
  "paymentLabel": "Payment",
90
90
  "paymentValue": "Bank transfer",
91
- "warrantyLabel": "Warranty",
91
+ "warrantyLabel": "Additional warranty (Multipart)",
92
92
  "warrantyValue": "1 year included",
93
93
  "deliveryLabel": "Delivery",
94
94
  "deliveryValue": "approx. 7–14 days · or collection",
@@ -103,8 +103,7 @@ export const vehicleProps = (car?: any, isOfferAvailable: boolean = true) => {
103
103
  wltpDischargedCombinedLabel: `${getFormattedNumber(vehicleOption.wltpCombined != null ? vehicleOption.wltpCombined : 19.9)}l/100km (entladen, komb)`,
104
104
  wltpCo2WtdCombinedShort: `${getFormattedNumber(vehicleOption.wltpCo2)}g CO2/km (gew., komb)*`,
105
105
  co2ClassWeighted: `CO2-Klasse ${(cbd.cbd as any)[vehicleOption.wltpEnergyEfficiencyClass] || vehicleOption.wltpEnergyEfficiencyClass || ''} (gew., komb)`,
106
- // ADV-187: no hard-coded fallback class — callers only render this when the class is defined.
107
- co2ClassDischarged: `CO2-Klasse ${(cbd.cbd as any)[vehicleOption.wltpDischargedEnergyEfficiencyClass] || vehicleOption.wltpDischargedEnergyEfficiencyClass || ''} (entladen, komb)`
106
+ co2ClassDischarged: `CO2-Klasse ${(cbd.cbd as any)[vehicleOption.wltpDischargedEnergyEfficiencyClass || 'selector_energyEfficiencyClass_g'] || ''} (entladen, komb)`
108
107
  },
109
108
  title: {
110
109
  'addOfferToMainSlide': 'Zum Schieberegler hinzufügen',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bytebrand/fe-ui-core-autobahn",
3
- "version": "1.0.155",
3
+ "version": "1.0.157",
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
@@ -1822,6 +1822,14 @@ line-height: 31px;
1822
1822
  gap: 8px;
1823
1823
  flex-wrap: wrap;
1824
1824
  }
1825
+ /* Figma 64860-62117 (Kaufen only): ground-truth design-context pull — "row" is gap-[14px]
1826
+ items-start, not the shared gap-8/align-center financing/leasing's own row keeps (base rule
1827
+ above, untouched) — the taller 2-row old-price stack (badge above struck price, see
1828
+ .cf-rate-old--buy below) needs top alignment against the big price instead of centering. */
1829
+ .md3 .cf-rate-row.cf-rate-row--buy {
1830
+ gap: 14px;
1831
+ align-items: flex-start;
1832
+ }
1825
1833
  .md3 .cf-rate-amount {
1826
1834
  font-family: 'Arial';
1827
1835
  font-style: normal;
@@ -1830,6 +1838,16 @@ line-height: 31px;
1830
1838
  line-height: 58px;
1831
1839
  color: #005CFF;
1832
1840
  }
1841
+ /* Figma 64860-62117 (Kaufen only): exact text spec pasted by user — Arimo/700/36px/100%
1842
+ line-height/-1.6px letter-spacing — smaller + tighter than the shared 54px Arial price
1843
+ financing/leasing keep (base rule above, untouched). 2-class selector scopes to
1844
+ .cf-rate-amount--buy only; the ≤600px rule further down already matches this 36px size. */
1845
+ .md3 .cf-rate-amount.cf-rate-amount--buy {
1846
+ font-family: 'Arimo';
1847
+ font-size: 36px;
1848
+ line-height: 100%;
1849
+ letter-spacing: -1.6px;
1850
+ }
1833
1851
  .md3 .cf-rate-side {
1834
1852
  display: flex;
1835
1853
  flex-direction: column;
@@ -1856,6 +1874,51 @@ line-height: 31px;
1856
1874
  color: #0E2238;
1857
1875
  white-space: nowrap;
1858
1876
  }
1877
+ /* Figma 64860-62117 (Kaufen only): ground-truth design-context pull ("uvp" node) — flex column
1878
+ (badge row stacked above the struck price, gap 1px), Arimo/700/15px/normal line-height/0
1879
+ letter-spacing/native line-through, color var(--semantic-error, #ba1a1a) — replaces the shared
1880
+ Arial/400/20px dark .cf-rate-old treatment financing/leasing's own isStrike price keeps
1881
+ untouched below. 2-class selector scopes this to .cf-rate-old--buy only. */
1882
+ .md3 .cf-rate-old.cf-rate-old--buy {
1883
+ display: flex;
1884
+ flex-direction: column;
1885
+ align-items: flex-start;
1886
+ gap: 1px;
1887
+ font-family: 'Arimo';
1888
+ font-weight: 700;
1889
+ font-size: 15px;
1890
+ line-height: normal;
1891
+ letter-spacing: 0;
1892
+ color: #BA1A1A;
1893
+ }
1894
+ /* line-through lives on .cf-rate-old-text (the price span) only, not the flex container above —
1895
+ a descendant's own text-decoration:none is NOT reliable for "undoing" an ancestor's decoration
1896
+ line (well-known CSS quirk: the line is drawn across the ancestor's whole inline content
1897
+ regardless), which was still drawing the strike through the badge's "2" one level up. */
1898
+ .md3 .cf-panel .cf-rate-old--buy .cf-rate-old-text {
1899
+ text-decoration: line-through;
1900
+ }
1901
+ /* replaces the custom red diagonal bar for Kaufen only — financing/leasing's own isStrike price
1902
+ keeps the shared ::after bar (base rule below, untouched). */
1903
+ .md3 .cf-rate-old.cf-rate-old--buy::after {
1904
+ display: none;
1905
+ }
1906
+ /* Same Figma frame ("footnote" node): a plain 13x13 ring in brand/primary, Arimo Regular 8px
1907
+ text-primary (#0e2238) — sits in normal flow as its own row above the struck price (the
1908
+ flex-column .cf-rate-old--buy above), not absolutely overlaid — scoped to .cf-rate-old--buy,
1909
+ financing/leasing's isStrike badge keeps the shared corner-overlay .pp-eyebrow-badge rule
1910
+ (top-right, bigger ring) untouched below. */
1911
+ .md3 .cf-panel .cf-rate-old--buy .pp-eyebrow-badge {
1912
+ position: static;
1913
+ width: 13px;
1914
+ height: 13px;
1915
+ font-family: 'Arimo';
1916
+ font-weight: 400;
1917
+ font-size: 8px;
1918
+ line-height: normal;
1919
+ border-width: 1px;
1920
+ color: #0E2238;
1921
+ }
1859
1922
  /* must stay AFTER the base .cf-rate-amount/.cf-rate-old rules above — equal specificity means
1860
1923
  source order decides the tie (same rule as the .co-form media query elsewhere in this file). */
1861
1924
  @media (max-width: 600px) {
@@ -1968,6 +2031,20 @@ line-height: 31px;
1968
2031
  gap: 6px;
1969
2032
  }
1970
2033
  }
2034
+ /* Figma 64860-62117 (Kaufen only): ground-truth "hero" node is a plain gap-[10px] row (price
2035
+ block + rating column packed together on the left), not the shared space-between push-to-
2036
+ far-edge financing/leasing's own hero row keeps (base rule above, untouched) — the 10px gap
2037
+ itself lives on .fin-eval-col--buy's margin-left just below, so this only cancels the
2038
+ space-between/gap-16 that would otherwise ADD to it. 2-class selector scopes to
2039
+ .cf-rate-top--buy; applies at every width (.fin-eval-col--buy's margin-left is the only
2040
+ intended gap, mobile included). */
2041
+ .md3 .cf-rate-top.cf-rate-top--buy {
2042
+ justify-content: flex-start;
2043
+ gap: 0;
2044
+ }
2045
+ .md3 .cf-panel .fin-eval-col.fin-eval-col--buy {
2046
+ margin-left: 10px;
2047
+ }
1971
2048
  .md3 .cf-rate-left {
1972
2049
  display: flex;
1973
2050
  flex-direction: column;
@@ -1982,13 +2059,15 @@ line-height: 31px;
1982
2059
  line-height: 15px;
1983
2060
  color: #0E2238;
1984
2061
  }
1985
- /* Kaufen's compact "Überweisung · Sofort verfügbar" recap — mobile only (show-mobile, see JSX);
1986
- desktop already has the same two facts as separate Zahlung/Verfügbarkeit rows further down
1987
- (.cf-buy-info), so this would just duplicate them there. */
1988
- @media (max-width: 600px) {
1989
- .md3 .cf-panel .co-aside-cond.co-aside-cond--buy {
1990
- margin-top: 5px;
1991
- }
2062
+ /* Kaufen's compact "Überweisung · sofort verfügbar" recap — Figma 64860-62117 renders it
2063
+ unconditionally (desktop AND mobile) right under the price, alongside the separate Zahlung/
2064
+ Zusatzgarantie/Verfügbar rows further down (.cf-buy-info) — previously hidden on desktop
2065
+ (show-mobile) on the wrong assumption that it only duplicated those rows; removed from JSX.
2066
+ 5px (ground-truth "Rate" column gap) replaces the shared 10px .co-aside-cond gap financing/
2067
+ leasing's own cond line keeps (base rule above, untouched) — was mobile-only before; Figma's
2068
+ same frame carries this gap at any width, so it's no longer scoped to the ≤600px query. */
2069
+ .md3 .cf-panel .co-aside-cond.co-aside-cond--buy {
2070
+ margin-top: 5px;
1992
2071
  }
1993
2072
  /* .fin-eval-col's shared base rule is `display: contents` (a CSS-grid trick that relies on a
1994
2073
  `.fin-price-grid` grid ancestor with named areas — cf-panel isn't a grid), so it needs the
@@ -1999,6 +2078,10 @@ line-height: 31px;
1999
2078
  gap: 8px;
2000
2079
  flex: none;
2001
2080
  align-items: stretch;
2081
+ /* Figma 64860-62117: fixed 106px column so the pill (below, display:flex + align-items:
2082
+ stretch cascading down) and the bar (already width:100%) both span the same width instead
2083
+ of shrink-wrapping to whichever tier label happens to be widest. */
2084
+ width: 106px;
2002
2085
  }
2003
2086
  @media (max-width: 600px) {
2004
2087
  .md3 .cf-panel .fin-eval-col {
@@ -2201,6 +2284,31 @@ line-height: 31px;
2201
2284
  width: 40px;
2202
2285
  height: 40px;
2203
2286
  }
2287
+ /* Rundum-Sorglos-Paket title "i" popover — same reasoning/pattern as the .cf-toggle override
2288
+ above (inherited rule is tuned for the small UVP-Rate footnote badge, top:16px/left:0/
2289
+ width:260px): left:0 was relative to the title's own 20px padding box, bleeding the popover
2290
+ past the card's left border. left:20px nudges it to align with the title's own content edge
2291
+ instead (close to the original 260px width — a full left:0/right:0/width:auto span was tried
2292
+ and was too wide/plain). right:20px + max-width is the mobile safety net: on a narrow card it
2293
+ shrinks instead of overflowing; on a normal-width card max-width wins and the box just sits
2294
+ 280px wide, anchored at left:20px (CSS over-constrained resolution recalculates the unused
2295
+ "right" rather than stretching). 3 class-selectors deep so it always wins over the 2-deep base
2296
+ rule regardless of source order. */
2297
+ .md3 .cf-panel .co-card-title .pp-uvp-tooltip {
2298
+ top: 28px;
2299
+ left: 40px;
2300
+ right: 20px;
2301
+ width: auto;
2302
+ max-width: 280px;
2303
+ }
2304
+ /* same box, narrower safety margin so the 280px cap still comfortably fits a phone-width card
2305
+ instead of being cut flush against the right edge. */
2306
+ @media (max-width: 600px) {
2307
+ .md3 .cf-panel .co-card-title .pp-uvp-tooltip {
2308
+ left: 20px;
2309
+ right: 12px;
2310
+ }
2311
+ }
2204
2312
  .md3 .cf-toggle .md-title-sm {
2205
2313
  display: inline-flex;
2206
2314
  align-items: center;
@@ -2212,6 +2320,14 @@ line-height: 31px;
2212
2320
  line-height: 15px;
2213
2321
  color: #1A1B22;
2214
2322
  }
2323
+ /* Rundum-Sorglos-Paket's toggle label should read the same size as the Zahlung/Zusatzgarantie/
2324
+ Verfügbarkeit rows above it (.cf-buy-info .calcrow-label, 15px/17px) — .cf-panel scopes this to
2325
+ CheckoutFinancePanelNext only, leaving AdditionalServicesNext's unrelated .co-rundum-footer
2326
+ .cf-toggle (step 4) at its own 13px untouched. */
2327
+ .md3 .cf-panel .cf-toggle .md-title-sm {
2328
+ font-size: 15px;
2329
+ line-height: 17px;
2330
+ }
2215
2331
  .md3 .cf-toggle .md-title-sm svg {
2216
2332
  cursor: pointer;
2217
2333
  }
@@ -2515,6 +2631,12 @@ line-height: 31px;
2515
2631
  padding: 14px 20px;
2516
2632
  border-bottom: 1px solid #C5C6D2;
2517
2633
  }
2634
+ /* Kaufen: .co-rundum-footer (the "in die Rate einrechnen" toggle) isn't rendered at all, so
2635
+ .co-rundum-box is the card's last element — the border-bottom above would just be a dangling
2636
+ line with nothing below it. */
2637
+ .md3 .cf-panel .co-rundum-box.co-rundum-box--buy {
2638
+ border-bottom: 0;
2639
+ }
2518
2640
  .md3 .cf-panel .co-rundum-footer {
2519
2641
  padding: 14px 20px;
2520
2642
  }
@@ -4565,6 +4687,23 @@ line-height: 31px;
4565
4687
  justify-content: space-between;
4566
4688
  gap: 10px;
4567
4689
  }
4690
+ /* "2. Rechnungsadresse" / "3. Lieferadresse" (.oo-addr-grid's two cards only — the shared 10px
4691
+ above still fits "Lieferung & Services"/Zahldetails fine, which have a whole row to themselves)
4692
+ — the base gap left too little room for "Rechnungsadresse" next to "Ändern" in the half-width
4693
+ grid column, wrapping the title to 2 lines. align-items: center (inherited from the base rule
4694
+ above) only centers against the FULL flex-row height, so while wrapped it centered "Ändern"
4695
+ against both title lines instead of matching the first one. */
4696
+ .md3 .oo-addr-grid .oo-card-title--action {
4697
+ gap: 4px;
4698
+ }
4699
+ /* The button has no explicit line-height, so the UA default ("normal", ~1.15-1.2x its 13px font)
4700
+ gives it a taller box than its own glyphs — align-items: center then centers that whole taller
4701
+ box against the title's 23px line-height box, landing "Ändern"'s visible text a bit low. line-
4702
+ height: 1 collapses the button's box to its font-size itself, so both sides center on their
4703
+ actual text instead of on mismatched leading. */
4704
+ .md3 .oo-addr-grid .oo-card-action {
4705
+ line-height: 1;
4706
+ }
4568
4707
  .md3 .oo-card-action {
4569
4708
  font-family: Arial;
4570
4709
  font-weight: 700;
@@ -4677,6 +4816,49 @@ line-height: 31px;
4677
4816
  border-left: 0 !important;
4678
4817
  }
4679
4818
  }
4819
+ /* "2 Jahre Garantie" "i" popover (OrderOverviewNext.tsx) — same AddServicesTooltipBody/
4820
+ pp-uvp-tooltip markup as step 3's title info trigger, but there's no .cf-panel ancestor here to
4821
+ inherit the shared "grey card" look from, so the full style (not just a position override) is
4822
+ defined fresh under .oo-ls-title-row. Anchored right:0 (the garantie cell is the row's last/
4823
+ rightmost of 3) so the box opens leftward instead of overflowing past the card's right edge;
4824
+ max-width keeps it inside a narrow phone screen once .oo-ls-row stacks to full width below. */
4825
+ .md3 .oo-ls-title-row .pp-uvp-tooltip {
4826
+ position: absolute;
4827
+ top: 24px;
4828
+ left: auto;
4829
+ right: 0;
4830
+ z-index: 20;
4831
+ width: 300px;
4832
+ max-width: calc(100vw - 48px);
4833
+ padding: 10px 12px;
4834
+ border-radius: 15px;
4835
+ background: #c5c6d2;
4836
+ box-shadow: var(--md-e2, 0 4px 12px rgba(0, 0, 0, 0.15));
4837
+ white-space: normal;
4838
+ cursor: default;
4839
+ }
4840
+ .md3 .oo-ls-title-row .pp-uvp-tooltip-close {
4841
+ position: absolute;
4842
+ top: -14px;
4843
+ right: -14px;
4844
+ display: flex;
4845
+ align-items: center;
4846
+ justify-content: center;
4847
+ width: 26px;
4848
+ height: 26px;
4849
+ border-radius: 50%;
4850
+ background: rgba(11, 19, 32, 0.55);
4851
+ border: 1.5px solid rgba(255, 255, 255, 0.25);
4852
+ padding: 0;
4853
+ cursor: pointer;
4854
+ }
4855
+ .md3 .oo-ls-title-row .pp-uvp-tooltip-body {
4856
+ font-family: 'Arimo';
4857
+ font-weight: 400;
4858
+ font-size: 11px;
4859
+ line-height: 13px;
4860
+ color: #0e2238;
4861
+ }
4680
4862
  /* "5. Monatlich zu zahlen" / "5. Gesamtpreis" (Figma: Finanzierung 64763:107263, Leasing
4681
4863
  64794:110315, Kaufen 64794:109673) — own "oo-pay-*" namespace, NOT the shared
4682
4864
  .cf-rate-row/.fin-eval-* classes CheckoutFinancePanelNext.tsx (step 3) uses: that component
@@ -4700,12 +4882,55 @@ line-height: 31px;
4700
4882
  width: 100%;
4701
4883
  }
4702
4884
  .md3 .oo-pay-price-row {
4885
+ position: relative;
4703
4886
  display: flex;
4704
4887
  align-items: center;
4705
4888
  justify-content: space-between;
4706
4889
  gap: 16px;
4707
4890
  width: 100%;
4708
4891
  }
4892
+ /* UVP footnote popover (both the Buy "②" and the Financing/Leasing "UVP-Rate ③" branches above —
4893
+ oo-pay-buy-uvp-row and oo-pay-secondary respectively, both descendants of this row) — same
4894
+ "grey card" pp-uvp-tooltip look step 3's .cf-panel scope and order-overview's own
4895
+ .oo-ls-title-row scope already give it, but nothing scoped it under this card's own oo-pay-*
4896
+ namespace, so it rendered as plain unstyled text. position:relative lives on this shared
4897
+ ancestor (not each branch's own element) since one rule here covers both. */
4898
+ .md3 .oo-pay-price-row .pp-uvp-tooltip {
4899
+ position: absolute;
4900
+ top: 28px;
4901
+ left: 0;
4902
+ z-index: 20;
4903
+ width: 300px;
4904
+ max-width: calc(100vw - 48px);
4905
+ padding: 10px 12px;
4906
+ border-radius: 15px;
4907
+ background: #c5c6d2;
4908
+ box-shadow: var(--md-e2, 0 4px 12px rgba(0, 0, 0, 0.15));
4909
+ white-space: normal;
4910
+ cursor: default;
4911
+ }
4912
+ .md3 .oo-pay-price-row .pp-uvp-tooltip-close {
4913
+ position: absolute;
4914
+ top: -14px;
4915
+ right: -14px;
4916
+ display: flex;
4917
+ align-items: center;
4918
+ justify-content: center;
4919
+ width: 26px;
4920
+ height: 26px;
4921
+ border-radius: 50%;
4922
+ background: rgba(11, 19, 32, 0.55);
4923
+ border: 1.5px solid rgba(255, 255, 255, 0.25);
4924
+ padding: 0;
4925
+ cursor: pointer;
4926
+ }
4927
+ .md3 .oo-pay-price-row .pp-uvp-tooltip-body {
4928
+ font-family: 'Arimo';
4929
+ font-weight: 400;
4930
+ font-size: 11px;
4931
+ line-height: 13px;
4932
+ color: #0e2238;
4933
+ }
4709
4934
  .md3 .oo-pay-left {
4710
4935
  display: flex;
4711
4936
  align-items: center;
@@ -4835,9 +5060,13 @@ line-height: 31px;
4835
5060
  align-items: flex-end;
4836
5061
  gap: 2px;
4837
5062
  }
5063
+ /* Figma 64794-109673 "secondaryStack": items-start, not center — the footnote badge (now a
5064
+ direct sibling of .oo-pay-uvp-amount, moved out so the struck price's own ::after bar doesn't
5065
+ draw its strike across the badge too) sits top-aligned/raised next to the price instead of
5066
+ vertically centered against it. */
4838
5067
  .md3 .oo-pay-buy-uvp-row {
4839
5068
  display: flex;
4840
- align-items: center;
5069
+ align-items: flex-start;
4841
5070
  gap: 2px;
4842
5071
  }
4843
5072
  .md3 .oo-pay-buy-leftgroup {
@@ -165,10 +165,7 @@ const getDecoratedLightProps = (
165
165
  if (hybridPlugin) {
166
166
  if (wltpWeightedPowerCombinedContent && wltpWeightedCombinedContent) {
167
167
  const dischargedCombined = Number.isFinite(wltpConsumptionCombined) ? wltpConsumptionCombined : 19.9;
168
- // ADV-187: the discharged class is derived server-side from wltpCo2Discharged; when the
169
- // backend could not derive it the field is selector_unknown — hide the segment, never
170
- // fall back to a hard-coded class here.
171
- const dischargedClass = car.environmentEmissions.wltpDischargedEnergyEfficiencyClass;
168
+ const dischargedClass = car.environmentEmissions.wltpDischargedEnergyEfficiencyClass || 'selector_energyEfficiencyClass_g';
172
169
  const weightedCombinedLabeled = t('vehicleProps:value.wltpWeightedCombinedLabel', { consumption: wltpWeightedCombined.toLocaleString(language), unit: translatedUnit });
173
170
  const dischargedCombinedLabeled = t('vehicleProps:value.wltpDischargedCombinedLabel', { consumption: dischargedCombined.toLocaleString(language), unit: translatedUnit });
174
171
  const co2WeightedContent = Number.isFinite(wltpCo2)
@@ -177,16 +174,14 @@ const getDecoratedLightProps = (
177
174
  const co2ClassWeighted = isPropDefined(wltpEnergyEfficiencyClass)
178
175
  ? t('vehicleProps:value.co2ClassWeighted', { class: t(`cbd:${wltpEnergyEfficiencyClass}`) })
179
176
  : null;
180
- const co2ClassDischarged = isPropDefined(dischargedClass)
181
- ? t('vehicleProps:value.co2ClassDischarged', { class: t(`cbd:${dischargedClass}`) })
182
- : null;
177
+ const co2ClassDischarged = t('vehicleProps:value.co2ClassDischarged', { class: t(`cbd:${dischargedClass}`) });
183
178
  return <React.Fragment>
184
179
  {`${wltpWeightedPowerCombinedContent} + `}
185
180
  {`${weightedCombinedLabeled}, `}
186
181
  {`${dischargedCombinedLabeled}`}
187
182
  {co2WeightedContent && <>, {co2WeightedContent}</>}
188
183
  {co2ClassWeighted && <>, {co2ClassWeighted}</>}
189
- {co2ClassDischarged && <>, {co2ClassDischarged}</>}
184
+ {<>, {co2ClassDischarged}</>}
190
185
  </React.Fragment>
191
186
  }
192
187
 
@@ -321,10 +316,7 @@ const getDecoratedLightProps = (
321
316
  if (hybridPlugin) {
322
317
  if (wltpWeightedPowerCombinedContent && wltpWeightedCombinedContent) {
323
318
  const dischargedCombined = Number.isFinite(wltpConsumptionCombined) ? wltpConsumptionCombined : 19.9;
324
- // ADV-187: the discharged class is derived server-side from wltpCo2Discharged; when the
325
- // backend could not derive it the field is selector_unknown — hide the segment, never
326
- // fall back to a hard-coded class here.
327
- const dischargedClass = car.environmentEmissions.wltpDischargedEnergyEfficiencyClass;
319
+ const dischargedClass = car.environmentEmissions.wltpDischargedEnergyEfficiencyClass || 'selector_energyEfficiencyClass_g';
328
320
  const weightedCombinedLabeled = t('vehicleProps:value.wltpWeightedCombinedLabel', { consumption: wltpWeightedCombined.toLocaleString(language), unit: translatedUnit });
329
321
  const dischargedCombinedLabeled = t('vehicleProps:value.wltpDischargedCombinedLabel', { consumption: dischargedCombined.toLocaleString(language), unit: translatedUnit });
330
322
  const co2WeightedContent = Number.isFinite(wltpCo2)
@@ -333,16 +325,14 @@ const getDecoratedLightProps = (
333
325
  const co2ClassWeighted = isPropDefined(wltpEnergyEfficiencyClass)
334
326
  ? t('vehicleProps:value.co2ClassWeighted', { class: t(`cbd:${wltpEnergyEfficiencyClass}`) })
335
327
  : null;
336
- const co2ClassDischarged = isPropDefined(dischargedClass)
337
- ? t('vehicleProps:value.co2ClassDischarged', { class: t(`cbd:${dischargedClass}`) })
338
- : null;
328
+ const co2ClassDischarged = t('vehicleProps:value.co2ClassDischarged', { class: t(`cbd:${dischargedClass}`) });
339
329
  return <React.Fragment>
340
330
  {`${wltpWeightedPowerCombinedContent} + `}
341
331
  {`${weightedCombinedLabeled}, `}
342
332
  {`${dischargedCombinedLabeled}`}
343
333
  {co2WeightedContent && <>, {co2WeightedContent}</>}
344
334
  {co2ClassWeighted && <>, {co2ClassWeighted}</>}
345
- {co2ClassDischarged && <>, {co2ClassDischarged}</>}
335
+ {<>, {co2ClassDischarged}</>}
346
336
  </React.Fragment>
347
337
  }
348
338
 
@@ -880,12 +880,10 @@ const getDecoratedProps = (
880
880
  // (entladen, komb) fuel consumption + a discharged CO2 class.
881
881
  if (hybridPlugin && wltpConsumptionWeightedPowerCombinedContent && wltpConsumptionWeightedCombinedContent) {
882
882
  const ADV88_DEFAULT_DISCHARGED_COMBINED = 19.9;
883
+ const ADV88_DEFAULT_DISCHARGED_CO2_CLASS = 'selector_energyEfficiencyClass_g';
883
884
 
884
885
  const dischargedCombined = Number.isFinite(wltpCombined) ? wltpCombined : ADV88_DEFAULT_DISCHARGED_COMBINED;
885
- // ADV-187: the discharged class is derived server-side from wltpCo2Discharged; when the
886
- // backend could not derive it the field is selector_unknown — hide the segment, never
887
- // fall back to a hard-coded class here.
888
- const dischargedClass = car.environmentEmissions.wltpDischargedEnergyEfficiencyClass;
886
+ const dischargedClass = car.environmentEmissions.wltpDischargedEnergyEfficiencyClass || ADV88_DEFAULT_DISCHARGED_CO2_CLASS;
889
887
 
890
888
  const weightedCombinedLabeled = t('vehicleProps:value.wltpWeightedCombinedLabel', { consumption: wltpWeightedCombined.toLocaleString(language), unit: translatedUnit });
891
889
  const dischargedCombinedLabeled = t('vehicleProps:value.wltpDischargedCombinedLabel', { consumption: dischargedCombined.toLocaleString(language), unit: translatedUnit });
@@ -895,9 +893,7 @@ const getDecoratedProps = (
895
893
  const co2ClassWeighted = isPropDefined(wltpEnergyEfficiencyClass)
896
894
  ? t('vehicleProps:value.co2ClassWeighted', { class: t(`cbd:${wltpEnergyEfficiencyClass}`) })
897
895
  : null;
898
- const co2ClassDischarged = isPropDefined(dischargedClass)
899
- ? t('vehicleProps:value.co2ClassDischarged', { class: t(`cbd:${dischargedClass}`) })
900
- : null;
896
+ const co2ClassDischarged = t('vehicleProps:value.co2ClassDischarged', { class: t(`cbd:${dischargedClass}`) });
901
897
 
902
898
  return <React.Fragment>
903
899
  {wrapValue(wltpConsumptionWeightedPowerCombinedContent)}{' + '}
@@ -905,7 +901,7 @@ const getDecoratedProps = (
905
901
  {wrapValue(dischargedCombinedLabeled)}
906
902
  {co2WeightedContent && <>{', '}{wrapValue(co2WeightedContent)}</>}
907
903
  {co2ClassWeighted && <>{', '}{co2ClassWeighted}</>}
908
- {co2ClassDischarged && <>{', '}{co2ClassDischarged}</>}
904
+ {', '}{co2ClassDischarged}
909
905
  </React.Fragment>
910
906
  }
911
907