@bytebrand/fe-ui-core-autobahn 1.0.58 → 1.0.60

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/CarRow.tsx CHANGED
@@ -31,6 +31,8 @@ import {
31
31
  OfferBadge,
32
32
  PriceTabs,
33
33
  PriceHero,
34
+ PricePaths,
35
+ PriceKauf,
34
36
  DropChip,
35
37
  PercentChip,
36
38
  ZeroDownChip,
@@ -104,7 +106,8 @@ const CarRow: React.FunctionComponent<CarRowProps> = ({ car, isFavorite, onToggl
104
106
  <div style={{ display: 'flex', flexWrap: 'nowrap', alignItems: 'center', gap: 5 }}>
105
107
  {car.isNew && <span className="neu-badge">NEU</span>}
106
108
  {car.drop != null ? <DropChip t={t} amount={car.drop} /> : showSave && <SaveChip t={t} amount={car.savings!} />}
107
- <ZeroDownChip t={t} show={car.zeroDown} compact />
109
+ <ZeroDownChip t={t} show={car.zeroDown} compact className="show-mobile" />
110
+ <ZeroDownChip t={t} show={car.zeroDown} className="hide-mobile" />
108
111
  <PercentChip pct={car.dropPct} />
109
112
  </div>
110
113
  <OfferBadge show={showOfferBadge} t={t} />
@@ -121,6 +124,14 @@ const CarRow: React.FunctionComponent<CarRowProps> = ({ car, isFavorite, onToggl
121
124
  >
122
125
  <IconButton name="heart" filled active={isFavorite} onClick={onToggleFavorite} label={t('autobahn:card.action.save')} />
123
126
  </div>
127
+ {/* desktop: heart sits on the photo itself (Figma "mcard"), not next to the title. */}
128
+ <div
129
+ onClick={(e) => e.stopPropagation()}
130
+ className="hide-mobile"
131
+ style={{ position: 'absolute', top: 10, right: 10, zIndex: 3 }}
132
+ >
133
+ <IconButton name="heart" active={isFavorite} onClick={onToggleFavorite} label={t('autobahn:card.action.save')} />
134
+ </div>
124
135
  {/* activity/trust badges — overlaid on the studio-white floor at the bottom of the photo
125
136
  itself (PM ref), not a separate divided strip below it. Bottom-right, raised above the
126
137
  carousel dots and the photo-count badge (positioning lives in .gallery-badges, md3.css). */}
@@ -128,17 +139,12 @@ const CarRow: React.FunctionComponent<CarRowProps> = ({ car, isFavorite, onToggl
128
139
  </div>
129
140
 
130
141
  <div className="listcard-info">
131
- <div style={{ display: 'flex', justifyContent: 'space-between', gap: 12 }}>
132
- <div style={{ minWidth: 0 }}>
133
- <div className="md-title" style={{ fontSize: 18, lineHeight: 1.2 }}>
134
- {car.make} {car.model}
135
- </div>
136
- <div className="md-body-sm" style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
137
- {car.trim}
138
- </div>
142
+ <div style={{ minWidth: 0 }}>
143
+ <div className="md-title" style={{ fontSize: 18, lineHeight: 1.2 }}>
144
+ {car.make} {car.model}
139
145
  </div>
140
- <div onClick={(e) => e.stopPropagation()} className="hide-mobile">
141
- <IconButton name="heart" active={isFavorite} onClick={onToggleFavorite} label={t('autobahn:card.action.save')} />
146
+ <div className="md-body-sm" style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
147
+ {car.trim}
142
148
  </div>
143
149
  </div>
144
150
  <div className="specstrip">
@@ -160,12 +166,13 @@ const CarRow: React.FunctionComponent<CarRowProps> = ({ car, isFavorite, onToggl
160
166
  <EcoLine t={t} car={car} />
161
167
  </div>
162
168
 
163
- {/* price column: Finanzierung/Leasing/Kaufen tabs pick ONE hero price (same tabbed
164
- component the grid CarCard uses), then the fixed Preisbewertung meter and the CTA,
165
- anchored to the bottom (margin-top:auto) so the button sits flush at the foot. */}
169
+ {/* price column — mobile keeps the Finanzierung/Leasing/Kaufen tab switcher (ONE hero price
170
+ at a time); desktop shows all three payment paths stacked at once (Figma "mcard"), via
171
+ the previously-unused PricePaths/PriceKauf pair. Both variants share the same
172
+ Preisbewertung meter + CTA, anchored to the bottom (margin-top:auto on .pp-purchase). */}
166
173
  <div className="listcard-price">
167
- <PriceTabs car={car} mode={mode} onModeChange={setMode} t={t} />
168
- <div className="pp-purchase">
174
+ <PriceTabs car={car} mode={mode} onModeChange={setMode} t={t} className="show-mobile" />
175
+ <div className="pp-purchase show-mobile">
169
176
  <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
170
177
  <PriceHero car={car} mode={mode} t={t} />
171
178
  {car.rating && (
@@ -204,6 +211,49 @@ const CarRow: React.FunctionComponent<CarRowProps> = ({ car, isFavorite, onToggl
204
211
  )}
205
212
  <PpActivity t={t} car={car} />
206
213
  </div>
214
+
215
+ <PricePaths car={car} t={t} className="hide-mobile" />
216
+ <div className="pp-purchase hide-mobile">
217
+ <PriceKauf car={car} t={t} />
218
+ <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
219
+ {car.rating && (
220
+ <StatChip tone={ratingTone(car.rating)} icon={ratingTone(car.rating) === 'good' ? 'check' : 'minus'}>
221
+ {t('autobahn:tier.' + car.rating)}
222
+ </StatChip>
223
+ )}
224
+ <div style={{ flex: 1 }}>
225
+ <PriceMeter t={t} car={car} />
226
+ </div>
227
+ </div>
228
+ <div className="pp-cta">
229
+ <Button
230
+ variant="filled"
231
+ block
232
+ iconRight="arrow-right"
233
+ onClick={(e) => {
234
+ e.stopPropagation();
235
+ onOpen(car);
236
+ }}
237
+ >
238
+ {t('autobahn:card.cta.viewOffer')}
239
+ </Button>
240
+ </div>
241
+ {adminAction && (
242
+ <div className="pp-cta">
243
+ <Button
244
+ variant="outlined"
245
+ block
246
+ onClick={(e) => {
247
+ e.stopPropagation();
248
+ adminAction.onClick();
249
+ }}
250
+ >
251
+ {adminAction.label}
252
+ </Button>
253
+ </div>
254
+ )}
255
+ <PpActivity t={t} car={car} />
256
+ </div>
207
257
  </div>
208
258
  </article>
209
259
  );
@@ -234,14 +234,18 @@ export const PercentChip: React.FunctionComponent<{ pct?: number }> = ({ pct })
234
234
  * there is room (grid cards / wide image) and uses the compact "0 € Anz." on the narrow list-card
235
235
  * image, so the badge row always stays on ONE line on both desktop and mobile.
236
236
  */
237
- export const ZeroDownChip: React.FunctionComponent<{ show?: boolean; compact?: boolean; t: TranslationFunction }> = ({
238
- show,
239
- compact,
240
- t,
241
- }) => {
237
+ export const ZeroDownChip: React.FunctionComponent<{
238
+ show?: boolean;
239
+ compact?: boolean;
240
+ t: TranslationFunction;
241
+ className?: string;
242
+ }> = ({ show, compact, t, className }) => {
242
243
  if (!show) return null;
243
244
  return (
244
- <span style={{ ...dealPill, background: 'var(--md-accent-container)', color: 'var(--md-on-accent-container)' }}>
245
+ <span
246
+ className={className}
247
+ style={{ ...dealPill, background: 'var(--md-accent-container)', color: 'var(--md-on-accent-container)' }}
248
+ >
245
249
  {compact ? t('autobahn:card.price.zeroDownCompact') : t('autobahn:card.price.zeroDown')}
246
250
  </span>
247
251
  );
@@ -341,6 +345,8 @@ export const EcoLine: React.FunctionComponent<EcoLineProps> = ({ car, style, t }
341
345
  export interface PricePathsProps {
342
346
  car: AutobahnCar;
343
347
  t: TranslationFunction;
348
+ /** extra class appended to the root (e.g. the SRP card's desktop-only `hide-mobile` marker). */
349
+ className?: string;
344
350
  }
345
351
 
346
352
  /** Map the AutobahnCar rating tone → meter segment index (0 best … 4 worst). */
@@ -379,6 +385,8 @@ export interface PriceTabsProps {
379
385
  mode: PaymentMode;
380
386
  onModeChange: (mode: PaymentMode) => void;
381
387
  t: TranslationFunction;
388
+ /** extra class appended to the root (e.g. the SRP card's mobile-only `show-mobile` marker). */
389
+ className?: string;
382
390
  }
383
391
 
384
392
  /**
@@ -387,14 +395,14 @@ export interface PriceTabsProps {
387
395
  * identical to the VDP's, not a second tab style. Leasing tab omitted when the car isn't
388
396
  * leasing-eligible (car.lease undefined) — same rule PricePaths already used.
389
397
  */
390
- export const PriceTabs: React.FunctionComponent<PriceTabsProps> = ({ car, mode, onModeChange, t }) => {
398
+ export const PriceTabs: React.FunctionComponent<PriceTabsProps> = ({ car, mode, onModeChange, t, className }) => {
391
399
  const tabs: Array<[PaymentMode, string]> = [
392
400
  ['financing', t('autobahn:finance.mode.financing')],
393
401
  ...(car.lease != null ? ([['leasing', t('autobahn:finance.mode.leasing')]] as Array<[PaymentMode, string]>) : []),
394
402
  ['buy', t('autobahn:card.price.buy')],
395
403
  ];
396
404
  return (
397
- <div className="fin-tabs" role="tablist" onClick={(e) => e.stopPropagation()}>
405
+ <div className={'fin-tabs' + (className ? ' ' + className : '')} role="tablist" onClick={(e) => e.stopPropagation()}>
398
406
  {tabs.map(([m, label]) => (
399
407
  <button
400
408
  key={m}
@@ -589,7 +597,7 @@ const PpRow: React.FunctionComponent<PpRowProps> = ({ label, value, unit, old, f
589
597
  * is omitted entirely when leasing isn't active. The Kaufen row is rendered separately
590
598
  * (PriceKauf) below a divider so only the financing rate stands out — no wall of blue prices.
591
599
  */
592
- export const PricePaths: React.FunctionComponent<PricePathsProps> = ({ car, t }) => {
600
+ export const PricePaths: React.FunctionComponent<PricePathsProps> = ({ car, t, className }) => {
593
601
  const hasLease = car.lease != null;
594
602
  // Terms of the config the displayed rate was actually computed with (per-car:
595
603
  // dealer rate + the user's saved localStorage adjustments) — default config
@@ -601,20 +609,34 @@ export const PricePaths: React.FunctionComponent<PricePathsProps> = ({ car, t })
601
609
  const leasMonths = car.leaseMonths ?? _leasPeriod;
602
610
  const hasLeasTerms = Boolean(leasMileage && leasMonths);
603
611
  const leasMileageStr = leasMileage ? Number(leasMileage).toLocaleString('de-DE') : '';
612
+ // same real struck-through reference rate PriceHero shows for the financing tab (oldMonthly) —
613
+ // shown here too now that both rows render at once instead of behind a tab.
614
+ const showOldMonthly = car.oldMonthly != null && car.oldMonthly > car.monthly;
604
615
  return (
605
- <div className="pricepaths">
616
+ <div className={'pricepaths' + (className ? ' ' + className : '')}>
606
617
  <div className="pp-hero">
607
618
  <div className="pp-eyebrow">{t('autobahn:card.price.financingFrom')}</div>
608
619
  <div className="pp-hero-rate">
609
- <b className="num pp-hero-val">{fmtRate(car.monthly)} €</b>
610
- <span className="pp-hero-unit">{t('autobahn:card.price.perMonth')}</span>
620
+ {showOldMonthly && (
621
+ <span className="pp-hero-old">
622
+ {(car.oldMonthly as number).toLocaleString('de-DE')} €<sup>2</sup>
623
+ </span>
624
+ )}
625
+ <b className="num pp-hero-val">
626
+ {fmtRate(car.monthly)} €<sup>1</sup>
627
+ </b>
628
+ <span className="pp-hero-unit">
629
+ {t('autobahn:card.price.perMonth')}
630
+ <sup>3</sup>
631
+ </span>
611
632
  </div>
612
633
  <div className="pp-hero-terms">
613
634
  {t('autobahn:card.price.financeTerms', {
614
635
  firstInstallment: finFirstInstallment,
615
636
  months: finMonths,
616
637
  count: finMonths,
617
- })}
638
+ })}{' '}
639
+ · <span className="pp-details-link">{t('autobahn:card.price.details')}</span>
618
640
  </div>
619
641
  </div>
620
642
  {hasLease && (
@@ -622,7 +644,8 @@ export const PricePaths: React.FunctionComponent<PricePathsProps> = ({ car, t })
622
644
  <PpRow label={t('autobahn:card.price.leasing')} value={car.lease as number} unit="mtl." />
623
645
  {hasLeasTerms && (
624
646
  <div className="pp-sub-terms">
625
- {t('autobahn:card.price.leaseTerms', { mileage: leasMileageStr, months: leasMonths })}
647
+ {t('autobahn:card.price.leaseTerms', { mileage: leasMileageStr, months: leasMonths })}{' '}
648
+ · <span className="pp-details-link">{t('autobahn:card.price.details')}</span>
626
649
  </div>
627
650
  )}
628
651
  </div>
package/chrome/TopBar.tsx CHANGED
@@ -466,8 +466,8 @@ const TopBar: React.FunctionComponent<TopBarProps> = ({
466
466
  }}
467
467
  >
468
468
  <div
469
- className="container"
470
- style={{ height: 64, display: 'flex', alignItems: 'center', gap: 16, padding: '0 16px' }}
469
+ className="container topbar-container"
470
+ style={{ height: 64, display: 'flex', alignItems: 'center', gap: 16 }}
471
471
  >
472
472
  {/* mobile hamburger */}
473
473
  <button
@@ -481,7 +481,7 @@ const TopBar: React.FunctionComponent<TopBarProps> = ({
481
481
 
482
482
  {/* full navigation — '/' belongs to WordPress behind traefik, not the SPA (see HOME_PATH). */}
483
483
  <a href={HOME_PATH} style={{ flex: 'none', display: 'inline-flex' }}>
484
- <img src={logoUrl} height="32" style={{ cursor: 'pointer', display: 'block' }} alt="auto.de" />
484
+ <img src={logoUrl} height="32" className="topbar-logo" style={{ cursor: 'pointer', display: 'block' }} alt="auto.de" />
485
485
  </a>
486
486
 
487
487
  {renderNav('hide-mobile', {
@@ -493,64 +493,68 @@ const TopBar: React.FunctionComponent<TopBarProps> = ({
493
493
 
494
494
  <div style={{ flex: 1 }} />
495
495
 
496
- <IconButton name="search" label={t('autobahn:chrome.search')} onClick={goSearch} />
497
- {/* Merkliste → /favourites with a real count badge — desktop-only now; the mobile row
498
- below shows it ONLY for the 'browsing' variant instead. */}
499
- <span className="hide-mobile" style={{ position: 'relative', display: 'inline-flex' }}>
500
- <IconButton name="heart" label={t('autobahn:chrome.favorites')} onClick={() => goRoute(R_FAVOURITES)} />
501
- {favoritesCount > 0 && (
502
- <span style={badgeStyle}>{favoritesCount}</span>
503
- )}
504
- </span>
505
- {/* Suchaufträge → /my-searches (saved searches list) — desktop-only; no Figma mobile
506
- header variant includes a bell icon. */}
507
- <span className="hide-mobile" style={{ position: 'relative', display: 'inline-flex' }}>
508
- <IconButton name="bell" label={t('autobahn:chrome.savedSearches')} onClick={() => goRoute(R_SAVED_SEARCHES)} />
509
- {notificationCount > 0 && (
510
- <span style={badgeStyle}>{notificationCount}</span>
511
- )}
512
- </span>
513
-
514
- {/* Mobile-only reduced icon set — which ONE extra icon (besides search+account, both
515
- always shown) appears depends on `variant`, per the 3 Figma mobile header specs. */}
516
- {variant === 'browsing' && (
517
- <span className="show-mobile" style={{ position: 'relative', display: 'inline-flex' }}>
496
+ {/* Action icons cluster — its own (smaller) gap, separate from the outer 16px gap between
497
+ hamburger/logo/nav: the Header-System spec calls for ~2-6px between actions but keeps
498
+ the wider spacing everywhere else. */}
499
+ <div className="topbar-actions" style={{ display: 'flex', alignItems: 'center' }}>
500
+ <IconButton name="search" label={t('autobahn:chrome.search')} onClick={goSearch} />
501
+ {/* Merkliste → /favourites with a real count badge — desktop-only now; the mobile row
502
+ below shows it ONLY for the 'browsing' variant instead. */}
503
+ <span className="hide-mobile" style={{ position: 'relative', display: 'inline-flex' }}>
518
504
  <IconButton name="heart" label={t('autobahn:chrome.favorites')} onClick={() => goRoute(R_FAVOURITES)} />
519
- {favoritesCount > 0 && <span style={badgeStyle}>{favoritesCount}</span>}
505
+ {favoritesCount > 0 && (
506
+ <span style={badgeStyle}>{favoritesCount}</span>
507
+ )}
520
508
  </span>
521
- )}
522
- {variant === 'conversion' && (
523
- <span className="show-mobile" style={{ position: 'relative', display: 'inline-flex' }}>
524
- <IconButton name="phone" label={t('autobahn:chrome.callUs')} onClick={() => goNav({ href: phoneHref })} />
509
+ {/* Suchaufträge → /my-searches (saved searches list) — desktop-only; no Figma mobile
510
+ header variant includes a bell icon. */}
511
+ <span className="hide-mobile" style={{ position: 'relative', display: 'inline-flex' }}>
512
+ <IconButton name="bell" label={t('autobahn:chrome.savedSearches')} onClick={() => goRoute(R_SAVED_SEARCHES)} />
513
+ {notificationCount > 0 && (
514
+ <span style={badgeStyle}>{notificationCount}</span>
515
+ )}
525
516
  </span>
526
- )}
527
517
 
528
- {/* language segmented control — hidden ≤600px (the account menu carries language rows). */}
529
- {onLangSelect && (
530
- <div className="lang-seg hide-mobile" aria-label={t('autobahn:chrome.switchLanguage')}>
531
- <button type="button" className={'lang-seg-btn' + (language !== 'en' ? ' on' : '')} onClick={() => onLangSelect('de')}>DE</button>
532
- <button type="button" className={'lang-seg-btn' + (language === 'en' ? ' on' : '')} onClick={() => onLangSelect('en')}>EN</button>
533
- </div>
534
- )}
518
+ {/* Mobile-only reduced icon set — which ONE extra icon (besides search+account, both
519
+ always shown) appears depends on `variant`, per the 3 Figma mobile header specs. */}
520
+ {variant === 'browsing' && (
521
+ <span className="show-mobile" style={{ position: 'relative', display: 'inline-flex' }}>
522
+ <IconButton name="heart" label={t('autobahn:chrome.favorites')} onClick={() => goRoute(R_FAVOURITES)} />
523
+ {favoritesCount > 0 && <span style={badgeStyle}>{favoritesCount}</span>}
524
+ </span>
525
+ )}
526
+ {variant === 'conversion' && (
527
+ <span className="show-mobile" style={{ position: 'relative', display: 'inline-flex' }}>
528
+ <IconButton name="phone" label={t('autobahn:chrome.callUs')} onClick={() => goNav({ href: phoneHref })} />
529
+ </span>
530
+ )}
535
531
 
536
- {/* Konto trigger + account layer — visible at EVERY width: this menu is the only place
537
- sign-out / settings / dealer switching exist, so it must never be display:none'd (the
538
- pre-cutover hide-tablet left 601–980px with no logout at all). Desktop keeps the
539
- tonal "Konto" pill; ≤600px it collapses to a plain icon (topbar-account-btn CSS) and
540
- opens the full-width mobile Account Layer instead of the small popover. */}
541
- <span style={{ position: 'relative', display: 'inline-flex' }}>
542
- <button
543
- className="mbtn mbtn--tonal mbtn--sm topbar-account-btn"
544
- style={{ marginLeft: 4 }}
545
- onClick={() => setAccount((a) => !a)}
546
- aria-expanded={account}
547
- aria-label={t('autobahn:chrome.account')}
548
- >
549
- <Icon name="user" size={18} />
550
- <span className="hide-mobile">{t('autobahn:chrome.account')}</span>
551
- </button>
552
- {account && renderAccountMenu()}
553
- </span>
532
+ {/* language segmented control — hidden ≤600px (the account menu carries language rows). */}
533
+ {onLangSelect && (
534
+ <div className="lang-seg hide-mobile" aria-label={t('autobahn:chrome.switchLanguage')}>
535
+ <button type="button" className={'lang-seg-btn' + (language !== 'en' ? ' on' : '')} onClick={() => onLangSelect('de')}>DE</button>
536
+ <button type="button" className={'lang-seg-btn' + (language === 'en' ? ' on' : '')} onClick={() => onLangSelect('en')}>EN</button>
537
+ </div>
538
+ )}
539
+
540
+ {/* Konto trigger + account layer — visible at EVERY width: this menu is the only place
541
+ sign-out / settings / dealer switching exist, so it must never be display:none'd (the
542
+ pre-cutover hide-tablet left 601–980px with no logout at all). Desktop keeps the
543
+ tonal "Konto" pill; ≤600px it collapses to a plain icon (topbar-account-btn CSS) and
544
+ opens the full-width mobile Account Layer instead of the small popover. */}
545
+ <span style={{ position: 'relative', display: 'inline-flex' }}>
546
+ <button
547
+ className="mbtn mbtn--tonal mbtn--sm topbar-account-btn"
548
+ onClick={() => setAccount((a) => !a)}
549
+ aria-expanded={account}
550
+ aria-label={t('autobahn:chrome.account')}
551
+ >
552
+ <Icon name="user" size={18} />
553
+ <span className="hide-mobile">{t('autobahn:chrome.account')}</span>
554
+ </button>
555
+ {account && renderAccountMenu()}
556
+ </span>
557
+ </div>
554
558
  </div>
555
559
 
556
560
  {account && renderMobileAccountLayer()}
@@ -22,7 +22,8 @@
22
22
  "perMonth": "/mtl.",
23
23
  "leasing": "Leasing",
24
24
  "leasingFrom": "Leasing ab",
25
- "buy": "Kaufen"
25
+ "buy": "Kaufen",
26
+ "details": "Details"
26
27
  },
27
28
  "eco": {
28
29
  "phevConsumptionLine": "+ {{consFuel}} (gew., komb.) · {{consDis}} (entl.) · CO₂ {{co2w}} · Klasse {{co2classW}} (gew.) / {{co2classD}} (entl.)*",
@@ -22,7 +22,8 @@
22
22
  "perMonth": "/mo.",
23
23
  "leasing": "Leasing",
24
24
  "leasingFrom": "Leasing from",
25
- "buy": "Buy"
25
+ "buy": "Buy",
26
+ "details": "Details"
26
27
  },
27
28
  "eco": {
28
29
  "phevConsumptionLine": "+ {{consFuel}} (weighted, combined) · {{consDis}} (discharged) · CO₂ {{co2w}} · Class {{co2classW}} (weighted) / {{co2classD}} (discharged)*",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bytebrand/fe-ui-core-autobahn",
3
- "version": "1.0.58",
3
+ "version": "1.0.60",
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
@@ -3962,6 +3962,23 @@ line-height: 31px;
3962
3962
  background: var(--md-sc-low);
3963
3963
  border-radius: var(--md-r-md);
3964
3964
  }
3965
+ /* desktop only (mobile keeps the flex-wrap flow above, untouched): a clean 2-column grid so
3966
+ pairs line up (km|EZ, Kraftstoff|Getriebe, Leistung|Halter) instead of reflowing by text
3967
+ length (Figma "mcard"). Facts render in DOM order [km, EZ, Getriebe, Kraftstoff, Leistung,
3968
+ Halter, Verfügbarkeit]; only Getriebe/Kraftstoff swap columns via `order` to land as
3969
+ Kraftstoff|Getriebe per the mockup — the array itself stays untouched (mobile relies on it). */
3970
+ @media (min-width: 601px) {
3971
+ .md3 .specstrip {
3972
+ display: grid;
3973
+ grid-template-columns: 1fr 1fr;
3974
+ }
3975
+ /* km/EZ/Kraftstoff pulled negative (in that order) so they sort ahead of the untouched,
3976
+ default-order Getriebe/Leistung/Halter/Verfügbarkeit — net effect: km, EZ, Kraftstoff,
3977
+ Getriebe, Leistung, Halter, Verfügbarkeit, i.e. only Kraftstoff hops in front of Getriebe. */
3978
+ .md3 .specstrip > span:nth-child(1) { order: -3; }
3979
+ .md3 .specstrip > span:nth-child(2) { order: -2; }
3980
+ .md3 .specstrip > span:nth-child(4) { order: -1; }
3981
+ }
3965
3982
  /* filter rail: collapsible sections */
3966
3983
  .md3 .frail-group {
3967
3984
  border-bottom: 1px solid var(--md-outline-variant);
@@ -4199,6 +4216,9 @@ line-height: 31px;
4199
4216
  .md3 .pp-hero-old { font-size: 15px; color: var(--md-error); text-decoration: line-through; white-space: nowrap; }
4200
4217
  .md3 .pp-hero-val sup, .md3 .pp-hero-old sup, .md3 .pp-hero-unit sup { font-size: .6em; }
4201
4218
  .md3 .pp-hero-terms { margin-top: 9px; font-size: 11.5px; color: var(--md-on-surface-variant); }
4219
+ /* "Details" link at the end of the financing/leasing terms lines (Figma "mcard") — quiet, same
4220
+ color as the rest of the terms line but underlined so it still reads as tappable. */
4221
+ .md3 .pp-details-link { color: var(--md-primary); text-decoration: underline; font-weight: 600; white-space: nowrap; }
4202
4222
  /* separator line between the financing block and the leasing row (legacy-design parity) */
4203
4223
  .md3 .pp-lease-row {
4204
4224
  margin-top: 16px;
@@ -5103,6 +5123,16 @@ line-height: 31px;
5103
5123
  padding: 8px 16px 16px;
5104
5124
  }
5105
5125
 
5126
+ /* Base (desktop) values for the two TopBar layout knobs the mobile Header-System spec overrides
5127
+ below — kept as real CSS rules (not inline styles) since an inline style on the element would
5128
+ always beat the media-query override regardless of specificity. */
5129
+ .md3 .topbar-container {
5130
+ padding: 0 16px;
5131
+ }
5132
+ .md3 .topbar-actions {
5133
+ gap: 16px;
5134
+ }
5135
+
5106
5136
  /* mobile-only "Account Layer" sheet (TopBar) — full-width panel under the header, per the Figma
5107
5137
  "03 | Account Layer" mockups. .show-mobile forces display:flex (see the ≤600px override
5108
5138
  below) — flex-direction:column comes from the inline style on the element itself. */
@@ -5131,30 +5161,53 @@ line-height: 31px;
5131
5161
  /* desktop "Konto" pill collapses to a plain icon-only button on mobile — the account layer above
5132
5162
  replaces the small popover the pill normally opens on wider screens. */
5133
5163
  @media (max-width: 600px) {
5164
+ /* "Header-System" spec dimensions (390px reference) — all scoped to the TopBar's own <header>
5165
+ (or a dedicated class) so none of this touches .icon-btn/.mbtn/.container usage elsewhere in
5166
+ the app (VDP gallery controls, financing modal, other pages' own containers, etc.). */
5134
5167
  .md3 .topbar-account-btn {
5135
5168
  background: transparent;
5136
5169
  border: 0;
5137
5170
  padding: 0;
5138
- width: 40px;
5139
- height: 40px;
5171
+ /* Touch target 48×48 — WCAG 2.2 minimum is 24×24, but the spec recommends 48×48 consistently
5172
+ for auto.de given the broad age range and high mobile share. */
5173
+ width: 48px;
5174
+ height: 48px;
5140
5175
  min-width: 0;
5141
5176
  border-radius: 50%;
5142
5177
  }
5143
5178
  .md3 .topbar-account-btn:hover {
5144
5179
  background: color-mix(in srgb, var(--md-on-surface) 8%, transparent);
5145
5180
  }
5146
- /* Mobile header icons measured against the real Figma export read visibly smaller than the
5147
- reference (the shared .icon-btn/.mbtn rules force 20px/18px everywhere in the app — fine
5148
- elsewhere, but the Figma mobile header specifically wants ~24px/22px). Scoped to icon
5149
- buttons INSIDE the TopBar's own <header> so it doesn't touch icon-btn/mbtn usage on other
5150
- pages (VDP gallery controls, financing modal, etc.). */
5181
+ /* Visible icon glyphs 24-28px (spec range) — the shared .icon-btn/.mbtn rules force 20px/18px
5182
+ everywhere else in the app, which is fine there but read visibly smaller than the Figma
5183
+ mobile header reference. */
5184
+ .md3 header .icon-btn {
5185
+ width: 48px;
5186
+ height: 48px;
5187
+ }
5151
5188
  .md3 header .icon-btn .lucide {
5152
5189
  width: 24px;
5153
5190
  height: 24px;
5154
5191
  }
5155
5192
  .md3 header .topbar-account-btn .lucide {
5156
- width: 22px;
5157
- height: 22px;
5193
+ width: 24px;
5194
+ height: 24px;
5195
+ }
5196
+ /* Horizontal margin 12px (spec: "Horizontaler Rand"). */
5197
+ .md3 .topbar-container {
5198
+ padding: 0 12px;
5199
+ }
5200
+ /* Logo 118-126px wide (spec range) — the SVG's own aspect ratio (120:34.909) means the desktop
5201
+ height=32 attribute renders ~110px wide, just under range; this bumps it into range on mobile
5202
+ specifically via a CSS width override (wins over the HTML height attribute). */
5203
+ .md3 .topbar-logo {
5204
+ width: 122px;
5205
+ height: auto;
5206
+ }
5207
+ /* Gap between action icons 2-6px (spec: "Abstand Aktionen") — separate from the wider gap the
5208
+ hamburger/logo/nav still use (that's the outer .container's own gap, untouched here). */
5209
+ .md3 .topbar-actions {
5210
+ gap: 4px;
5158
5211
  }
5159
5212
  }
5160
5213
  /* language segmented DE|EN — active tab filled, inactive ghost */