@bytebrand/fe-ui-core-autobahn 1.0.122 → 1.0.124

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
@@ -30,6 +30,7 @@ import {
30
30
  PpActivity,
31
31
  GalleryBadges,
32
32
  GalleryCounter,
33
+ GalleryActivityPill,
33
34
  OfferBadge,
34
35
  PriceTabs,
35
36
  PriceHero,
@@ -224,44 +225,19 @@ const CarRow: React.FunctionComponent<CarRowProps> = ({
224
225
  <div className="show-mobile">
225
226
  <GalleryBadges car={car} t={t} />
226
227
  </div>
227
- {/* desktop: single row — light "1|N" photo counter + a combined eye/heart chip, no
228
- availability/Geprüft pill (not present in the Figma "mcard" export).
229
- Same 3-state policy as the mobile pills above (GalleryCounter/GalleryBadges,
230
- cardParts.tsx) and CarCard's GalleryCounter/GalleryActivityPill, just inlined here
231
- instead of reusing those components (different chip markup — one shared "N|M" +
232
- eye+heart pill instead of three separate ones):
233
- - single-image: car.photos is the real, eagerly-known count (not the lazily-loaded
234
- gallery array length) — accurate even before the extra photos have loaded.
235
- - loading: viewsCount/favoritesCount are undefined until stats resolve for this car
236
- — hidden, no skeleton, same as everywhere else.
237
- - zero-value: a real 0 is intentionally treated the same as "not loaded" — hidden,
238
- not shown as "eye 0" / "heart 0" (noise on a dense results list otherwise). */}
239
- <div
240
- className="hide-mobile"
241
- style={{ position: 'absolute', bottom: 20, left: 12, zIndex: 3, display: 'flex', alignItems: 'center', gap: 6 }}
242
- >
243
- {car.photos > 1 && (
244
- <span className="mcard-photo-chip">
245
- <Icon name="maximize" size={16} stroke={1.7} />
246
- {photoIndex + 1}|{car.photos}
247
- </span>
248
- )}
249
- {(!!car.viewsCount || !!car.favoritesCount) && (
250
- <span className="mcard-photo-chip">
251
- {!!car.viewsCount && (
252
- <>
253
- <Icon name="eye" size={16} stroke={1.7} />
254
- {car.viewsCount}
255
- </>
256
- )}
257
- {!!car.favoritesCount && (
258
- <>
259
- <Icon name="heart" size={16} stroke={1.7} />
260
- {car.favoritesCount}
261
- </>
262
- )}
263
- </span>
264
- )}
228
+ {/* desktop: same light "1|N" photo counter + eye/heart activity pill as CarCard (Figma
229
+ "mcard" export) — reusing GalleryCounter(light)/GalleryActivityPill (cardParts.tsx)
230
+ instead of the old inlined "N|M" + combined eye+heart chip this used to render, so
231
+ list view and grid view can never visually drift again. Same bottom:10/left:10 +
232
+ bottom:10/right:10 placement, same 3-state policy (both components self-guard:
233
+ GalleryCounter hides at total<=1, GalleryActivityPill hides when both counts are
234
+ falsy — loading/zero read the same as "nothing worth showing yet", see their own
235
+ doc comments in cardParts.tsx) — no manual guards needed here any more. */}
236
+ <div className="hide-mobile" style={{ position: 'absolute', bottom: 10, left: 10, zIndex: 3 }}>
237
+ <GalleryCounter current={photoIndex + 1} total={car.photos} light />
238
+ </div>
239
+ <div className="hide-mobile" style={{ position: 'absolute', bottom: 10, right: 10, zIndex: 3 }}>
240
+ <GalleryActivityPill viewsCount={car.viewsCount} favoritesCount={car.favoritesCount} />
265
241
  </div>
266
242
  </div>
267
243
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bytebrand/fe-ui-core-autobahn",
3
- "version": "1.0.122",
3
+ "version": "1.0.124",
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
@@ -298,9 +298,38 @@ line-height: 31px;
298
298
  flex: none;
299
299
  transition: background 0.15s;
300
300
  }
301
- .md3 .icon-btn:hover {
301
+ /* :not(:disabled) — same convention as .co-rail-step:hover:not(:disabled) elsewhere in this file:
302
+ a disabled icon-btn (e.g. VdpViewer's zoom in/out at ZOOM_MIN/MAX) shouldn't show a hover fill
303
+ that implies it's still clickable. */
304
+ .md3 .icon-btn:hover:not(:disabled) {
302
305
  background: var(--md-sc-high);
303
306
  }
307
+ .md3 .icon-btn:disabled {
308
+ cursor: not-allowed;
309
+ }
310
+ /* VDP viewer (desktop) toolbar — zoom out/in, 360°, and close share this white-circle+shadow
311
+ look; email/call get the primary-blue one below. Both are CLASSES, not inline `style` (an
312
+ inline style background permanently pins it, blocking :hover entirely — no exceptions, so
313
+ this used to be the one thing silently killing hover on all five of these buttons). Works
314
+ without any extra :hover rule of its own for the light variant: .icon-btn:hover:not(:disabled)
315
+ above is more specific (4 selectors vs. this rule's 2) so it already wins on hover for free. */
316
+ .md3 .vdp-viewer-light {
317
+ background: var(--md-sc-lowest, #fff);
318
+ box-shadow: var(--md-e1);
319
+ }
320
+ .md3 .vdp-viewer-contact {
321
+ background: var(--md-primary);
322
+ box-shadow: var(--md-e1);
323
+ }
324
+ /* the contact (email/call) pair needs its OWN :hover, and it needs the explicit `.icon-btn`
325
+ repeated in the selector to match .icon-btn:hover:not(:disabled) above's specificity (4
326
+ selectors each, :not()'s specificity is its argument's — a plain `.vdp-viewer-contact:hover`
327
+ is only 3 and would silently lose, tinting these the light pair's neutral grey instead of the
328
+ deeper blue below, the same "primary button, one shade darker" treatment .mbtn--filled:hover
329
+ already uses). Source order (this comes after) breaks the tie once specificity is equal. */
330
+ .md3 .icon-btn.vdp-viewer-contact:hover {
331
+ background: var(--autode-blau-strong);
332
+ }
304
333
  .md3 .icon-btn--filled {
305
334
  background: #FFFFFF;
306
335
  border: 1px solid #D4DDE7;
@@ -2263,6 +2292,12 @@ line-height: 31px;
2263
2292
  display: inline-flex;
2264
2293
  gap: 6px;
2265
2294
  margin: 12px 0;
2295
+ /* .dash-card is display:flex/flex-direction:column (legacy .section) with the default
2296
+ align-items:stretch, so this box (its width left auto) stretches to the card's full width
2297
+ as a flex item regardless of being inline-flex — without justify-content the "0 0 0" digits
2298
+ then just sit at the box's flex-start (left) edge instead of centering like the icon above
2299
+ them (EmployeeIcon centers via its own margin:0 auto, unaffected by this). */
2300
+ justify-content: center;
2266
2301
  }
2267
2302
  .md3 .dash-odo span {
2268
2303
  display: inline-flex;
@@ -2924,11 +2959,132 @@ line-height: 31px;
2924
2959
  .md3 .co-step-label {
2925
2960
  display: none;
2926
2961
  }
2927
- .md3 .co-step {
2962
+ /* CheckoutPage/CheckoutChromeNext never actually add the `.md3` class anywhere in their own
2963
+ tree (only comments say they expect it — confirmed by grep), so a lone .md3-prefixed rule
2964
+ here would silently never match on this page at all. Every rule below is written twice —
2965
+ `.md3 .x` (wins by source order over the un-media-queried base rule of the same
2966
+ specificity, IF `.md3` ever is present) and plain `.x` (the one that actually matches
2967
+ today, and stays correct even if that .md3 gap gets fixed later) — so the fix holds
2968
+ regardless of that gap. */
2969
+ .md3 .co-step,
2970
+ .co-step {
2971
+ /* was flex: none — with .co-step-label hidden two rules up, each step's only remaining
2972
+ content is a fixed 30px dot + this connector bar; flex:none sized every step off the
2973
+ bar's own min-width instead of sharing the row, and together with the bar's min-width
2974
+ below (was 24px, MORE than the base 16px) the 5 steps' combined min-content width ran
2975
+ past a 390px viewport — hence the horizontal scroll. flex:1 (matches the desktop base)
2976
+ lets them actually share the row and shrink together instead. */
2977
+ flex: 1;
2978
+ }
2979
+ /* last step (5) has no .co-step-bar (nothing after the final dot to connect to) — at an
2980
+ equal flex:1 share like its siblings, its box was still as wide as theirs, leaving empty
2981
+ trailing space after its dot that read as "not stretched to the edge". flex:none sizes it
2982
+ down to just its dot's own 30px instead, and steps 1-4 (still flex:1) automatically absorb
2983
+ the freed-up space into their own bars — dot 5 ends up flush at the row's right edge. */
2984
+ .md3 .co-step:last-child,
2985
+ .co-step:last-child {
2928
2986
  flex: none;
2929
2987
  }
2930
- .md3 .co-step-bar {
2931
- min-width: 24px;
2988
+ .md3 .co-step-bar,
2989
+ .co-step-bar {
2990
+ min-width: 10px;
2991
+ margin: 0 4px;
2992
+ }
2993
+ /* was 28px auto 4px / 0 24px (desktop values) at every width — no dedicated mobile value. */
2994
+ .md3 .co-stepper,
2995
+ .co-stepper {
2996
+ margin: 10px auto 10px;
2997
+ padding: 0 14px;
2998
+ }
2999
+ .md3 .co-header-row,
3000
+ .co-header-row {
3001
+ /* was 12px 24px (desktop) at every width — no dedicated mobile value. */
3002
+ padding: 18px 14px;
3003
+ /* was 16px (desktop) at every width. */
3004
+ gap: 10px;
3005
+ }
3006
+ .md3 .co-header-car,
3007
+ .co-header-car {
3008
+ /* was 16px (desktop) at every width. */
3009
+ gap: 10px;
3010
+ }
3011
+ .md3 .co-car-img,
3012
+ .co-car-img {
3013
+ /* was 64x44 (desktop) at every width — same 64:44 aspect ratio, just bigger. */
3014
+ width: 80px;
3015
+ height: 55px;
3016
+ }
3017
+ .md3 .co-price-amount,
3018
+ .co-price-amount {
3019
+ /* was 20px (desktop) at every width. */
3020
+ font-size: 22px;
3021
+ }
3022
+ /* "Weiter"/"Zurück" (.co-next) — the shared Button component's size="lg" gives every mbtn
3023
+ here .mbtn--lg's bigger dimensions (h:48/padding 0 30, or h:46 on phone via the NewMainPage
3024
+ mobile rule elsewhere); on this narrow a sheet that reads oversized next to the rest of the
3025
+ form. Drops back to plain .mbtn's own base sizing instead. (.co-next .mbtn--lg is already
3026
+ 2 classes, matching .md3 .mbtn--lg's own specificity, so this one didn't need the
3027
+ dual-selector treatment above to win either way.) */
3028
+ .co-next .mbtn--lg {
3029
+ height: 40px;
3030
+ padding: 0 24px;
3031
+ font-size: 14px;
3032
+ /* same size for both — flex:1 splits whatever's left on their shared row evenly, so
3033
+ Weiter's longer label/icon can't make it wider than back. */
3034
+ flex: 1;
3035
+ }
3036
+ /* .mbtn--outlined ("back"): explicit base .mbtn + .mbtn--outlined properties, not just the
3037
+ size tweak above — same reasoning as the dual-selector rules elsewhere in this block
3038
+ (CheckoutPage never actually adds the `.md3` class anywhere in its own tree), just spelled
3039
+ out in full this time since the request was for this button's whole look, not one property.
3040
+ border-radius/border are explicit per-request overrides (10px + a real 1px border), not
3041
+ the shared design-system's var(--md-r-full) pill + inset-box-shadow border trick — a
3042
+ deliberately different, more "squared" look for just this button. */
3043
+ .md3 .co-next .mbtn--outlined,
3044
+ .co-next .mbtn--outlined {
3045
+ font-family: var(--md-font);
3046
+ font-weight: 600;
3047
+ letter-spacing: 0.1px;
3048
+ display: inline-flex;
3049
+ align-items: center;
3050
+ justify-content: center;
3051
+ gap: 8px;
3052
+ white-space: nowrap;
3053
+ background: transparent;
3054
+ color: #005cff;
3055
+ /* label text renders as the literal lowercase "back" (no matching key in the CheckoutPage
3056
+ locale files — i18next falls back to printing the raw t('back') key itself), next to
3057
+ Weiter's already-capitalized German label. Fixed here rather than chasing down/adding
3058
+ the missing translation key, since the ask was specifically about the letter case. */
3059
+ text-transform: capitalize;
3060
+ border-radius: 10px;
3061
+ border: 1px solid #005cff;
3062
+ box-shadow: none;
3063
+ }
3064
+ /* .mbtn--filled ("Weiter"): hardcoded #005cff per explicit request, not var(--md-primary) —
3065
+ same dual-selector reasoning as above. */
3066
+ .md3 .co-next .mbtn--filled,
3067
+ .co-next .mbtn--filled {
3068
+ background: #005cff;
3069
+ }
3070
+ /* back on the left, Weiter on the right, on their OWN row — .co-error (when a step's
3071
+ validation fails) must not crowd into that same row with them. flex-wrap + the .co-error
3072
+ rule below (flex-basis:100%, forcing it to consume its whole line) does this with the
3073
+ existing flat DOM (.co-next's 3 children are plain siblings — .co-error, then both
3074
+ buttons, no wrapping element around the buttons) — .co-error always lands on its own
3075
+ first line, and whatever's left (the two buttons — or, when there's no error, ALL of
3076
+ .co-next's content) wraps onto the next line and is laid out by flex-direction:row same
3077
+ as before. The existing gap:8px (base .co-next, unmodified) applies between wrapped lines
3078
+ too, so the error/button rows don't need their own separate margin. */
3079
+ .md3 .co-next,
3080
+ .co-next {
3081
+ flex-direction: row;
3082
+ flex-wrap: wrap;
3083
+ align-items: stretch;
3084
+ }
3085
+ .md3 .co-next .co-error,
3086
+ .co-next .co-error {
3087
+ flex: 1 0 100%;
2932
3088
  }
2933
3089
  }
2934
3090
  /* below the 992px desktop breakpoint the content no longer reserves the 424px left
@@ -3456,12 +3612,14 @@ line-height: 31px;
3456
3612
  box-shadow: 0px 2px 6px rgba(0, 0, 0, 0.08);
3457
3613
  }
3458
3614
  /* pin the VDP gallery's own edge offset — the base .car-arrow--l/--r rule above was moved to
3459
- 10px for the SRP card carousels; the VDP viewer keeps its original, larger offset. */
3615
+ 10px for the SRP card carousels; the VDP gallery box's OWN overlay elements (push-badges,
3616
+ favorite/share, the photo-count/activity pills) all sit at a 14px inset, so the arrows match
3617
+ that instead (was 22px — inconsistent with the rest of this box). */
3460
3618
  .md3 .vdp-gallerybox .car-arrow--l {
3461
- left: 22px;
3619
+ left: 14px;
3462
3620
  }
3463
3621
  .md3 .vdp-gallerybox .car-arrow--r {
3464
- right: 22px;
3622
+ right: 14px;
3465
3623
  }
3466
3624
 
3467
3625
  /* ==========================================================================
@@ -3657,7 +3815,7 @@ line-height: 31px;
3657
3815
  .md3 .vdp-meta > span {
3658
3816
  display: inline-flex;
3659
3817
  align-items: center;
3660
- gap: 0px;
3818
+ gap: 6px;
3661
3819
  white-space: nowrap;
3662
3820
  }
3663
3821
  .md3 .vdp-meta > span + span::before {
@@ -4082,8 +4240,7 @@ line-height: 31px;
4082
4240
  }
4083
4241
  /* consistent studio treatment for ALL inventory photos: white backdrop + soft grounding floor
4084
4242
  shadow, so mixed-source cutouts look uniform (Carvana/Cinch catalog look) */
4085
- .md3 .cc-media,
4086
- .md3 .listcard-media {
4243
+ .md3 .cc-media {
4087
4244
  background: radial-gradient(
4088
4245
  58% 13px at 50% 90%,
4089
4246
  rgba(16, 22, 42, 0.15),
@@ -4091,6 +4248,12 @@ line-height: 31px;
4091
4248
  ),
4092
4249
  #fff;
4093
4250
  }
4251
+ /* SRP list card: plain white, no floor shadow under the car — the wider (320px) media column
4252
+ made that soft dark oval read as an actual smudge/shadow under the vehicle instead of a
4253
+ subtle grounding cue. .cc-media (home/offer grid cards) keeps it, unaffected. */
4254
+ .md3 .listcard-media {
4255
+ background: #fff;
4256
+ }
4094
4257
  /* offer cards: border + soft shadow so they sit on the page (not borderless-floaty) */
4095
4258
  .md3 .cc-card {
4096
4259
  box-shadow: var(--md-e1);
@@ -4099,7 +4262,12 @@ line-height: 31px;
4099
4262
  /* horizontal listing card (SRP): image | info | price */
4100
4263
  .md3 .listcard {
4101
4264
  display: grid;
4102
- grid-template-columns: 300px 1fr 240px;
4265
+ /* media column widened 300→320 (340 was too much): at the max 6 dots (CardCarousel slices
4266
+ images to 6), the centered dots row got wide enough to visually collide with
4267
+ .cc-activity-pill (bottom-right, anchored 10px from the edge) and came within ~1.5px of
4268
+ .cc-photo-pill (bottom-left) too — both pills are anchored a fixed 10px from the column's
4269
+ edges, so a wider column pushes them further apart without anything else needing to change. */
4270
+ grid-template-columns: 320px 1fr 240px;
4103
4271
  align-items: stretch;
4104
4272
  }
4105
4273
  .md3 .listcard-media {
@@ -4491,6 +4659,19 @@ line-height: 31px;
4491
4659
  .md3 .body-tile .md-label-sm {
4492
4660
  color: var(--md-on-surface-variant);
4493
4661
  }
4662
+ /* the label span has no explicit width — as a column-flex item with align-items:center it
4663
+ sizes to its own natural (unwrapped) content width, so a long compound label like
4664
+ "SUV/Geländewagen" (no spaces to wrap at) overflows past the tile's rounded edges instead
4665
+ of wrapping to a 2nd line. Cap it to the tile's own content width so the browser is forced
4666
+ to wrap — plain `overflow-wrap: break-word` prefers a normal break point ("/" counts, same
4667
+ as "and/or" wrapping at the slash in body text) and only breaks mid-word as a genuine last
4668
+ resort. `word-break: break-word` looked like the same fix but isn't: it forces min-content
4669
+ sizing as if EVERY character were a break point, which made the browser ignore the "/" and
4670
+ split "Geländewa" / "gen" instead — do not add it back. */
4671
+ .md3 .body-tile > span:not(.md-label-sm) {
4672
+ max-width: 100%;
4673
+ overflow-wrap: break-word;
4674
+ }
4494
4675
  /* payment paths: financing hero (eyebrow + big rate + terms) on top, Leasing/Kaufen quiet rows */
4495
4676
  .md3 .pricepaths {
4496
4677
  min-width: 0;
@@ -6495,7 +6676,10 @@ line-height: 31px;
6495
6676
  gap: 24px;
6496
6677
  }
6497
6678
  .md3 .listcard {
6498
- grid-template-columns: 260px 1fr;
6679
+ /* same dots/pill clearance reasoning as the base 300→320 bump above — this range (601–980px,
6680
+ price column dropped to 1 row below) still shows the desktop pills (.hide-mobile only
6681
+ disappears at 600px), so it needs the same margin, proportionally applied. */
6682
+ grid-template-columns: 280px 1fr;
6499
6683
  }
6500
6684
  /* tablet: price column spans full width below image+info, zones stack in a normal column */
6501
6685
  .md3 .listcard-price {
@@ -7432,7 +7616,6 @@ line-height: 31px;
7432
7616
  .md3 .vdp-push-badges {
7433
7617
  gap: 6px;
7434
7618
  margin-bottom: 13px;
7435
- padding-left: 12px;
7436
7619
  }
7437
7620
  }
7438
7621
 
@@ -7750,9 +7933,14 @@ color: #005CFF;
7750
7933
  @media (max-width: 600px) {
7751
7934
  /* full-bleed gallery: edge-to-edge, app-like. Extra height (vs. the plain photo height)
7752
7935
  reserves room at the bottom for the controls row, now overlaid on the image itself
7753
- (round 15 — supersedes the earlier "no overlay" layout). */
7936
+ (round 15 — supersedes the earlier "no overlay" layout).
7937
+ -14px, not -24px: cancels .vdp-main's OWN mobile side padding (14px !important, below —
7938
+ .vdp-gallerybox's direct containing block), not the 24px desktop inline value. Was still
7939
+ -24px from before that 14px override existed, so this was overshooting the true viewport
7940
+ edge by 10px — .car-arrow/.vdp-push-badges (positioned against .cardcar, i.e. against THIS
7941
+ box) were landing ~4px from the real edge instead of their own stated 14px. */
7754
7942
  .md3 .vdp-gallerybox {
7755
- margin: 0 -24px;
7943
+ margin: 0 -14px;
7756
7944
  border-radius: 0;
7757
7945
  border: 0;
7758
7946
  box-shadow: none;
@@ -7764,11 +7952,16 @@ color: #005CFF;
7764
7952
  .md3 .vdp-gallery {
7765
7953
  position: relative;
7766
7954
  }
7767
- /* controls row now sits ON the image as an overlay, bottom-aligned (round 15) */
7955
+ /* controls row now sits ON the image as an overlay, bottom-aligned (round 15).
7956
+ left/right: 0, not 14px — unlike .vdp-gallerybox above, .vdp-gallery is NOT full-bleed
7957
+ (no negative margin of its own), so it's still sitting inside .vdp-main's 14px padding —
7958
+ that padding alone already supplies the 14px gutter from the true edge. The extra 14px
7959
+ here was stacking on top of it (14+14=28px), pulling .gallery-photos-btn/the share+save
7960
+ icons noticeably further in than .car-arrow/.vdp-push-badges' real 14px on the same edges. */
7768
7961
  .md3 .gallery-controls-row {
7769
7962
  position: absolute;
7770
- left: 14px;
7771
- right: 14px;
7963
+ left: 0;
7964
+ right: 0;
7772
7965
  bottom: 12px;
7773
7966
  z-index: 3;
7774
7967
  padding: 0;
@@ -9075,6 +9268,15 @@ color: #005CFF;
9075
9268
  line-height: 16px;
9076
9269
  color: #ffffff;
9077
9270
  }
9271
+ /* the base .mbtn .lucide rule (width/height:18px) was overriding FinanceCalculator.tsx's own
9272
+ size={23} prop on this icon — CSS width/height on an <svg> always wins over its own
9273
+ width/height attributes, so the component-level size bump alone did nothing visible.
9274
+ Scoped to just this button (not .fin-cta-row .mbtn--outlined/"Anrufen" next to it, whose
9275
+ 18px icon wasn't part of this request). */
9276
+ .md3 .fin-cta-row .mbtn--filled .lucide {
9277
+ width: 23px;
9278
+ height: 23px;
9279
+ }
9078
9280
  .md3 .fin-cta-row .mbtn--outlined {
9079
9281
  width: 108px;
9080
9282
  background: #ffffff;
@@ -9349,7 +9551,31 @@ color: #005CFF;
9349
9551
  border-radius: var(--md-r-lg, 16px);
9350
9552
  box-shadow: var(--md-e1);
9351
9553
  padding: 20px;
9352
- margin-bottom: 20px;
9554
+ margin-bottom: 15px;
9555
+ /* the legacy .section class (still applied alongside dash-card — see DashboardOverview.tsx
9556
+ and its sibling sections) sets a fixed height: 361px/195px per tier with
9557
+ display:flex/flex-direction:column; min-height:auto only raises the floor, it never cancels
9558
+ that fixed height, so once a card's content (e.g. the table after the padding fix above)
9559
+ grew past 361px it kept overflowing the rounded border instead of growing the card — that
9560
+ was the "676 more vehicles" sitting outside the block.
9561
+ height:100% (not auto) on purpose: each dash-card's immediate parent is a react-flexbox-grid
9562
+ <Col>, and every Row of <Col>s already stretches its columns to the tallest one by default
9563
+ (align-items: normal → stretch) — 100% lets the card fill that already-equalized column
9564
+ height instead of only fitting its own content, which is what keeps e.g. "Create Car" (tall
9565
+ icon + copy) and "Base data" (three short lines) sitting in the same Row at equal height with
9566
+ their bottom edges aligned, the way the old fixed-height tiers did whenever content happened
9567
+ to fit them. A lone/tallest card in a Row still just gets its own content height back, since
9568
+ 100% then resolves to exactly that — no clipping either way.
9569
+ BUT plain height:100% + margin-bottom on the SAME box fights itself: the <Col> is a flex
9570
+ item, which per spec always establishes its own block-formatting-context for its children —
9571
+ so dash-card's margin-bottom can never collapse through it into extra Row spacing, it can
9572
+ only overflow past the (exactly 100%-filled) Col edge, which does NOT push the next Row down
9573
+ — net visible gap: 0, cards touching, no matter what the margin-bottom value is set to. Carve
9574
+ the same margin OUT of the height instead (100% − margin) so box + margin together still sum
9575
+ to exactly the stretched Col height, but the box itself stops short and the margin is real,
9576
+ visible empty space above the next Row rather than overflow past this one. Keep this in sync
9577
+ with margin-bottom above if that value ever changes. */
9578
+ height: calc(100% - 15px);
9353
9579
  min-height: auto;
9354
9580
  }
9355
9581
  .md3 .dash-card-title {
@@ -9363,9 +9589,24 @@ color: #005CFF;
9363
9589
  font: 600 14px var(--md-font);
9364
9590
  color: var(--md-primary);
9365
9591
  text-decoration: none;
9592
+ /* the legacy .secondaryLink class (still applied alongside dash-link — see
9593
+ DashboardOverview.tsx) draws a bordered, uppercase, fixed-210px button; dash-link on its own
9594
+ only ever added color/font on top of that, so the "To Overview" link still rendered as the
9595
+ old boxy button underneath. Reset every legacy box property so it reads as a plain md3 text
9596
+ link. */
9597
+ width: auto;
9598
+ min-width: 0;
9599
+ padding: 0;
9600
+ border: 0;
9601
+ border-radius: 0;
9602
+ background: transparent;
9603
+ box-shadow: none;
9604
+ text-align: left;
9605
+ text-transform: none;
9366
9606
  }
9367
9607
  .md3 .dash-link:hover {
9368
9608
  text-decoration: underline;
9609
+ background: transparent;
9369
9610
  }
9370
9611
  .md3 .dash-link--primary {
9371
9612
  background: var(--md-primary);
@@ -9378,27 +9619,82 @@ color: #005CFF;
9378
9619
  text-decoration: none;
9379
9620
  opacity: 0.92;
9380
9621
  }
9622
+ /* DashboardBaseData/CreateCar/DataExport/DataImport render their one trailing link/button as a
9623
+ DIRECT child of the dash-card section (no wrapper) — .dash-card is display:flex;
9624
+ flex-direction:column (from the legacy .section class), so margin-top:auto on it consumes all
9625
+ the column's leftover space and pins it flush to the card's bottom edge, matching cards whose
9626
+ content happens to fill the row exactly. Scoped to a DIRECT child so it does NOT reach
9627
+ DashboardEmployee's two links — those sit inside .linksWrapper (its own already-working
9628
+ margin-top:auto), one level deeper, so this selector never matches them. */
9629
+ .md3 .dash-card > .dash-link {
9630
+ margin-top: auto;
9631
+ }
9632
+ /* the legacy DealerDashboardPage__table/__tableRow classes (still applied alongside dash-table —
9633
+ see DashboardOverview.tsx) render each cell as its own coloured, gapped box (border-spacing:
9634
+ separate + a per-cell background) and split the columns 55/15/15/15/15 (=115%, doesn't even
9635
+ add up) under table-layout:fixed — that's the mismatched-column / disjointed-boxes look
9636
+ reported live. Every property that leaked through below is reset here at matching or higher
9637
+ specificity so it can't win regardless of stylesheet load order. */
9381
9638
  .md3 .dash-table {
9639
+ /* legacy .table also sets width:calc(100% + 30px) + margin-left/right:-15px — a full-bleed
9640
+ trick tuned for .section's OLD 15px padding. width:100% above already wins on specificity,
9641
+ but the negative margins were never a competing declaration (nothing here set `margin`), so
9642
+ they kept shifting the whole table 15px left, leaving a lopsided ~15px extra gap on the
9643
+ right — that's the "more to the left" look reported live. */
9644
+ margin: 0;
9382
9645
  width: 100%;
9383
9646
  border-collapse: collapse;
9647
+ table-layout: fixed;
9384
9648
  font-size: 13.5px;
9385
9649
  }
9650
+ .md3 .dash-table th,
9651
+ .md3 .dash-table td {
9652
+ background: transparent;
9653
+ width: auto;
9654
+ padding: 10px 12px;
9655
+ text-align: center;
9656
+ }
9386
9657
  .md3 .dash-table th {
9387
- text-align: left;
9388
9658
  font: 600 12.5px var(--md-font);
9389
9659
  color: var(--md-on-surface-variant);
9390
- padding: 6px 8px;
9660
+ padding-top: 6px;
9661
+ padding-bottom: 6px;
9391
9662
  border-bottom: 1px solid var(--md-outline-variant);
9663
+ white-space: nowrap;
9392
9664
  }
9393
9665
  .md3 .dash-table td {
9394
- padding: 9px 8px;
9395
9666
  border-bottom: 1px solid var(--md-outline-variant);
9396
9667
  color: var(--md-on-surface);
9397
9668
  vertical-align: middle;
9398
9669
  }
9670
+ /* :first-child/:last-child match the legacy rules' own specificity tier (class + pseudo-class +
9671
+ element) — a plain .dash-table td here would only TIE the legacy selector and could lose on
9672
+ load order, so `width` is repeated on :first-child rather than left to the shared rule above. */
9673
+ .md3 .dash-table th:first-child,
9674
+ .md3 .dash-table td:first-child {
9675
+ /* the 3 middle (★/24h/Total) columns stay unset — table-layout:fixed shares whatever's left
9676
+ over (100 - 46 - 10 = 44, ~14.7% each) evenly between them. */
9677
+ width: 46%;
9678
+ text-align: left;
9679
+ padding-left: 0;
9680
+ }
9681
+ .md3 .dash-table th:last-child,
9682
+ .md3 .dash-table td:last-child {
9683
+ width: 10%;
9684
+ text-align: right;
9685
+ padding-right: 0;
9686
+ }
9399
9687
  .md3 .dash-table tr:last-child td {
9400
9688
  border-bottom: 0;
9401
9689
  }
9690
+ .md3 .dash-table tbody tr:hover td {
9691
+ background: var(--md-sc-low);
9692
+ }
9693
+ /* StarIcon's <path> has no fill prop (defaults to solid black) — pick up the same muted grey as
9694
+ the rest of the header text instead of a heavy pure-black star. */
9695
+ .md3 .dash-table th [class*="tableIcon"] {
9696
+ fill: var(--md-on-surface-variant);
9697
+ }
9402
9698
 
9403
9699
  /* ============================================================================
9404
9700
  LEGACY → md3 IN-PLACE BRIDGE (account / dealer / admin pages)
@@ -168,7 +168,12 @@ const CardCarousel: React.FunctionComponent<CardCarouselProps> = ({
168
168
  className="cardcar-dots"
169
169
  style={{
170
170
  position: 'absolute',
171
- bottom: 23,
171
+ // vertically centered against the sibling bottom-left/right pills (.cc-photo-pill /
172
+ // .cc-activity-pill, both height:30 at bottom:10 → vertical center at 25px from the
173
+ // card's bottom edge): dots are 7px tall, so bottom:21.5 puts THEIR center at
174
+ // 21.5 + 3.5 = 25 too. Shared by CarCard (grid) and CarRow (list) alike — both use
175
+ // the same pill sizing/position, so one value serves both.
176
+ bottom: 21.5,
172
177
  left: -5,
173
178
  right: 0,
174
179
  display: 'flex',
package/ui/IconButton.tsx CHANGED
@@ -14,6 +14,19 @@ export interface IconButtonProps {
14
14
  active?: boolean;
15
15
  size?: number;
16
16
  style?: React.CSSProperties;
17
+ /** native disabled — also suppresses :hover (.icon-btn:hover:not(:disabled), md3.css), so a
18
+ * visually-dimmed/at-limit button (e.g. VdpViewer's zoom in/out at ZOOM_MIN/MAX) doesn't
19
+ * still show a hover fill implying it's clickable when the action would be a no-op anyway. */
20
+ disabled?: boolean;
21
+ /**
22
+ * Extra class(es) appended after 'icon-btn'/'icon-btn--filled'. Prefer this over `style` for
23
+ * anything that should still respond to `:hover` — an inline `style` background always beats
24
+ * a stylesheet rule (the base `.icon-btn:hover` included), so a caller reaching for `style` to
25
+ * tint the button silently kills its own hover in the process. A class-based tint doesn't:
26
+ * `.icon-btn:hover` is more specific than a single extra class, so it keeps winning on hover
27
+ * for free (see VdpViewer.tsx's `.vdp-viewer-light`/`.vdp-viewer-contact` for exactly this).
28
+ */
29
+ className?: string;
17
30
  }
18
31
 
19
32
  const IconButton: React.FunctionComponent<IconButtonProps> = ({
@@ -25,12 +38,15 @@ const IconButton: React.FunctionComponent<IconButtonProps> = ({
25
38
  active,
26
39
  size = 20,
27
40
  style,
41
+ className,
42
+ disabled,
28
43
  }) => (
29
44
  <button
30
- className={'icon-btn' + (filled ? ' icon-btn--filled' : '')}
45
+ className={'icon-btn' + (filled ? ' icon-btn--filled' : '') + (className ? ` ${className}` : '')}
31
46
  onClick={onClick}
32
47
  aria-label={label}
33
48
  style={style}
49
+ disabled={disabled}
34
50
  >
35
51
  <Icon
36
52
  name={name}
@@ -1009,10 +1009,14 @@ const FinanceCalculator: React.FunctionComponent<FinanceCalculatorProps> = ({
1009
1009
  variant="filled"
1010
1010
  size="lg"
1011
1011
  className={secured ? 'fin-cta-secured' : undefined}
1012
- style={{ width: 216, height: 50, paddingLeft: 0, paddingRight: 9, gap: 18 }}
1012
+ style={{ width: 216, height: 50, paddingLeft: 0, paddingRight: 9, gap: secured ? 3 : 18 }}
1013
1013
  onClick={onSecure}
1014
1014
  >
1015
- {secured ? <Icon name="shield-check" size={18} /> : <DocCheckIcon size={23} />}
1015
+ {/* size 23 either way — was 18 for the secured (shield-check) state, notably
1016
+ smaller than DocCheckIcon's 23 in the default state despite sharing the exact
1017
+ same 50px button box; nothing about switching to "already secured" should
1018
+ shrink the icon. */}
1019
+ {secured ? <Icon name="shield-check" size={23} /> : <DocCheckIcon size={23} />}
1016
1020
  {secured && secureLabel ? secureLabel : t('autobahn:finance.secureOffer')}
1017
1021
  </Button>
1018
1022
  <div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 4, minWidth: 0 }}>
@@ -252,7 +252,7 @@ export const VdpHeaderCondition: React.FunctionComponent<VdpHeaderCardProps> = (
252
252
  * splits these 3 pieces across the "before/after finance card" layout — see the page).
253
253
  */
254
254
  export const VdpHeaderCard: React.FunctionComponent<VdpHeaderCardProps> = ({ vehicle, t }) => (
255
- <div className="mcard" style={{ marginTop: 16, padding: '22px 24px 18px' }}>
255
+ <div className="mcard" style={{ marginTop: 16, padding: '18px 14px 18px' }}>
256
256
  <VdpHeaderSummary vehicle={vehicle} t={t} />
257
257
  <div style={{ marginTop: 14 }}>
258
258
  <VdpHeaderHighlights vehicle={vehicle} t={t} />
package/vdp/VdpViewer.tsx CHANGED
@@ -761,9 +761,6 @@ const VdpViewer: React.FunctionComponent<VdpViewerProps> = ({ images, start = 0,
761
761
  return () => window.removeEventListener('resize', measure);
762
762
  }, [images.length, layout]);
763
763
 
764
- // Desktop-only styles, unchanged from the original layout.
765
- const iconBtnStyle: React.CSSProperties = { background: 'var(--md-sc-lowest, #fff)', boxShadow: 'var(--md-e1)' };
766
- const contactBtnStyle: React.CSSProperties = { background: 'var(--md-primary)', boxShadow: 'var(--md-e1)' };
767
764
 
768
765
  const overlay = (
769
766
  <div className="md3">
@@ -1469,45 +1466,67 @@ const VdpViewer: React.FunctionComponent<VdpViewerProps> = ({ images, start = 0,
1469
1466
  {i + 1} / {n}
1470
1467
  </span>
1471
1468
  <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
1469
+ {/* background/shadow now come from vdp-viewer-light/vdp-viewer-contact CSS classes,
1470
+ not inline `style` (that used to be iconBtnStyle/contactBtnStyle, see their old
1471
+ definitions above) — an inline style background permanently pins it, blocking
1472
+ :hover entirely (that's what killed hover on ALL of these, close included, until
1473
+ close got fixed the same way just above). Zoom's opacity IS still inline — that
1474
+ part is genuinely per-render dynamic (zoom-limit state), and plain opacity
1475
+ doesn't fight :hover the way background did. `disabled` at the zoom limit is
1476
+ new too — was opacity-only before, so the button still showed a hover fill at
1477
+ ZOOM_MIN/MAX implying it was clickable when zoomOut/zoomIn there is a no-op;
1478
+ the portrait/landscape zoom buttons further up already use real `disabled` the
1479
+ same way (:disabled also suppresses :hover, see .icon-btn:hover:not(:disabled)
1480
+ in md3.css), this desktop pair was just the one place still missing it. */}
1472
1481
  <IconButton
1473
1482
  name="minus"
1474
1483
  label={t('autobahn:viewer.zoomOutLabel')}
1475
1484
  onClick={zoomOut}
1476
- style={{ ...iconBtnStyle, opacity: zoom === ZOOM_MIN ? 0.4 : 1 }}
1485
+ className="vdp-viewer-light"
1486
+ style={{ opacity: zoom === ZOOM_MIN ? 0.4 : 1 }}
1487
+ disabled={zoom === ZOOM_MIN}
1477
1488
  />
1478
1489
  <IconButton
1479
1490
  name="plus"
1480
1491
  label={t('autobahn:viewer.zoomInLabel')}
1481
1492
  onClick={zoomIn}
1482
- style={{ ...iconBtnStyle, opacity: zoom === ZOOM_MAX ? 0.4 : 1 }}
1493
+ className="vdp-viewer-light"
1494
+ style={{ opacity: zoom === ZOOM_MAX ? 0.4 : 1 }}
1495
+ disabled={zoom === ZOOM_MAX}
1483
1496
  />
1484
1497
  {has360 && (
1485
1498
  // IconButton only takes a lucide `name` — Icon360 is a one-off inlined SVG (see
1486
- // its own comment above), so this is IconButton's own markup (.icon-btn + the
1487
- // same iconBtnStyle) built by hand instead of a name it can't render.
1499
+ // its own comment above), so this is IconButton's own markup (.icon-btn +
1500
+ // vdp-viewer-light) built by hand instead of a name it can't render.
1488
1501
  <button
1489
1502
  type="button"
1490
- className="icon-btn"
1503
+ className="icon-btn vdp-viewer-light"
1491
1504
  aria-label={t('autobahn:viewer.threeSixtyLabel')}
1492
1505
  onClick={() => on360 && on360()}
1493
- style={iconBtnStyle}
1494
1506
  >
1495
1507
  <Icon360 color="currentColor" />
1496
1508
  </button>
1497
1509
  )}
1498
1510
  {onEmail && (
1499
- <IconButton name="mail" label={t('autobahn:viewer.emailLabel')} onClick={onEmail} style={contactBtnStyle} color="#fff" />
1511
+ <IconButton name="mail" label={t('autobahn:viewer.emailLabel')} onClick={onEmail} className="vdp-viewer-contact" color="#fff" />
1500
1512
  )}
1501
1513
  {onCall && (
1502
1514
  <IconButton
1503
1515
  name="phone"
1504
1516
  label={t('autobahn:viewer.callLabel', { phone })}
1505
1517
  onClick={onCall}
1506
- style={contactBtnStyle}
1518
+ className="vdp-viewer-contact"
1507
1519
  color="#fff"
1508
1520
  />
1509
1521
  )}
1510
- <IconButton name="x" filled label={t('autobahn:viewer.closeLabel')} onClick={onClose} />
1522
+ {/* was `filled` (.icon-btn--filled forces a hard 34×34, !important) — every other
1523
+ button in this row (zoom/360/email/call) sits on the plain .icon-btn 40×40 box,
1524
+ so `filled` made this one visibly smaller than its neighbors. size=24 (was the
1525
+ IconButton default, 20) scales the X up with the button so it reads just as
1526
+ filled inside the now-bigger circle as it did in the old 34px one. Same
1527
+ vdp-viewer-light CLASS the zoom/360 buttons use (not an inline `style`) so
1528
+ :hover keeps working — see the className doc comment on IconButton itself. */}
1529
+ <IconButton name="x" size={24} className="vdp-viewer-light" label={t('autobahn:viewer.closeLabel')} onClick={onClose} />
1511
1530
  </div>
1512
1531
  </div>
1513
1532
  <div style={{ flex: 1, position: 'relative', minHeight: 0, background: '#fff', overflow: 'hidden' }}>