/* dealer.css — extracted from the inline <style> block in dealer.html (Phase 0),
   plus the dealer chrome + grid redesign (Phase 2A/2B). */

/* ================================================================
   Dealer chrome — autocom visual language (2A). SCOPED under
   body.dealer-theme so the SHARED navbar rules in style.css (also used
   by auctioneer.html / timeline.html / planned-lots.html) are NOT
   touched and those surfaces keep their orange bar unchanged.

   White top bar, dark nav text, 1px light-gray bottom border; orange
   (--theme-primary) kept for the logo + accents only; light-gray page
   background; white content cards. dealer.html drops `navbar-dark
   bg-primary` from the <nav>, so the orange `.navbar.bg-primary`
   gradient never applies — these rules paint the white bar and
   re-colour every header element that style.css had set white-on-orange
   (title, id, meta, clock pill, dividers, status, Lots button, user
   chip, hamburger). The user-menu dropdown + mobile collapse popovers
   keep their dark styling this pass (deferred — they read fine as
   dark menus on a white page).
   ================================================================ */
body.dealer-theme {
  background: #eaeaea;
  /* (5.2/5.3) Shared max-width for the CENTERED dealer content cluster.
     Widened to 1580px to match the NF_2027 prototype's .fluid-max-width
     (proto.autorola.com/NF_2027/showcar.html) — designer feedback 3.6. The
     left (1fr) column now resolves to ~1180px (the .dealer-bid-unit /
     .dealer-grid__vehicle cap) so 1180 + 0.75rem gap + 360px right rail ≈ 1552
     fills the cluster with Messages/Lots flush to the right of the bid unit —
     the bid-unit cap was raised in step (from 820) to keep the earlier "no gap
     before the rail" property at the wider width. Read by .dealer-grid to cap
     and centre the cluster. */
  --dealer-cluster-max: 1580px;
}

/* White top bar. NF_2027 chrome: the page separator is a subtle bottom
   SHADOW (shadow-sm language), not a hard 1px border.

   The white bar itself stays FULL-WIDTH (edge-to-edge sticky chrome), but its
   CONTENT is capped to the same centred width as the panel cluster below
   (--dealer-cluster-max) so the icons/menus/text line up directly above the
   panels rather than sitting out at the screen edges — matching the CI
   prototype. The cap lives on .container-fluid (margin-inline:auto centres it);
   the white bg + shadow stay on the full-width <nav>. On viewports below the
   cap this is a no-op, matching the full-bleed mobile grid. */
body.dealer-theme .app-header .navbar {
  background: var(--theme-white);
  border-bottom: 0;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.06);
}
body.dealer-theme .app-header .container-fluid {
  max-width: var(--dealer-cluster-max);
  margin-inline: auto;
}

/* Brand / title → dark; id + meta → muted; switch-auction link → orange */
body.dealer-theme .app-header .auction-title { color: var(--theme-text); }
/* The auction title (auction name) is now the sole header text; the raw
   auctionId slug beside it read as a redundant "double title"
   (e.g. "Mercedes-Benz Spring 2026 (berlin-spring-2026)"), so hide the id
   label on the white NF_2027 bar. The #auctionIdLabel element stays in the
   DOM — hydrateAuctionHeader() and the planned-lots bootstrap still write
   to it. Timeline keeps its id (orange bar; doesn't load dealer.css). */
body.dealer-theme .app-header .auction-id { display: none; }
body.dealer-theme .app-header .auction-meta { color: var(--theme-text-muted); }
body.dealer-theme .app-header .auction-meta .meta-separator { color: #b8b8b8; }
body.dealer-theme .app-header .auction-meta a { color: var(--theme-primary); }
body.dealer-theme .app-header .auction-meta a:hover { color: var(--theme-primary-hover); }

/* ---- Auction branding band ----
   The auction's OWN identity (vendor co-brand + auction title) relocated out
   of the top bar into a slim strip between the header and the content panels
   (dealer.html + auctioneer.html). The inner cluster box mirrors the content
   grid's containment (.container-fluid gutter + max-width:cluster; margin
   auto) so the band's left edge lines up with the panels below it — NOT with
   the header, which uses its own larger padding. Dark-on-white — the base
   .auction-title/.auction-id colours are white (for the old orange bar) and
   are only re-darkened inside .app-header, so the band must set its own. */
/* The band is part of the page background (the grey behind the panels), NOT a
   white box: transparent so the body colour shows through, no delineating
   border. */
.auction-brand-band {
  background: transparent;
}
.auction-brand-band__inner {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.75rem;
  max-width: var(--dealer-cluster-max);
  margin-inline: auto;
  /* Left-align the vendor branding with the panel content column below it (the
     "Lot No." card heading — cluster-left + the card's 1rem padding) and the
     header back-chevron. The band's cluster box shares its left edge with the
     grid, so a 1rem side padding puts the branding on the same vertical line.
     Top 1.5rem / bottom 0.5rem keeps the balanced spacing (main's py-3 adds
     ~1rem below, so the top padding carries the matching gap above). */
  padding: 1.5rem 1rem 0.5rem;
}
.auction-brand-band .auction-title {
  margin: 0; font-size: 1.1rem; font-weight: 300; color: var(--theme-text);
}
.auction-brand-band .auction-id {
  display: inline; font-size: 0.9rem; font-weight: 400; color: var(--theme-text-muted);
}
/* Vendor logos are external owned-auction assets, typically dark-on-WHITE
   JPEGs. On the transparent (grey) band a white-background logo would sit in
   its own white rectangle — the very "white box" we're removing. `multiply`
   blends the logo's white pixels into the page background while keeping the
   dark mark, so the co-brand reads as part of the background. Scoped to the
   band so the planned-lots header logo is unaffected. */
.auction-brand-band .vendor-brand__logo { mix-blend-mode: multiply; }
/* The band has room the cramped top bar did not, so keep the co-brand visible
   on phones (base rules hide .vendor-brand and the title <768px). Scoped to
   :not([hidden]) so the empty vendor slot still collapses when there's no
   vendor — otherwise the forced display would beat .vendor-brand[hidden]. */
@media (max-width: 767.98px) {
  .auction-brand-band .vendor-brand:not([hidden]) { display: inline-flex !important; }
  .auction-brand-band .auction-title { font-size: 1rem; }
}

/* (Left header) Back-to-selector affordance — the CI prototype's angle-left
   ("<") sitting to the LEFT of the logo, replacing the old "Switch auction"
   text link. Borderless, muted grey, darkens on hover. The .header-brand
   flex gap (1rem) provides the spacing to the logo, matching the prototype. */
body.dealer-theme .app-header .header-back {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  line-height: 1;
  text-decoration: none;
}
/* Thin "<" chevron asset (assets/icons/chevron-left.svg) — light-weight like
   the prototype's fa-light angle-left, greyed via opacity; darkens on hover. */
body.dealer-theme .app-header .header-back__icon {
  width: 1.75rem;
  height: 1.75rem;
  opacity: 0.65;
  display: block;
  transition: opacity 0.15s ease;
}
body.dealer-theme .app-header .header-back:hover .header-back__icon { opacity: 1; }

/* Header dividers → light gray */
body.dealer-theme .app-header .header-divider { background: #e2e2e2; }

/* Connection status text → dark-muted */
body.dealer-theme .app-header .connection-status,
body.dealer-theme .app-header #statusText { color: var(--theme-text-muted); }

/* Lots button (.view-toggle-btn): style.css paints white text on a blue
   wash (invisible on white). Repaint white/outlined with an orange
   accent active state. */
body.dealer-theme .app-header .view-toggle-btn {
  background: var(--theme-white);
  border: 1px solid #d9d9d9;
  color: var(--theme-text);
}
body.dealer-theme .app-header .view-toggle-btn:hover {
  background: #f5f5f5;
  border-color: #c9c9c9;
}
body.dealer-theme .app-header .view-toggle-btn.active {
  background: rgba(var(--theme-primary-rgb), 0.08);
  border-color: rgba(var(--theme-primary-rgb), 0.4);
  color: var(--theme-primary);
  box-shadow: none;
}

/* Classic-view toggle (new dealer-only control) — subtle outlined
   button on the desktop bar. */
body.dealer-theme .app-header .dealer-classic-link {
  background: var(--theme-white);
  border: 1px solid #d9d9d9;
  color: var(--theme-text-muted);
  padding: 0.4rem 0.75rem;
  border-radius: 0.5rem;
  font-weight: 500;
  text-decoration: none;
  display: inline-flex;
  align-items: center;
}
body.dealer-theme .app-header .dealer-classic-link:hover {
  background: #f5f5f5;
  border-color: #c9c9c9;
  color: var(--theme-text);
}

/* 2E: header live-audio mute button — same white/outlined chrome as the
   Lots / Classic controls. Icon-only; shown only while ivsStreamLive
   (syncIvsChrome toggles .d-none). */
body.dealer-theme .app-header .dealer-mute-btn {
  background: var(--theme-white);
  border: 1px solid #d9d9d9;
  color: var(--theme-text);
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
body.dealer-theme .app-header .dealer-mute-btn:hover {
  background: #f5f5f5;
  border-color: #c9c9c9;
}

/* #55 rework: auction-sound enable hint ("Push button to toggle sound"). The
   wrapper is the positioning anchor; the callout hangs below the header sound
   button and its ::before arrow points up at it. Right-anchored + width-capped
   so it never pushes past a narrow phone viewport (which would cause horizontal
   scroll). Shown/hidden by syncSoundHint() toggling .d-none. (The IVS stream
   reused this same pattern for its own mute nag, which was removed in #55.) */
.dealer-mute-wrap {
  position: relative;
  display: inline-flex;
}
.dealer-mute-hint {
  position: absolute;
  top: calc(100% + 10px);
  right: 0;
  z-index: 1080;               /* above the header chrome */
  display: flex;
  align-items: center;
  gap: 6px;
  /* The positioned ancestor (.dealer-mute-wrap) is only as wide as the button,
     so an auto-width absolute box would shrink-fit to a cramped column. Grow to
     the text's natural width (capped) and extend LEFTWARD from right:0. */
  width: max-content;
  max-width: min(78vw, 260px);
  padding: 7px 8px 7px 11px;
  background: var(--theme-primary, #ff5a00);
  color: #fff;
  border-radius: 8px;
  font-size: .8rem;
  font-weight: 600;
  line-height: 1.25;
  white-space: normal;
  box-shadow: 0 6px 18px rgba(0, 0, 0, .28);
  animation: soundHintIn .18s ease-out;
}
/* Upward-pointing arrow, aligned under the mute button (right side). */
.dealer-mute-hint::before {
  content: "";
  position: absolute;
  bottom: 100%;
  right: 14px;
  border: 6px solid transparent;
  border-bottom-color: var(--theme-primary, #ff5a00);
}
/* OK button — dismisses the hint WITHOUT enabling sound. A compact pill so it
   reads as a deliberate acknowledgement, not the enable action. */
.dealer-mute-hint__ok {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 34px;
  padding: 2px 10px;
  border: 0;
  border-radius: 999px;
  background: rgba(255, 255, 255, .22);
  color: #fff;
  font-size: .74rem;
  font-weight: 700;
  line-height: 1.4;
  cursor: pointer;
}
.dealer-mute-hint__ok:hover {
  background: rgba(255, 255, 255, .38);
}
@keyframes soundHintIn {
  from { opacity: 0; transform: translateY(-4px); }
  to   { opacity: 1; transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
  .dealer-mute-hint { animation: none; }
  #ivsUnmuteOverlay { animation: none; }
}

/* Header icon buttons on the white bar (e.g. the auction-sound mute
   #soundMuteBtn). style.css paints .btn-icon white-on-translucent-white for
   the OLD dark/orange bar — invisible on the light NF_2027 bar until hover
   changes the background. Repaint for the white bar so the control is always
   visible. Shared: both dealer.html and auctioneer.html load dealer.css and
   use body.dealer-theme, so this governs both. */
body.dealer-theme .app-header .btn-icon {
  background: var(--theme-white);
  border: 1px solid #d9d9d9;
  color: var(--theme-text);
}
body.dealer-theme .app-header .btn-icon:hover {
  background: #f5f5f5;
  border-color: #c9c9c9;
  color: var(--theme-text);
  transform: none;
}
body.dealer-theme .app-header .btn-icon.active {
  background: rgba(var(--theme-primary-rgb), 0.08);
  border-color: rgba(var(--theme-primary-rgb), 0.4);
  color: var(--theme-primary);
}
/* Visible keyboard focus ring — style.css tuned focus for the dark bar, so
   .btn-icon renders with no visible indicator on white. Match the avatar. */
body.dealer-theme .app-header .btn-icon:focus-visible {
  outline: 2px solid var(--theme-primary);
  outline-offset: 2px;
}

/* Auction-sound mute (#soundMuteBtn) — a BARE, borderless control that
   matches the language selector (.lang-menu__toggle): no box / border /
   background, and a larger glyph sized like the faded globe (1.25rem)
   rather than the boxed 36px .btn-icon default. Icon-only, no text label.
   Scoped to the id so the other boxed header controls (Settings cog,
   Timeline, History) keep the .btn-icon box. Shared by both dealer.html
   and auctioneer.html (both body.dealer-theme + dealer.css). The
   volume-high/volume-xmark glyphs are self-hosted mask icons sized via
   1em, so font-size drives their size. */
body.dealer-theme .app-header #soundMuteBtn.btn-icon {
  width: auto;
  height: auto;
  padding: 0.4rem 0.5rem;
  background: transparent;
  border: 0;
  border-radius: 0.5rem;
  color: var(--theme-text);
}
body.dealer-theme .app-header #soundMuteBtn.btn-icon:hover {
  background: rgba(0, 0, 0, 0.04);
  border: 0;
  color: var(--theme-text);
  transform: none;
}
body.dealer-theme .app-header #soundMuteBtn #soundMuteIcon {
  font-size: 1.25rem;
  line-height: 1;
  display: block;
}

/* 2E: the bare `hidden` attribute loses to Bootstrap's `.card{display:flex}`
   (equal origin, author rule wins). This guard makes `ivsCard.hidden = true`
   actually collapse the whole video card when the auctioneer is not
   streaming. */
#ivsCard[hidden] { display: none !important; }

/* #55b: over-video "tap to unmute" affordance. Positioned within the video
   host (#ivsVideoHost is position:relative) at bottom-centre so it clears the
   bottom-left #ivsLiveBadge. A semi-opaque dark pill with white icon+text for
   contrast over any frame; pointer-events:auto so it is clickable even though
   the sibling overlay layers are pointer-events:none. High z-index keeps it
   above the <video>. Shown/hidden via the bare `hidden` attribute, toggled by
   _ivsSyncUnmuteOverlay(); the guard below beats any Bootstrap display rule. */
/* #68: a real call-to-action, not a subtle pill. Brand-orange, centred over
   the video, larger type, a soft pulse to draw the eye — matching the
   prominence the old "Tap to hear the auctioneer" overlay had. Shown only
   while the live stream is muted (_ivsSyncUnmuteOverlay). */
#ivsUnmuteOverlay {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  z-index: 5;
  display: inline-flex;
  align-items: center;
  gap: .55rem;
  max-width: calc(100% - 24px);
  padding: 13px 26px;
  border: 0;
  border-radius: 999px;
  background: var(--theme-primary, #ff5a00);
  color: #fff;
  font-size: 1.02rem;
  font-weight: 700;
  letter-spacing: .01em;
  line-height: 1;
  white-space: nowrap;
  cursor: pointer;
  pointer-events: auto;
  box-shadow: 0 6px 22px rgba(0, 0, 0, .45);
  animation: ivsUnmutePulse 1.9s ease-in-out infinite;
}
#ivsUnmuteOverlay:hover {
  background: var(--theme-primary-hover, #e14e00);
  animation: none;
  transform: translate(-50%, -50%) scale(1.03);
}
#ivsUnmuteOverlay i { font-size: 1.1em; }
@keyframes ivsUnmutePulse {
  0%, 100% { box-shadow: 0 6px 22px rgba(0, 0, 0, .45); transform: translate(-50%, -50%) scale(1); }
  50%      { box-shadow: 0 8px 30px rgba(var(--theme-primary-rgb, 255, 90, 0), .6); transform: translate(-50%, -50%) scale(1.045); }
}
/* The bare `hidden` attribute must win over the inline-flex above. */
#ivsUnmuteOverlay[hidden] { display: none !important; }

/* (3.1) NF_2027: the profile control is a BARE solid-orange circular avatar
   used as the dropdown toggle — no bordered box / pill / background wrapper
   around it, and (to match the proto) the "Demo Dealer" name text + chevron
   are dropped. #dealerNameDisplay stays in the DOM (enterApp() writes it);
   #userMenuButton / #userMenuDropdown / #dealerAvatar and the dropdown
   behaviour are unchanged — only the surrounding chrome is stripped. */
body.dealer-theme .app-header .user-identity {
  background: transparent;
  border: 0;
  padding: 0;
  box-shadow: none;
}
body.dealer-theme .app-header .user-identity:hover {
  background: transparent;
  border: 0;
}
/* Keyboard focus ring — style.css paints a 60%-white outline (tuned for
   the old orange bar) that is invisible on the white NF_2027 bar. Repaint
   it in the theme primary so the avatar button has a visible focus
   indicator on all three white-bar pages (dealer / auctioneer /
   planned-lots). */
body.dealer-theme .app-header .user-identity:focus-visible {
  outline: 2px solid var(--theme-primary);
  outline-offset: 2px;
}
body.dealer-theme .app-header .user-identity__name { color: var(--theme-text); }
/* Drop the visible name block + chevron to match the proto (bare avatar).
   The name wrapper carries Bootstrap's `d-xl-block` (display:block !important
   at >=1200px), so a plain `display:none` loses the cascade there and the
   "Demo Dealer" name reappears on wide desktops. `!important` beats the
   utility so ONLY the orange circle shows at every width. #dealerNameDisplay
   stays in the DOM inside this hidden wrapper — enterApp() still writes it —
   and the mobile dropdown header (#dealerNameMobile) is a separate node that
   is unaffected. */
body.dealer-theme .app-header .user-identity__info { display: none !important; }
body.dealer-theme .app-header .user-identity .fa-angle-down { display: none; }

/* NF_2027 profile avatar: solid orange circle with a white person icon.
   #dealerAvatar keeps its (JS-written) initial as textContent — we hide the
   letter (color:transparent) and paint a fa-user glyph via ::before, so
   enterApp()'s `dealerAvatar.textContent = initial` write is untouched and
   the JS hook stays intact. Overrides the blue-gradient rule in style.css
   (`.user-identity--dealer .user-identity__avatar`) on higher specificity. */
body.dealer-theme .app-header .user-identity--dealer .user-identity__avatar {
  width: auto;
  height: auto;
  background: transparent;
  border: 0;
  color: transparent;
  font-size: 0;
  line-height: 1;
  position: relative;
}
body.dealer-theme .app-header .user-identity--dealer .user-identity__avatar::before {
  content: "\f2bd"; /* fa-circle-user (solid) — filled orange disc; the
                       person is a knock-out that reads white on the white
                       header, exactly like the prototype's .btn-user. */
  font-family: "Font Awesome 6 Free";
  font-weight: 900;
  font-size: 2rem; /* ~32px, matching the prototype avatar */
  line-height: 1;
  color: var(--theme-primary);
}

/* NF_2027 language selector — manual dropdown (no Bootstrap JS bundle).
   Globe + current locale code toggle on the white bar; a clean light
   dropdown listing every supported locale. Mirrors the user-menu
   show/hide pattern (hidden attribute + click-outside handler). */
body.dealer-theme .app-header .lang-menu { position: relative; }
/* (3.1) Bare language selector — current locale code + a faded globe icon,
   with NO box / border / background around it. Matches the CI prototype's
   top-right "EN" followed by a light-weight globe (no chevron). The dropdown
   itself keeps its bordered light panel; only the toggle is unboxed.
   toggleLangMenu()/#langMenuDropdown behaviour is unchanged. */
body.dealer-theme .app-header .lang-menu__toggle {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.4rem 0.5rem;
  background: transparent;
  border: 0;
  border-radius: 0.5rem;
  color: var(--theme-text);
  font-weight: 400;
  font-size: 0.85rem;
  letter-spacing: 0.02em;
  cursor: pointer;
  transition: background-color 0.15s ease;
}
body.dealer-theme .app-header .lang-menu__toggle:hover {
  background: rgba(0, 0, 0, 0.04);
}
/* Faded globe asset (assets/icons/globe.svg) sitting AFTER the locale code,
   mirroring the prototype's 20px, opacity-.3 light globe (which is notably
   larger than the "EN" code beside it). */
body.dealer-theme .app-header .lang-menu__globe {
  width: 1.25rem;
  height: 1.25rem;
  opacity: 0.35;
  display: block;
}
body.dealer-theme .app-header .lang-menu__dropdown {
  position: absolute;
  top: calc(100% + 0.4rem);
  right: 0;
  min-width: 180px;
  max-height: 320px;
  overflow-y: auto;
  padding: 0.35rem;
  background: var(--theme-white);
  border: 1px solid #e2e2e2;
  border-radius: 0.65rem;
  box-shadow: 0 12px 30px rgba(0, 0, 0, 0.14);
  z-index: 1040;
}
body.dealer-theme .app-header .lang-menu__dropdown[hidden] { display: none; }
body.dealer-theme .app-header .lang-menu__item {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0.5rem 0.7rem;
  border: 0;
  border-radius: 0.4rem;
  background: transparent;
  color: var(--theme-text);
  font-size: 0.85rem;
  font-weight: 500;
  text-align: left;
  cursor: pointer;
}
body.dealer-theme .app-header .lang-menu__item:hover { background: #f2f2f2; }
body.dealer-theme .app-header .lang-menu__item.is-active {
  color: var(--theme-primary);
  font-weight: 600;
}
body.dealer-theme .app-header .lang-menu__item .lang-menu__code {
  font-size: 0.72rem;
  color: var(--theme-text-muted);
  letter-spacing: 0.04em;
}
body.dealer-theme .app-header .lang-menu__item.is-active .lang-menu__code { color: var(--theme-primary); }

/* NF_2027 language selector (mobile collapse). The collapse has a dark navy
   backdrop, so the label reads light; the native select keeps its default
   light chrome. */
body.dealer-theme .mobile-lang {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  padding: 0.35rem 0;
}
body.dealer-theme .mobile-lang__label {
  display: flex;
  align-items: center;
  color: rgba(255, 255, 255, 0.85);
  font-size: 0.85rem;
  font-weight: 500;
}

/* Hamburger toggler — navbar-dark was removed, so Bootstrap paints no
   icon image; supply a dark one and a white/outlined button. */
body.dealer-theme .navbar-toggler {
  background: var(--theme-white);
  border-color: #d9d9d9;
}
body.dealer-theme .navbar-toggler:hover { background: #f5f5f5; }
body.dealer-theme .navbar-toggler:focus { box-shadow: 0 0 0 3px rgba(var(--theme-primary-rgb), 0.15); }
body.dealer-theme .navbar-toggler-icon {
  background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='%23212529' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");
  background-repeat: no-repeat;
  background-position: center;
  background-size: 100%;
}

    /* ----- Sub-header (lot label + state pill) REMOVED in 6.1 -----
       The lot label moved into the bid-panel heading (#lotNoLabel +
       #bidPanelLotName) and the state pill was dropped (lot/hammer state
       is conveyed by the in-bid-panel progress strip). The .dealer-subheader
       / .dealer-statepill rules — including the round-8 cluster centring
       (max-width: var(--dealer-cluster-max); margin-inline: auto) — are gone. */

    /* ----- Bid CTA (2C / NF_2027) -----
       #bidButton is the standout primary action: full-width, orange
       (--theme-primary = #ff5a00), 8px radius, three STACKED lines matching
       the NF_2027 prototype:
         line 1 — gavel (opacity-75) + #bidButtonLabel (small, UPPERCASE verb)
         line 2 — #bidButtonAmount (bold / large amount, full formatEUR output)
         line 3 — #bidButtonVat (small, 50% opacity VAT disclosure)
       The gavel is a SIBLING of #bidButtonLabel (never nested inside it):
       renderBidPanel() reassigns #bidButtonLabel.textContent every pass, which
       would wipe a nested icon. Each span's textContent/display is written by
       renderBidPanel(); this only styles them. On SOLD/YOU-WON the amount +
       VAT lines are display:none'd, collapsing to the centered label line. */
    .dealer-bid-cta {
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      gap: 0.1rem;
      width: 100%;
      min-height: 64px;
      padding: 0.55rem 1rem;
      background: var(--theme-primary);
      border: 1px solid var(--theme-primary);
      border-radius: 8px;
      color: #fff;
      line-height: 1.15;
      transition: background-color 0.15s ease, border-color 0.15s ease;
    }
    .dealer-bid-cta:hover:not(:disabled),
    .dealer-bid-cta:focus-visible:not(:disabled) {
      background: var(--theme-primary-hover);
      border-color: var(--theme-primary-hover);
      color: #fff;
    }
    .dealer-bid-cta:disabled {
      background: var(--theme-primary-disabled);
      border-color: var(--theme-primary-disabled);
      color: #fff;
      box-shadow: none;
      opacity: 1;
    }
    /* Line 1 — gavel + verb label */
    .dealer-bid-cta__line--label {
      display: inline-flex;
      align-items: center;
      gap: 0.35rem;
      font-size: 0.72rem;
      font-weight: 400;
      letter-spacing: 0.03em;
      text-transform: uppercase;
      line-height: 1;
    }
    .dealer-bid-cta__icon { font-size: 0.85rem; opacity: 0.75; flex: 0 0 auto; }
    /* Line 2 — a SMALL currency CODE + a bold/large grouped NUMBER (proto
       3.4). The wrapper (.dealer-bid-cta__amount) carries the bold/large type;
       #bidButtonAmount (the number) inherits it, while #bidButtonCode is
       knocked down to a small, slightly-muted code that sits just left of the
       number on the same baseline. */
    .dealer-bid-cta__amount {
      font-size: 1.4rem;
      font-weight: 700;
      letter-spacing: 0.01em;
      line-height: 1.1;
      white-space: nowrap;
    }
    .dealer-bid-cta__code {
      font-size: 0.72rem;
      font-weight: 400;
      letter-spacing: 0.04em;
      opacity: 0.85;
      margin-right: 0.3rem;
      vertical-align: baseline;
    }
    /* Line 3 — VAT disclosure */
    .dealer-bid-cta__vat {
      font-size: 0.72rem;
      font-weight: 400;
      opacity: 0.5;
      line-height: 1.2;
      text-align: center;
      white-space: normal;
    }

    /* ----- Bid stepper flanking the CTA (4.7) -----
       The readonly #dealerBidInput was removed; #bidMinus / #bidPlus now sit
       either side of the orange CTA and step the amount shown ON the button
       (#bidButtonAmount). The row stretches its children to equal height so
       the two square −/+ buttons match the tall multi-line CTA. The CTA loses
       Bootstrap .w-100 (width:100%!important, which would break this flex row)
       and flexes to fill the space between the two step buttons. */
    /* (6.1b) Vehicle summary shown inside the bid panel, just above the CTA
       row — the lot identity relocated from the panel heading so it sits next
       to the bid button. Mirrors the sticky bar's __veh type scale; the card
       has width so it may wrap. :empty collapses the gap when there is no
       live car (pre-auction / between lots). */
    /* ---- Dealer bid layout: horizontal (NF_2027 prototype) ----
       Vehicle info on the LEFT (flex:1), the bid group on the RIGHT: the orange
       CTA with the +/- steppers stacked to its right, and the min-bid hint (+
       Reset link) directly BENEATH the button. This is THE dealer bid layout —
       the earlier stacked variant and its side-by-side preview were retired in
       favour of it. */
    /* Horizontal 3-column grid per the NF_2027 prototype (proto.autorola.com/
       NF_2027/showcar.html): vehicle info hard-LEFT · Highest bid + reserve
       status bar-CENTERED · CTA + steppers hard-RIGHT. The 1fr auto 1fr track
       sizing is what centers the middle in the bar regardless of the (unequal)
       vehicle / button widths — a plain flex row can't do that. Mirrors the
       sticky bar so the two bid surfaces read identically. */
    .dealer-bid2__inner {
      display: grid;
      grid-template-columns: 1fr auto 1fr;
      align-items: center;
      gap: 0.9rem;
    }
    /* Vehicle info: hard-left, stacked — title, location, "#lot · mileage". */
    /* max-width:100% + overflow:hidden bound the block to its 1fr grid track so
       the title's own text-overflow:ellipsis actually engages. Without this,
       justify-self:start sizes the block to its (nowrap) content, letting a long
       vehicle name overflow the track and collide with the centered highest-bid
       column at narrow panel widths. */
    .dealer-bid2__veh { min-width: 0; max-width: 100%; overflow: hidden; justify-self: start; text-align: center; padding-inline-start: clamp(1.5rem, 5vw, 6rem); }
    .dealer-bid2__veh-title {
      font-weight: 600; font-size: 0.9rem; color: var(--theme-text);
      white-space: nowrap; overflow: hidden; text-overflow: ellipsis; line-height: 1.25;
    }
    .dealer-bid2__veh-loc,
    .dealer-bid2__veh-ids {
      font-size: 0.78rem; color: var(--theme-text-muted); margin-top: 0.05rem;
      white-space: nowrap; overflow: hidden; text-overflow: ellipsis; line-height: 1.25;
    }
    .dealer-bid2__veh-title:empty, .dealer-bid2__veh-loc:empty, .dealer-bid2__veh-ids:empty { display: none; }
    /* Middle column: Highest bid + reserve status, bar-centered between the
       vehicle info and the CTA (prototype parity). #bidLeader is kept in the DOM
       for the e2e specs but hidden — the amount carries the signal. */
    .dealer-bid2__status { justify-self: center; text-align: center; min-width: 0; }
    .dealer-bid2__status-label {
      font-size: 0.64rem; text-transform: uppercase; letter-spacing: 0.04em;
      color: var(--theme-text-muted); line-height: 1.2; display: block;
    }
    .dealer-bid2__status-amt {
      font-weight: 600; font-size: 1.05rem; color: var(--theme-text); line-height: 1.25;
    }
    .dealer-bid2__status #bidLeader { display: none; }
    /* Right column: the CTA+steppers row, hard-right, with the hint stacked
       beneath the button (so it sits under the CTA, not full-width under the
       whole row). */
    .dealer-bid2__bidgroup { justify-self: end; display: flex; flex-direction: column; gap: 0.4rem; }
    /* CTA + steppers side by side; stretch so the stepper column matches the
       button height exactly (the button is the tallest child). */
    .dealer-bid2__controls { display: flex; align-items: stretch; gap: 0.5rem; }
    /* Cap the CTA (overrides .dealer-bid-cta width:100%) so it sits beside the
       vehicle info rather than filling the row — same sizing as the sticky btn. */
    .dealer-bid2__btn { flex: 0 0 auto; width: auto; min-width: 190px; max-width: 320px; }
    /* +/− steppers STACKED vertically to the right of the button; together they
       span the button height (each flexes to half). */
    .dealer-bid2__steps { flex: 0 0 auto; display: flex; flex-direction: column; gap: 0.3rem; }
    /* Scoped ABOVE .dealer-bid-step-btn (which is defined later with flex:0 0
       auto and same specificity) so these steppers actually stretch to share
       the button height instead of collapsing to their content height. */
    .dealer-bid2__steps .dealer-bid2__step {
      flex: 1 1 0; min-width: 48px; min-height: 0; padding: 0 0.5rem; font-size: 1.35rem;
    }
    /* Min-bid hint (+ Reset): a small centered line directly under the button. */
    .dealer-bid2__hint {
      text-align: center; font-size: 0.78rem; color: var(--theme-text-muted);
    }
    @media (max-width: 575.98px) {
      /* Narrow: stack the three columns (vehicle / highest bid / CTA); drop the
         "#lot · mileage" line. The +/- steppers STAY stacked to the RIGHT of the
         button (same as desktop) — they do NOT reflow to a row below. Override
         the desktop grid back to a flex column. */
      .dealer-bid2__inner { display: flex; flex-direction: column; align-items: stretch; gap: 0.5rem; }
      /* Stacked & centered — drop the desktop left inset so the block stays centered. */
      .dealer-bid2__veh, .dealer-sticky-bid__veh { padding-inline-start: 0; }
      .dealer-bid2__veh-ids { display: none; }
      /* Keep the sticky bar's vehicle block identical to the in-panel one:
         drop the "#lot · mileage" line at the SAME width (not only ≤480). */
      .dealer-sticky-bid__veh-ids { display: none; }
      .dealer-bid2__bidgroup { align-self: stretch; }
      .dealer-bid2__btn { flex: 1 1 auto; max-width: none; }
    }

    .dealer-bid-step-btn {
      flex: 0 0 auto;
      min-width: 52px;
      display: flex;
      align-items: center;
      justify-content: center;
      font-size: 1.5rem;
      line-height: 1;
      padding: 0 0.75rem;
    }
    /* Grayed-out affordance when the − step can't go lower (already at the
       asking/minimum floor). renderBidPanel() sets #bidMinus.disabled; make
       that unmistakably read as disabled (muted fill + text, no pointer). */
    .dealer-bid-step-btn:disabled,
    .dealer-bid-step-btn[disabled] {
      background: #f1f3f4;
      border-color: #dcdfe2;
      color: var(--theme-neutral-gray);
      opacity: 0.65;
      cursor: not-allowed;
    }

    /* ----- Sticky bottom bid bar (2.3) -----
       Fixed to the viewport bottom, full-width, orange CTA matching
       #bidButton. Hidden by default; syncStickyBid() adds `.is-visible`
       only when the main #bidButton is scrolled out of view AND the lot is
       biddable. z-index sits BELOW the fixed banners (1080) so a banner
       shown over a live biddable lot is never covered by the bar. A subtle
       top shadow lifts it off the page; safe-area padding keeps the button
       clear of the iOS home indicator. */
    .dealer-sticky-bid {
      position: fixed;
      left: 0;
      right: 0;
      bottom: 0;
      z-index: 1040;
      display: none;
      padding: 0.6rem 0.9rem calc(0.6rem + env(safe-area-inset-bottom, 0px));
      background: var(--theme-white);
      border-top: 1px solid #e2e2e2;
      box-shadow: 0 -4px 16px rgba(0, 0, 0, 0.12);
    }
    .dealer-sticky-bid.is-visible { display: block; }
    /* (3.2) Inner: 3-column grid — vehicle info hard-LEFT · highest bid + reserve
       bar-CENTERED · CTA hard-RIGHT. 1fr auto 1fr centers the middle in the bar
       regardless of the (unequal) vehicle / button widths. Capped to the dealer
       cluster width so it lines up with the content. Matches the in-panel bid
       area and the NF_2027 prototype. */
    .dealer-sticky-bid__inner {
      display: grid; grid-template-columns: 1fr auto 1fr;
      align-items: center; gap: 0.9rem;
      max-width: var(--dealer-cluster-max); margin-inline: auto;
    }
    /* Left column, three lines centered within the column — identical to the
       in-panel .dealer-bid2__veh (NF_2027 prototype parity). */
    /* Same track-bounding as .dealer-bid2__veh — without max-width/overflow a
       long vehicle name overflows the 1fr track and collides with the centered
       highest-bid column at narrow widths. */
    .dealer-sticky-bid__veh { min-width: 0; max-width: 100%; overflow: hidden; justify-self: start; text-align: center; padding-inline-start: clamp(1.5rem, 5vw, 6rem); }
    .dealer-sticky-bid__veh-title {
      font-weight: 600; font-size: 0.9rem; color: var(--theme-text);
      white-space: nowrap; overflow: hidden; text-overflow: ellipsis; line-height: 1.25;
    }
    .dealer-sticky-bid__veh-loc,
    .dealer-sticky-bid__veh-ids {
      font-size: 0.78rem; color: var(--theme-text-muted); margin-top: 0.05rem;
      white-space: nowrap; overflow: hidden; text-overflow: ellipsis; line-height: 1.25;
    }
    .dealer-sticky-bid__veh-title:empty, .dealer-sticky-bid__veh-loc:empty, .dealer-sticky-bid__veh-ids:empty { display: none; }
    /* Middle section: highest bid + reserve status, bar-centered between the
       vehicle info and the button (NF_2027 prototype parity). */
    .dealer-sticky-bid__status { justify-self: center; text-align: center; min-width: 0; }
    .dealer-sticky-bid__status-label {
      font-size: 0.64rem; text-transform: uppercase; letter-spacing: 0.04em;
      color: var(--theme-text-muted); line-height: 1.2; display: block;
    }
    .dealer-sticky-bid__status-amt {
      font-weight: 600; font-size: 1.05rem; color: var(--theme-text); line-height: 1.25;
    }
    .dealer-sticky-bid__status-reserve {
      font-size: 0.74rem; color: var(--theme-text-muted); line-height: 1.2;
    }
    /* Hard-right, content-sized (no longer fills the bar); capped to a
       comfortable band. Overrides .dealer-bid-cta width:100%. */
    .dealer-sticky-bid__btn {
      justify-self: end; width: auto; min-width: 200px; max-width: 360px;
    }
    @media (max-width: 480px) {
      /* Drop the middle section to protect the button; the grid collapses to
         vehicle (left) | button (right). Let the button fill its column. */
      .dealer-sticky-bid__status { display: none; }
      .dealer-sticky-bid__btn { min-width: 150px; max-width: none; }
      .dealer-sticky-bid__veh-ids { display: none; }
    }
    /* The sticky bar's button now carries the SAME `.dealer-bid-cta` class as
       the primary #bidButton and mirrors its exact 3-line inner structure
       (.dealer-bid-cta__line--label / __amount / __vat), so all typography —
       font-size, weight, letter-spacing, opacity, line breaks — comes from the
       one shared type scale. There is deliberately no sticky-specific type
       override here any more: the earlier bespoke rules (single 1.1rem line,
       __main/__amount/__vat) were exactly what made the sticky button diverge
       from the primary CTA. `.dealer-bid-cta` already sets width:100%,
       min-height, padding, background/border/radius/shadow, and the
       hover/disabled treatments, and the `.dealer-bid2__btn` override that
       caps its width does NOT match here (the sticky button is not inside the
       bid group), so it fills the fixed bar full-width. */
    /* Reserve space at the bottom of the page while the sticky bar is shown
       so the fixed bar never overlaps the lot list (or any bottom content).
       Matches the taller two-line bar; applies at every width because the bar
       is full-width fixed-bottom. */
    /* Reserve the bar's ACTUAL rendered height (published by syncStickyBid()
       as --dealer-sticky-height; already includes the bar's safe-area padding)
       so page content is never hidden behind it. Falls back to the old 96px
       for the brief moment before the JS runs. */
    /* Reserve on .app-container (the scroll container), NOT body: body is
       pinned to height:100% (one viewport) so its padding never reaches the
       document bottom. See the .app-container base rule in style.css. */
    body.dealer-sticky-visible .app-container {
      padding-bottom: var(--dealer-sticky-height, calc(96px + env(safe-area-inset-bottom, 0px)));
    }

    /* ----- Sticky bid bar vs. debug panel co-existence -----
       The debug panel (style.css) is also `position: fixed; bottom: 0`, sits
       at the very bottom, reserves its own slice via
       `body.has-debug-panel .app-container { padding-bottom: --debug-panel-height }`,
       and carries a far higher z-index (99999). Left alone the two fixed-bottom
       elements stack at bottom:0 and the panel covers the bid bar, while the
       two single-class .app-container-padding rules clobber each other (so
       content is reserved for only one). Fix both: lift the bar to sit directly
       ABOVE the panel, and reserve space for the two of them together so the lot
       content shrinks instead of being overlapped by either. */
    body.has-debug-panel .dealer-sticky-bid {
      bottom: var(--debug-panel-height);
    }
    /* Both fixed elements shown → reserve the sum on .app-container. The two
       body classes + .app-container give specificity (0,3,1), which beats each
       single-class reservation above so the combined padding wins. */
    body.has-debug-panel.dealer-sticky-visible .app-container {
      padding-bottom: calc(var(--debug-panel-height) + var(--dealer-sticky-height, calc(96px + env(safe-area-inset-bottom, 0px))));
    }

    /* ----- Bid centre-piece + Bid history unit (2+3+7) -----
       The bidding + bid-history live as TWO side-by-side panels inside a
       width-CONSTRAINED unit so the bid area no longer spans the whole
       column. DOM order is bid-then-history (keeps the bid controls first
       in tab order, and mobile stacks bid → history). On desktop the split
       is a NORMAL flex `row`, which keeps the same left→right visual order
       as the DOM: the bid centre-piece on the LEFT and the narrow history
       panel on the RIGHT — per the user (2.1). */
    .dealer-bid-unit {
      max-width: 1180px;
    }
    .dealer-bid-split {
      display: flex;
      flex-direction: column;   /* mobile: bid on top, history below */
      gap: 0.75rem;
    }
    @media (min-width: 992px) {
      .dealer-bid-split {
        flex-direction: row;   /* bid (1st child) → left, history → right */
        align-items: stretch;  /* (3.5) both panels take the ROW height */
      }
      /* (3.5) The bid panel dictates the row height; the history panel
         stretches to match (align-items:stretch) and clips internally
         (overflow:hidden) so its bids list scrolls inside rather than
         growing the row. min-height:0 lets the flex column shrink to the
         stretched height instead of its content height. */
      .dealer-bidhistory-panel {
        /* (clip fix) Widened from clamp(220px,32%,280px): at 992/1280 the old
           basis was too narrow and the single-line row (name + You + amount +
           HH:MM:SS time) clipped the amount/time under overflow-x:hidden. This
           basis clears that content at every breakpoint. (The former >=1400
           override that widened it further was removed in 5.1, along with the
           3-col vehicle-left layout it belonged to.) */
        flex: 0 0 clamp(240px, 34%, 300px);
        min-height: 0;
        overflow: hidden;
      }
      .dealer-bidhistory-panel .card-body { min-height: 0; }
      .dealer-bid-panel        { flex: 1 1 auto; min-width: 0; }
    }

    /* (3.5) Bids list scroller — fills the bid-history card body and scrolls
       internally. No fixed max-height (that used to let history exceed the
       bid panel). `flex-grow-1` is kept on the element so the mobile cap
       (.dealer-grid .card-body > .flex-grow-1 { max-height:240px }) still
       applies on phones, where the two panels stack instead of sharing a
       row height. */
    .dealer-bidhistory__scroll {
      flex: 1 1 0;
      min-height: 0;
      overflow-y: auto;
      /* (4.4) When the vertical scrollbar appears it narrows the content box;
         the single-line rows (name truncates via .dealer-bidrow__name) shrink
         to fit, and this guard makes sure no spurious HORIZONTAL scrollbar is
         ever shown alongside it. */
      overflow-x: hidden;
    }

    /* ----- Compact SINGLE-LINE bid-history rows (3.8) -----
       Now that "Bidder-" is stripped the names are short, so each bid is one
       <td> on ONE line: name (+ optional "You" badge) pinned LEFT, amount +
       muted time pinned RIGHT, no wrap/overlap. #bidTableBody still emits one
       <tr> per bid (e2e-load-bearing) and the amount text is preserved.
       Own-bid rows keep .table-success. */
    .dealer-bidrow { vertical-align: middle; }
    .dealer-bidrow--compact {
      display: flex;
      align-items: baseline;
      gap: 0.5rem;
      line-height: 1.3;
      white-space: nowrap;
    }
    .dealer-bidrow__name {
      flex: 1 1 auto;
      min-width: 0;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }
    /* "You" badge sits just after the name; amount is pushed to the right
       edge (margin-left:auto) with the time trailing it. */
    .dealer-bidrow__you { flex: 0 0 auto; }
    .dealer-bidrow__amount {
      flex: 0 0 auto;
      margin-left: auto;
      font-weight: 700;
      color: var(--theme-text);
    }
    .dealer-bidrow__time {
      flex: 0 0 auto;
      font-size: 0.72rem;
      color: var(--theme-text-muted);
      white-space: nowrap;
    }


    /* ----- Compact progress strip (2D) -----
       Wrapper holding the hammer / waiting / withdraw chip strips now that
       they live inside the bid panel, just under #dealerBidWrap. A thin
       separator divides them from the bid-history list below. Only one of
       the three inner strips is visible at a time (updateWithdrawChips
       toggles their display). */
    .dealer-progress-strip {
      padding: 0.55rem 0.75rem;
      border-bottom: 1px solid #eee;
    }

    /* ----- Reserve-status line -----
       "Reserve met" / "Reserve not met" shown in the middle column, directly
       under the highest bid (driven off onSale — see the HTML comment on
       #reserveStatus). Uses the site's normal text colour for BOTH states (the
       words carry the signal) — no brown/green tint. Centered, matching the
       prototype's middle column and the sticky bar. */
    /* ----- "Above reserve" ring on the bid panel -----
       A 4px green ring around the whole dealer bid panel while the lot is on
       sale, so going on sale is unmissable (mve finding). Implemented as an
       absolutely-positioned ::after OVERLAY, deliberately not a real border:
         - a real 4px border would grow the card by 3px per side (it already
           carries Bootstrap's 1px .border), and the panel must stay the SAME
           SIZE; an overlay is out of flow so the footprint is untouched.
         - `border-radius: inherit` reproduces the card's own corner radius
           exactly, so the ring keeps the existing rounded corners.
         - an inset box-shadow would have to fight `.shadow-sm`'s !important.
         - #dealerBidPanel already sets position:relative, so the overlay
           anchors to the card with no extra positioning context.
       pointer-events:none keeps the CTA / steppers clickable through it. No
       z-index on purpose: as the last child it paints above the card's own
       content, but any positioned overlay that is meant to dominate (the
       auctioneer message-ack cover) still paints over it. */
    #dealerBidPanel.is-above-reserve::after {
      content: "";
      position: absolute;
      inset: 0;
      border: 4px solid var(--theme-success);
      border-radius: inherit;
      pointer-events: none;
    }

    .reserve-status {
      display: flex;
      align-items: center;
      justify-content: center;
      font-size: 0.8rem;
      font-weight: 400;
      text-align: center;
      color: var(--theme-text);
    }

    /* Met state — a high-contrast dark chip matching the reference prototype
       (fill #212529 sampled from it, white label, 0.375rem radius, NOT a
       pill). The plain-text treatment above did not read clearly enough as
       the "this lot will hammer" signal. Only the MET state is promoted; the
       not-met state stays quiet text, so the chip's presence is the signal.
       The chip is the inner span, not .reserve-status itself — that wrapper
       is a full-width centring flex row, so filling it would stretch the
       chip across the whole column. */
    .reserve-status--met #reserveStatusText {
      display: inline-flex;
      align-items: center;
      gap: 0.3rem;
      background-color: #212529;
      color: #fff;
      border-radius: 0.375rem;
      padding: 0.2rem 0.5rem;
      font-weight: 600;
    }
    /* ::before, not an inline <svg>: updateReserveStatus() writes
       textContent, which would wipe a real child node. */
    .reserve-status--met #reserveStatusText::before {
      content: "";
      flex: 0 0 auto;
      width: 0.85em;
      height: 0.85em;
      background-color: currentColor;
      -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M4 12.5l5 5L20 6.5' fill='none' stroke='%23000' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat;
      mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M4 12.5l5 5L20 6.5' fill='none' stroke='%23000' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat;
    }

    /* ----- Hammer chip strip (quiet, 2D) -----
       Restyle of #hammerProgress (specs/screens/live-auction.md → Dealer status
       card). The DOM still carries .hammer-progress__step[data-step] nodes —
       updateHammerBanner() reads them for is-active / is-done — but they now
       render as a QUIET inline row of small text steps: no circular icon
       badges, normal case, matching the sticky bar's calm status style. Only
       the ACTIVE step is gently tinted so the four-step progression (accepting
       → going once → going twice → sold) still reads; the colour semantics are
       kept but soft. */
    .hammer-chips {
      background: transparent;
      border: 0;
      padding: 0;
    }
    .hammer-chips .hammer-progress__track,
    .hammer-chips .hammer-progress__indicator { display: none; }
    .hammer-chips .hammer-progress__steps {
      display: grid;
      grid-template-columns: repeat(4, 1fr);
      gap: 0.4rem;
      margin: 0;
    }
    .hammer-chips .hammer-progress__step {
      background: transparent;
      border: 1px solid transparent;
      border-radius: 8px;
      /* TWICE THE HEIGHT (mve, for dealer engagement). Driven by min-height
         plus centring rather than padding alone, so a chip that wraps to two
         lines is exactly as tall as a one-line chip - the strip cannot change
         height as the labels change, which is what kept the
         navbar -> strip -> content rhythm steady in the first place. */
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      min-height: 3rem;
      padding: 0.45rem 0.35rem;
      font-size: 0.8rem;
      font-weight: 400;
      color: var(--theme-text-muted);
      text-align: center;
      line-height: 1.2;
      /* The accent bar below grows from 0, so height is animated too. */
      transition: background-color 0.2s ease, color 0.2s ease;
      position: relative;
      overflow: hidden;
    }
    /* Active step — a soft tint carries the progression; semantics kept but
       quiet (no loud fills, no icon badges). NONE "Accepting Bids" (only shown
       above reserve, once onSale is true) green; GOING once/twice amber; SOLD
       green. */
    /* Active step. Tints are roughly double their previous strength: at the
       old 8-12% the extra height read as empty space rather than emphasis.
       Still tints, not fills - the strip stays a progress indicator, not a
       banner. */
    .hammer-chips .hammer-progress__step.is-active {
      background: rgba(255, 90, 0, 0.16);
      color: var(--theme-primary);
      font-weight: 600;
    }
    .hammer-chips .hammer-progress__step[data-step="NONE"].is-active {
      background: rgba(var(--theme-success-rgb), 0.20);
      color: var(--theme-success-dark);
    }
    .hammer-chips .hammer-progress__step[data-step="GOING_ONCE"].is-active,
    .hammer-chips .hammer-progress__step[data-step="GOING_TWICE"].is-active {
      /* --pulse-rgb is the single source of the going-step colour: the static
         background here, the pulse keyframe, and the reduced-motion fallback
         all read it, so a per-viewer override (leading / outbid, below) moves
         all three together. Hard-coding amber here left a state where a
         leading dealer got an amber chip with green text if the animation was
         not running. */
      --pulse-rgb: 255, 193, 7;
      background: rgba(var(--pulse-rgb), 0.24);
      color: #6d4e00;
    }
    .hammer-chips .hammer-progress__step[data-step="SOLD"].is-active {
      background: rgba(25, 135, 84, 0.20);
      color: var(--theme-success-dark);
    }
    /* Accent bar along the bottom of the active chip, in that step's own
       colour. It reads at a glance from across a room, which the tint alone
       does not, and it uses the height we just gained instead of padding it
       out. currentColor, so each step's bar matches its text automatically. */
    /* Pulse on the ADVANCING hammer steps (mve). ~1.2 s cycle against the
       ceremony's ~4 s dwell, so each step pulses about three times: read as
       urgency, not as a strobe.

       Only GOING_ONCE / GOING_TWICE. Not SOLD (terminal - it holds until the
       next lot, so the pulse would never stop on a settled outcome) and not
       NONE / "Accepting bids" (can sit for minutes - the persistent element
       06-motion.md's rule explicitly warns against pulsing).

       A box-shadow pulse, per the design language's "pulsing shadows are for
       state". Shadow and background only: nothing animates layout, so the
       doubled-height strip cannot reflow mid-ceremony. */
    /* Driven by --pulse-rgb so ONE keyframe serves all three per-viewer
       colours below. Three near-identical keyframes would drift apart the
       first time the timing is tuned. */
    @keyframes hammer-step-pulse {
      0%, 100% {
        box-shadow: 0 0 0 0 rgba(var(--pulse-rgb), 0);
        background-color: rgba(var(--pulse-rgb), 0.24);
      }
      50% {
        box-shadow: 0 0 0 4px rgba(var(--pulse-rgb), 0.35);
        background-color: rgba(var(--pulse-rgb), 0.38);
      }
    }
    .hammer-chips .hammer-progress__step[data-step="GOING_ONCE"].is-active,
    .hammer-chips .hammer-progress__step[data-step="GOING_TWICE"].is-active {
      animation: hammer-step-pulse 1.2s ease-in-out infinite;
    }

    /* ----- Per-viewer hammer colour (mve) -----
       The same auction moment reads differently for each dealer:
         (no attribute / "none") amber   — you have not bid on this lot
         "leading"               green   — you are the high bidder
         "outbid"                orange  — you bid, someone is above you
       Green deliberately reuses the ACCEPTING BIDS success colour, so "green"
       means the same thing everywhere in the strip. Orange is the brand
       primary — the sharpest colour in the palette, and the one already used
       for calls to action, which is right for "bid again or lose it".

       Scoped to the two GOING steps on purpose. NONE and SOLD keep their fixed
       meaning: "Accepting bids" is not about this dealer, and a SOLD lot's
       outcome is carried by the sold banner and the You-won badge. */
    #hammerProgress[data-my-position="leading"] .hammer-progress__step[data-step="GOING_ONCE"].is-active,
    #hammerProgress[data-my-position="leading"] .hammer-progress__step[data-step="GOING_TWICE"].is-active {
      --pulse-rgb: var(--theme-success-rgb);
      color: var(--theme-success-dark);
    }
    #hammerProgress[data-my-position="outbid"] .hammer-progress__step[data-step="GOING_ONCE"].is-active,
    #hammerProgress[data-my-position="outbid"] .hammer-progress__step[data-step="GOING_TWICE"].is-active {
      --pulse-rgb: 255, 90, 0;
      color: var(--theme-primary);
    }
    /* Accessibility: hold the peak tint so the ACTIVE state is still obvious,
       and drop the motion entirely. */
    @media (prefers-reduced-motion: reduce) {
      /* Hold the pulse's peak tint and drop the motion. Uses --pulse-rgb, so
         it picks up the per-viewer colour rather than snapping back to amber
         for a dealer who is leading or outbid. */
      .hammer-chips .hammer-progress__step[data-step="GOING_ONCE"].is-active,
      .hammer-chips .hammer-progress__step[data-step="GOING_TWICE"].is-active {
        animation: none;
        background-color: rgba(var(--pulse-rgb), 0.38);
      }
    }

    .hammer-chips .hammer-progress__step.is-active::after,
    .withdraw-chips .hammer-progress__step.is-active::after {
      content: "";
      position: absolute;
      left: 0;
      right: 0;
      bottom: 0;
      height: 3px;
      background: currentColor;
      opacity: 0.85;
    }
    .hammer-chips .hammer-progress__step.is-done {
      opacity: 0.7;
    }

    /* ----- Withdraw chip strip -----
       Sibling of .hammer-chips. Shown in place of the hammer strip while
       a withdraw countdown is active (and held until the next lot is
       presented when it finalises to WITHDRAWN). The three chips
       (Accepting bids → Countdown → Withdrawn) mirror the hammer-chip
       dimensions so the navbar→strip→content rhythm stays steady when the
       strips swap. The `.hammer-progress__step` class is reused so
       updateWithdrawChips() can lean on the same data-step / is-active /
       is-done conventions as the hammer code.

       Colour: warning-amber for Accepting-bids/Countdown (the dealer can
       still affect the outcome by bidding); deep red `#dc3545` for
       Withdrawn (the lot has been pulled). */
    .withdraw-chips {
      background: transparent;
      border: 0;
      padding: 0;
    }
    /* (6.2) Three equal-width steps (NOTICE / COUNTDOWN / WITHDRAWN), mirroring
       the QUIET hammer strip — no icon badges, normal case, muted text; only
       the active step is softly tinted. Flex row so it wraps on narrow phones. */
    .withdraw-chips .hammer-progress__steps {
      display: flex;
      align-items: flex-start;
      justify-content: center;
      flex-wrap: wrap;
      gap: 0.4rem;
      margin: 0;
    }
    .withdraw-chips .hammer-progress__step {
      flex: 1 1 0;
      min-width: 0;
      background: transparent;
      border: 1px solid transparent;
      border-radius: 8px;
      /* TWICE THE HEIGHT (mve, for dealer engagement). Driven by min-height
         plus centring rather than padding alone, so a chip that wraps to two
         lines is exactly as tall as a one-line chip - the strip cannot change
         height as the labels change, which is what kept the
         navbar -> strip -> content rhythm steady in the first place. */
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      min-height: 3rem;
      padding: 0.45rem 0.35rem;
      font-size: 0.8rem;
      font-weight: 400;
      color: var(--theme-text-muted);
      text-align: center;
      line-height: 1.2;
      /* The accent bar below grows from 0, so height is animated too. */
      transition: background-color 0.2s ease, color 0.2s ease;
      position: relative;
      overflow: hidden;
    }
    /* Active states — soft amber for the NOTICE warning step, soft red for the
       withdrawn-final step. */
    .withdraw-chips .hammer-progress__step[data-step="NOTICE"].is-active {
      background: rgba(245, 158, 11, 0.24);
      color: #6d4e00;
      font-weight: 600;
    }
    .withdraw-chips .hammer-progress__step[data-step="WITHDRAWN"].is-active {
      background: rgba(220, 53, 69, 0.18);
      color: #a02330;
      font-weight: 600;
    }
    .withdraw-chips .hammer-progress__step.is-done {
      opacity: 0.7;
    }
    /* (6.2) COUNTDOWN active — amber like NOTICE. While counting, the step's
       label is the live remaining second (5 → 1): scale it up so the number
       is the focal point (there is no longer an icon above it). */
    .withdraw-chips .hammer-progress__step[data-step="COUNTDOWN"].is-active {
      --pulse-rgb: 245, 158, 11;
      background: rgba(var(--pulse-rgb), 0.20);
      color: #6d4e00;
      font-size: 1.15rem;
      font-weight: 700;
      position: relative;
      overflow: hidden;
    }
    /* Pulse on each counted number (mve: "please add pulse to 5, 4, 3, 2, 1
       for withdrawing"). Reuses the hammer strip's keyframe via --pulse-rgb, so
       there is one pulse definition in the file rather than two that drift.
       1.2 s against the 3 s step means about two and a half beats per number,
       and updateWithdrawChips() RESTARTS the animation on every digit change
       (the same once-per-digit hook that speaks the clip), so the first beat
       always lands on the new number instead of drifting against it. */
    .withdraw-chips .hammer-progress__step[data-step="COUNTDOWN"].is-active.wc-pulse {
      animation: hammer-step-pulse 1.2s ease-in-out infinite;
    }
    @media (prefers-reduced-motion: reduce) {
      .withdraw-chips .hammer-progress__step[data-step="COUNTDOWN"].is-active.wc-pulse {
        animation: none;
        background-color: rgba(var(--pulse-rgb), 0.38);
      }
    }

    /* (The pre-auction .waiting-chips strip was removed — it duplicated the
       #bidPanelWaiting body placeholder; see dealer.html. dealer-legacy.html
       keeps its own copy inline.) */

    /* ----- Lot alarm full-viewport flash -----
       Orange wash that fades back to the dealer screen so the dealer
       can't miss the alarm even when their attention is elsewhere
       (specs/features/14-dealer-lot-alarms.md → "Don't lose the
       notification because the dealer was in another tab"). */
    .lot-alarm-overlay {
      position: fixed; inset: 0;
      background: rgba(255, 140, 0, 0.55);
      pointer-events: none;
      z-index: 2000;
      opacity: 0;
      animation: lot-alarm-flash 1.6s ease-out forwards;
    }
    @keyframes lot-alarm-flash {
      0%   { opacity: 0; }
      10%  { opacity: 0.85; }
      30%  { opacity: 0.55; }
      55%  { opacity: 0.75; }
      100% { opacity: 0; }
    }

    /* ----- Vehicle hero (2F): compact photo + orange title link -----
       Replaces the decorative gradient hero + thumb chips. Dense, neutral
       card treatment in the autocom family; the title is the sole orange
       accent. */
    .dealer-vehicle-hero {
      display: flex;
      flex-direction: column;
      gap: 0.6rem;
    }
    .dealer-vehicle-hero__photo {
      position: relative;
      width: 100%;
      aspect-ratio: 16 / 9;
      border-radius: 10px;
      overflow: hidden;
      background: #f0f0f0;
      border: 1px solid #e6e6e6;
    }
    .dealer-vehicle-hero__photo img {
      width: 100%; height: 100%;
      object-fit: cover;
      display: block;
    }
    .dealer-vehicle-hero__placeholder {
      position: absolute; inset: 0;
      display: flex; align-items: center; justify-content: center;
      color: #c2c2c2;
      font-size: 2.6rem;
    }
    .dealer-vehicle-hero__title {
      font-size: 1.05rem;
      font-weight: 600;
      line-height: 1.25;
      color: var(--theme-primary);
      text-decoration: none;
      word-break: break-word;
    }
    .dealer-vehicle-hero__title:hover,
    .dealer-vehicle-hero__title:focus {
      color: var(--theme-primary);
      text-decoration: underline;
    }

    /* ----- Vehicle description (2F / 3.10) ----- */
    .dealer-vehicle-desc {
      border-top: 1px solid #eee;
      padding-top: 0.6rem;
    }
    .dealer-vehicle-desc #carDescription {
      color: var(--theme-text);
      line-height: 1.45;
      white-space: pre-line;
    }

    /* ----- Vehicle panel: NF_2027 showcar look & feel (3.10) -----
       A tidy section heading + a clean Specifications label/value grid with
       thin row separators (~3 columns on desktop). Uses OUR existing data
       via the preserved ids (#carYear/#carMileage/#carTransmission/#carFuel/
       #carColor/#carLocation/#carRegistration); no empty Equipment/Condition/
       Service sections. The section heading is shared by Specifications and
       Description so the two read as one system. */
    .dealer-section-head {
      font-size: 0.72rem;
      text-transform: uppercase;
      letter-spacing: 0.06em;
      color: var(--theme-text-muted);
      font-weight: 600;
      margin: 0.25rem 0 0.4rem;
    }
    .dealer-specs {
      display: grid;
      grid-template-columns: repeat(2, minmax(0, 1fr));
      column-gap: 1.25rem;
    }
    @media (min-width: 576px) {
      .dealer-specs { grid-template-columns: repeat(3, minmax(0, 1fr)); }
    }
    .dealer-spec {
      display: flex;
      flex-direction: column;
      gap: 0.1rem;
      padding: 0.45rem 0;
      border-bottom: 1px solid #ededed;
      min-width: 0;
    }
    /* Prototype shows the field label as small muted text (normal case) above
       a slightly larger value — see showcar.html Specifications grid. */
    .dealer-spec__label {
      font-size: 0.75rem;
      color: var(--theme-text-muted);
      font-weight: 400;
    }
    .dealer-spec__value {
      font-size: 0.95rem;
      color: var(--theme-text);
      font-weight: 400;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }

    /* =====================================================================
       AVADS Vehicle panel — NF_2027 showcar look & feel
       Gallery filmstrip + gradient arrows + lightbox, dark collapsible
       accordion (native <details>), highlights row, identity header, and
       the equipment / damages / service list treatments.
       ===================================================================== */

    /* ----- Identity header (replaces the static "Vehicle Details" label) - */
    .dealer-veh-head__lot {
      display: block;
      font-size: 0.68rem;
      text-transform: uppercase;
      letter-spacing: 0.05em;
      color: var(--theme-text-muted);
      font-weight: 600;
      margin-bottom: 0.05rem;
    }
    .dealer-veh-head__title {
      color: var(--theme-primary);
      text-decoration: none;
      font-weight: 600;
    }
    .dealer-veh-head__title:hover,
    .dealer-veh-head__title:focus {
      color: var(--theme-primary);
      text-decoration: underline;
    }
    .dealer-veh-head__sub {
      font-size: 0.8rem;
      color: var(--theme-text-muted);
      margin-top: 0.15rem;
      line-height: 1.3;
    }

    /* ----- Gallery: centre-mode variable-width filmstrip ----------------- */
    .dealer-gallery {
      position: relative;
      margin-bottom: 0.85rem;
      /* White backdrop so the 8px gaps between photos read as white lines, not
         black — matches the NF_2027 prototype gallery. */
      background: #fff;
      border-radius: 8px;
      overflow: hidden;
    }
    .dealer-gallery.dealer-gallery--empty { background: #f0f0f0; }
    /* No photos → collapse the (empty) scroll viewport so only the neutral
       placeholder tile defines the gallery height (avoids a double-height box). */
    .dealer-gallery--empty .dealer-gallery__viewport { display: none; }
    .dealer-gallery__viewport {
      overflow-x: auto;
      overflow-y: hidden;
      scroll-snap-type: x mandatory;
      -webkit-overflow-scrolling: touch;
      scrollbar-width: none;
    }
    .dealer-gallery__viewport::-webkit-scrollbar { display: none; }
    .dealer-gallery__track {
      display: flex;
      align-items: center;
      justify-content: flex-start;
      gap: 8px;
      height: 150px;
    }
    .dealer-gallery__slide {
      flex: 0 0 auto;
      height: 150px;
      padding: 0;
      margin: 0;
      border: 0;
      background: transparent;
      cursor: pointer;
      scroll-snap-align: center;
      display: block;
    }
    .dealer-gallery__slide img {
      height: 100%;
      width: auto;
      display: block;
      object-fit: cover;
    }
    /* Single photo fills the frame (cover) like the prototype hero. */
    .dealer-gallery--single .dealer-gallery__track { justify-content: center; }
    .dealer-gallery--single .dealer-gallery__slide { width: 100%; }
    .dealer-gallery--single .dealer-gallery__slide img { width: 100%; object-fit: cover; }
    .dealer-gallery__empty {
      display: none;
      align-items: center;
      justify-content: center;
      height: 150px;
      color: #b9b9b9;
      font-size: 2.6rem;
    }
    @media (min-width: 576px) {
      .dealer-gallery__track,
      .dealer-gallery__slide,
      .dealer-gallery__empty { height: 210px; }
    }
    @media (min-width: 992px) {
      .dealer-gallery__track,
      .dealer-gallery__slide,
      .dealer-gallery__empty { height: 240px; }
    }
    /* Full-height gradient side arrows with a white chevron (charcoal fade). */
    .dealer-gallery__arrow {
      position: absolute;
      top: 0;
      bottom: 0;
      width: 18%;
      max-width: 72px;
      border: 0;
      z-index: 3;
      cursor: pointer;
      display: flex;
      align-items: center;
      padding: 0 6px;
    }
    .dealer-gallery__arrow--prev {
      left: 0;
      justify-content: flex-start;
      background: linear-gradient(to left, transparent, rgba(17, 17, 17, 0.5) 55%, rgba(17, 17, 17, 0.85));
    }
    .dealer-gallery__arrow--next {
      right: 0;
      justify-content: flex-end;
      background: linear-gradient(to right, transparent, rgba(17, 17, 17, 0.5) 55%, rgba(17, 17, 17, 0.85));
    }
    .dealer-gallery__arrow svg {
      width: 15px;
      height: 28px;
      filter: drop-shadow(0 0 6px rgba(0, 0, 0, 0.85));
    }
    /* Touch devices swipe/scroll — hide the arrows on the narrowest widths. */
    @media (max-width: 575.98px) {
      .dealer-gallery__arrow { display: none !important; }
    }
    /* ----- Highlights row (reg date / mileage / engine / gearbox) -------- */
    .dealer-highlights {
      display: flex;
      gap: 0.4rem;
      justify-content: space-around;
      padding: 0.55rem 0 0.7rem;
      border-bottom: 1px solid #ededed;
      margin-bottom: 0.35rem;
    }
    .dealer-highlight {
      flex: 1 1 0;
      min-width: 0;
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 0.25rem;
      text-align: center;
    }
    .dealer-highlight i { font-size: 1.15rem; color: var(--theme-text-muted); }
    .dealer-highlight__val {
      font-size: 0.8rem;
      font-weight: 300;
      color: var(--theme-text);
      max-width: 100%;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }

    /* ----- Collapsible sections (native <details>, dark header + chevron) - */
    .dealer-accordion {
      margin-top: 0.35rem;
      border: 1px solid #e4e4e4;
      border-radius: 6px;
      overflow: hidden;
    }
    .dealer-acc__item { border-top: 1px solid #e4e4e4; }
    .dealer-acc__item:first-child { border-top: 0; }
    .dealer-acc__head {
      list-style: none;
      cursor: pointer;
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 0.5rem;
      background: #444;
      color: #fff;
      padding: 10px 12px;
      font-size: 0.78rem;
      font-weight: 400;
      letter-spacing: 0.02em;
      user-select: none;
    }
    .dealer-acc__head::-webkit-details-marker { display: none; }
    .dealer-acc__head::marker { content: ""; }
    .dealer-acc__head:hover { background: #111; }
    .dealer-acc__chevron {
      flex: 0 0 auto;
      font-size: 0.8rem;
      transition: transform 0.2s ease-in-out;
    }
    .dealer-acc__item[open] > .dealer-acc__head .dealer-acc__chevron { transform: rotate(180deg); }
    .dealer-acc__body { padding: 0.75rem 0.85rem; background: #fff; }
    /* Prototype shades the Condition + Tyres section bodies light grey. */
    .dealer-acc__body--shade { background: #f1f1f1; }
    .dealer-acc__body .dealer-specs { column-gap: 1.25rem; }
    .dealer-acc__body .dealer-section-head:first-child { margin-top: 0; }

    /* ----- Equipment / damages / service / notes ------------------------- */
    .dealer-eq {
      list-style: none;
      margin: 0;
      padding: 0;
      display: grid;
      grid-template-columns: 1fr;
      gap: 0.3rem 1rem;
    }
    @media (min-width: 576px) { .dealer-eq { grid-template-columns: 1fr 1fr; } }
    /* Prototype equipment = multi-column list of plain bordered lines. */
    .dealer-eq__item {
      font-size: 0.85rem;
      color: var(--theme-text);
      min-width: 0;
      padding: 0.4rem 0;
      border-bottom: 1px solid #ededed;
    }
    .dealer-eq__count { color: var(--theme-text-muted); font-weight: 600; }
    .dealer-dmg,
    .dealer-svc { list-style: none; margin: 0.25rem 0 0; padding: 0; }
    .dealer-dmg__item {
      padding: 0.4rem 0;
      border-bottom: 1px solid #ededed;
      display: flex;
      flex-direction: column;
      gap: 0.1rem;
    }
    .dealer-dmg__loc { font-size: 0.82rem; font-weight: 600; color: var(--theme-text); }
    .dealer-dmg__desc { font-size: 0.8rem; color: var(--theme-text-muted); }
    .dealer-svc li { padding: 0.3rem 0; border-bottom: 1px solid #ededed; font-size: 0.82rem; color: var(--theme-text); }
    .dealer-svc li:last-child,
    .dealer-dmg__item:last-child { border-bottom: 0; }
    .dealer-veh-note {
      font-size: 0.85rem;
      color: var(--theme-text);
      line-height: 1.45;
      margin: 0.4rem 0 0;
      white-space: pre-line;
    }

    /* ----- Fullscreen photo lightbox ------------------------------------- */
    .dealer-lightbox {
      position: fixed;
      inset: 0;
      /* Above the vehicle-detail modal backdrop (.dealer-modal-backdrop,
         z-index 1090) so the gallery lightbox opens IN FRONT of the modal on
         planned-lots (where the gallery lives inside that modal) — at 1080 it
         opened behind it and stayed invisible. In-auction the gallery is
         inline, so this only raises it further above page content. */
      z-index: 1100;
      background: rgba(0, 0, 0, 0.9);
      display: none;
      align-items: center;
      justify-content: center;
    }
    .dealer-lightbox.is-open { display: flex; }
    .dealer-lightbox__img { max-width: 92vw; max-height: 82vh; object-fit: contain; border-radius: 4px; }
    .dealer-lightbox__close {
      position: absolute;
      top: 12px;
      right: 18px;
      background: transparent;
      border: 0;
      color: #fff;
      font-size: 2.4rem;
      line-height: 1;
      cursor: pointer;
      z-index: 2;
    }
    .dealer-lightbox__nav {
      position: absolute;
      top: 50%;
      transform: translateY(-50%);
      background: rgba(0, 0, 0, 0.4);
      border: 0;
      color: #fff;
      font-size: 2.4rem;
      line-height: 1;
      width: 52px;
      height: 72px;
      cursor: pointer;
    }
    .dealer-lightbox__nav--prev { left: 10px; }
    .dealer-lightbox__nav--next { right: 10px; }
    .dealer-lightbox__caption {
      position: absolute;
      bottom: 16px;
      left: 0;
      right: 0;
      text-align: center;
      color: #fff;
      font-size: 0.9rem;
    }
    body.dealer-lightbox-open { overflow: hidden; }

    /* ----- Sample-vehicle preview banner (?avadsDemo=1) ------------------ */
    .dealer-demo-banner {
      background: rgba(var(--theme-primary-rgb), 0.12);
      border: 1px solid rgba(var(--theme-primary-rgb), 0.4);
      color: var(--theme-text);
      font-size: 0.8rem;
      font-weight: 600;
      border-radius: 6px;
      padding: 0.5rem 0.7rem;
      margin-bottom: 0.75rem;
    }

    /* ----- Lots list status badge (3.2) -----
       The Lots table "Status" column shows the lot STATUS (not the start
       price). Neutral pill by default; UPCOMING (the only status the dealer
       Lots panel lists today) gets a subtle orange-accent tint. */
    /* Lot-status badge colours now come from the auctioneer's shared Bootstrap
       subtle-utility palette (LOT_STATUS_BADGE_CLS in dealer.html), so both
       roles read identically; the old bespoke .dealer-lot-status--* tints
       (louder orange/green/red) were retired in favour of that. */

    /* ----- Presented-lots separator + done rows -----
       Finalized (SOLD / WITHDRAWN) lots sink below this labelled divider so
       the top of the list is always the next vehicle up. The rule is a
       full-width band with an uppercase caption; done rows read back a step
       (dimmed) so the eye lands on the upcoming group first. Rendered by
       loadUpcomingCars() in dealer.html. */
    .dealer-lot-sep__cell {
      padding: 0.4rem 0.5rem 0.3rem;
      font-size: 0.66rem;
      font-weight: 600;
      letter-spacing: 0.07em;
      text-transform: uppercase;
      color: var(--theme-text-muted);
      background: rgba(0, 0, 0, 0.04);
      border-top: 2px solid rgba(0, 0, 0, 0.12);
      border-bottom: 1px solid rgba(0, 0, 0, 0.06);
    }
    /* Sub-caption under the "Completed" heading: withdrawn lots can return
       via the Re-Present Withdrawn Lot path (specs/features/02-lot-control.md),
       so set that expectation quietly — normal-case, lighter, on its own line. */
    .dealer-lot-sep__note {
      display: block;
      margin-top: 0.1rem;
      font-size: 0.62rem;
      font-weight: 400;
      letter-spacing: 0;
      text-transform: none;
      opacity: 0.7;
    }
    .dealer-lot-row--done td { opacity: 0.55; }
    /* Lot-number column in the Lots list of BOTH personas (auctioneer.html and
       dealer.html rowFor): a tight leading column — a LIGHT, compact label, not
       a bold chip. Normal weight + muted colour so the vehicle name stays the
       emphasis; width:1% + text-nowrap keep the column as narrow as its digits. */
    .lots-lotno {
      width: 1%;
      font-weight: 300;
      color: var(--theme-neutral-gray);
      /* Tighten the gap to the vehicle name: Bootstrap `.p-2` (which the cell
         also carries) sets a .5rem right padding with !important, so the
         override needs !important too. The unclassed vehicle <td> keeps its
         own .5rem left padding, so the pair reads as a snug leading label. */
      padding-right: 0.15rem !important;
    }
    /* Trim the vehicle cell's leading padding too, so the number sits right
       up against the make/model (targets the unclassed <td> after .lots-lotno
       in BOTH personas' Lots rows). */
    .lots-lotno + td { padding-left: 0.2rem !important; }

    /* Inline alarm-bell control in the dealer Lots panel (right-most column;
       specs/features/14-dealer-lot-alarms.md). A bare icon button — muted by
       default, brand-gold when armed — matching planned-lots.html's .pl-alarm-btn
       so the bell reads the same on both surfaces. The cell hugs its icon. */
    .lots-alarm-cell { width: 1%; text-align: center; white-space: nowrap; color: var(--theme-neutral-gray); }
    .lots-alarm-btn {
      border: 0;
      background: transparent;
      cursor: pointer;
      color: var(--theme-neutral-gray);
      padding: 2px 6px;
      border-radius: 6px;
      line-height: 1;
      transition: color 0.15s ease, background 0.15s ease;
    }
    .lots-alarm-btn:hover { color: #ffc107; background: #fffbe6; }
    .lots-alarm-btn--armed { color: #ffc107; }
    .lots-alarm-btn--armed:hover { color: #e0a800; background: #fff8dc; }
    .lots-alarm-btn:focus-visible { outline: 2px solid var(--theme-primary); outline-offset: 1px; }

    /* ----- Lot-notes row icon (#16) -----
       Sits next to the alarm bell in the actions cell. Always present (notes
       work for any lot status); opens the notes MANAGE dialog for that lot.
       A small count badge overlaps the top-right when the lot has notes. */
    .lots-note-btn {
      position: relative;
      border: 0;
      background: transparent;
      cursor: pointer;
      color: var(--theme-neutral-gray);
      padding: 2px 6px;
      border-radius: 6px;
      line-height: 1;
      transition: color 0.15s ease, background 0.15s ease;
    }
    .lots-note-btn:hover { color: var(--theme-primary); background: #f1f3f4; }
    .lots-note-btn--has { color: var(--theme-primary); }
    .lots-note-btn:focus-visible { outline: 2px solid var(--theme-primary); outline-offset: 1px; }
    .lots-note-badge {
      position: absolute;
      top: -3px;
      right: -1px;
      min-width: 15px;
      height: 15px;
      padding: 0 3px;
      border-radius: 999px;
      background: var(--theme-primary);
      color: #fff;
      font-size: 0.62rem;
      font-weight: 600;
      line-height: 15px;
      text-align: center;
    }

    /* ----- Lot-notes MANAGE dialog (#16) -----
       Chrome comes from .dealer-modal (below); these are the note-specific
       header/subtitle bits. Shared by dealer + auctioneer (both load this
       stylesheet). */
    .lot-notes-modal { max-width: 480px; }
    .lot-notes-modal__scope {
      font-size: 0.78rem;
      color: var(--theme-neutral-gray, #555);
      margin-top: 2px;
    }
    .lot-notes-modal__subtitle {
      font-size: 0.85rem;
      font-weight: 600;
      color: #1a1a1a;
      margin-bottom: 0.6rem;
    }

    /* ----- Lots tabs (All Lots / Won lots) -----
       Drives visibility of #upcomingTableBody (All Lots) vs
       #myWinsTableBody (Won lots) via the .lots-tab--active class.
       (1) autocom style: the active tab is marked by a THIN ORANGE
       UNDERLINE, not a filled orange pill — orange is reserved for the
       bid CTA. Tabs read as text tabs on a hairline rail. */
    .lots-tabs {
      display: flex; gap: 1.1rem;
      border-bottom: 1px solid #e6e6e6;
    }
    .lots-tab {
      border: 0;
      background: transparent;
      border-radius: 0;
      border-bottom: 2px solid transparent;
      margin-bottom: -1px;   /* overlap the .lots-tabs hairline */
      padding: 0.35rem 0.15rem;
      font-size: 0.82rem;
      font-weight: 600;
      color: var(--theme-text-muted);
      cursor: pointer;
      display: inline-flex; align-items: center; gap: 0.4rem;
    }
    .lots-tab:hover { color: var(--theme-text); }
    .lots-tab--active {
      background: transparent;
      border-bottom-color: var(--theme-primary);
      color: var(--theme-text);
    }
    .lots-tab__count {
      background: rgba(0,0,0,0.06);
      color: var(--theme-text-muted);
      border-radius: 999px;
      font-size: 0.7rem;
      padding: 0.05rem 0.45rem;
    }
    .lots-tab--active .lots-tab__count {
      background: rgba(var(--theme-primary-rgb), 0.14);
      color: var(--theme-primary);
    }

    /* ----- Messages thread placeholder ----- */
    .messages-empty {
      text-align: center; color: var(--theme-text-muted);
      padding: 2.5rem 1rem;
    }
    .messages-empty i {
      display: block; font-size: 1.8rem; margin-bottom: 0.5rem; opacity: 0.5;
    }

    /* ----- (4.9) Unread vs read auctioneer replies -----
       Unread replies read as NEW via a call-to-action orange bubble (the same
       --theme-primary the bid CTA uses, with the JS's white text); read replies
       stay neutral gray. The e2e-load-bearing Bootstrap classes are UNCHANGED —
       the JS keeps .bg-primary on unread bubbles and .bg-light on read ones
       (messaging-read-state.spec asserts both). This ONLY re-tints .bg-primary
       inside the dealer thread. On click the JS swaps .bg-primary→.bg-light so
       this rule stops applying and the bubble turns gray. */
    body.dealer-theme #askThread .bg-primary {
      background-color: var(--theme-primary) !important;
    }

    /* ----- (4.10) Softer alternating rows -----
       The default table-striped shade (~5% black) read as noisy gray. Retint
       the striped accent to a near-white so the zebra is barely-there. The
       hover highlight (.table-hover / --bs-table-hover-bg) is deliberately
       left untouched. Covers the bid-history table + both lots tables (any
       striped list inside the dealer grid). */
    body.dealer-theme .dealer-grid .table-striped {
      --bs-table-striped-bg: #fafafa;
    }

    /* ----- (4.11) Non-collapsing panels + in-panel waiting message -----
       Pre-auction (no live lot) several panels used to collapse to a sliver.
       A modest min-height floor keeps them visibly present; the desktop
       stretch/flex rules still grow them past this when there IS content. */
    .dealer-bid-panel,
    .dealer-bidhistory-panel,
    .dealer-grid__vehicle > .card,
    .dealer-grid__messages > .card,
    .dealer-grid__lots > .card {
      min-height: 160px;
    }

    /* Collapsible Messages panel (dealer) — mirrors the auctioneer's. Header
       is a button; collapsed by default (.is-collapsed in the markup). When
       collapsed, hide the body and stop the card growing so it shrinks to just
       the header (the !important beats the card's inline flex + the 160px
       floor above). */
    .msg-panel-header { cursor: pointer; user-select: none; }
    .msg-panel-header:focus-visible { outline: 2px solid var(--theme-primary); outline-offset: -2px; }
    .msg-panel-caret { font-size: 0.8em; opacity: 0.7; }
    #askAuctioneerCard.is-collapsed { flex: 0 0 auto !important; min-height: 0 !important; }
    #askAuctioneerCard.is-collapsed > .card-body { display: none !important; }

    /* Standard collapsible Lots (#62) + Bid History (#63) panels — same idiom
       as the Messages panel above. Card-level `.is-collapsed` hides the
       card-body (display:none, NO DOM removal — expand is instant). Defaults
       expanded; persisted per-persona. Collapsed sizing: min-height:0 + h-100
       override drop the 160px floor / full-height stretch so the Lots card is
       just its header; the Bid History card sits in a stretch row beside the
       bid panel, so align-self:flex-start stops it stretching to the bid unit's
       height and leaving an empty tall box. */
    #lotsCard.is-collapsed > .card-body,
    #bidHistoryCard.is-collapsed > .card-body { display: none !important; }
    #lotsCard.is-collapsed {
      flex: 0 0 auto !important; min-height: 0 !important; height: auto !important;
    }
    #bidHistoryCard.is-collapsed {
      min-height: 0 !important; align-self: flex-start !important;
    }
    /* Unread indicator: the counter badge turns orange when there are unread
       messages — visible in the collapsed header. */
    #askAuctioneerCountBadge.msg-count--unread {
      background-color: #ff5a00 !important;
      color: #fff !important;
      border-color: #ff5a00 !important;
    }
    /* The bid-panel-body waiting message (toggled by renderVehicleGate): a
       centred, muted block that fills the space where the bid form would be,
       so the panel never reads as an empty box before the auction starts. */
    .dealer-bid-waiting {
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      text-align: center;
      gap: 0.25rem;
      min-height: 96px;
      font-size: 0.9rem;
    }
    .dealer-bid-waiting i { font-size: 1.4rem; }

    /* ================================================================
       Dealer layout — CSS Grid (2B). Replaces the old Bootstrap
       .row.dealer-grid and its full-viewport flex machinery
       (body/app/main flex column + per-column max-height:100% +
       .card{height:100%}). New model: the page scrolls naturally,
       panels size to their content, and only the long lists (bid
       history, messages, lots) keep an internal max-height + scroll
       via their existing inline styles (340px / 130px / 280px).

       Grid children are the five presentational wrappers in dealer.html
       (.dealer-grid__{bid,vehicle,video,messages,lots}); every
       load-bearing ID/class inside each card is untouched.

       Mobile-first single column (source order is vehicle, bid, video,
       messages, lots; `order` restacks it to bid → vehicle → messages →
       lots → video). Desktop (>=992px) places them with
       grid-template-areas. Bootstrap `h-100` on the inner cards is inert
       here: each grid item (the wrapper) sizes to content under
       align-items:start, so `height:100%` of a content-sized wrapper is
       a no-op. Only the five containers that exist this pass are placed;
       the plan's separate history/purchase-info areas arrive in a later
       pass (2C/2G). */
    .dealer-grid {
      display: grid;
      grid-template-columns: 1fr;
      gap: 0.75rem;
      align-items: start;   /* panels size to content, never stretch */
      /* (5.2/5.3) Centred, capped cluster (proto style) rather than stretching
         edge-to-edge. max-width + auto inline margins centre the grid inside
         its .container-fluid; on wide screens equal gutters appear on both
         sides. The cap is sized (see --dealer-cluster-max) so the desktop 1fr
         left column lands on ~820px and the right rail stays flush beside the
         bid unit. No-op on mobile (viewport < cap). */
      max-width: var(--dealer-cluster-max);
      margin-inline: auto;
      width: 100%;
    }

    /* (6.3) Two column wrappers, each an INDEPENDENT flex column, so column
       heights never couple. CSS grid rows are global — the old 2x2 areas grid
       coupled row heights across columns, leaving a gap under the shorter bid
       unit and a tall-empty Lots panel under a short left column. Wrapping
       [bid,vehicle] and [video,messages,lots] into their own flex columns
       decouples them: the vehicle sits directly under the bid unit and the
       rail ends at its natural height. Trade-off: the two columns' bottoms no
       longer line up (each ends where its content ends). */
    .dealer-grid__main,
    .dealer-grid__rail {
      display: flex;
      flex-direction: column;
      gap: 0.75rem;
      min-width: 0;
    }
    /* Render order WITHIN each wrapper (independent flex containers, so the
       repeated 1/2 values don't collide). DOM order differs from visual order
       so the single-column mobile stack still reads
       bid → vehicle → messages → lots → video (video last). LEFT wrapper DOM is
       vehicle→bid; RIGHT wrapper DOM is video→messages→lots. */
    .dealer-grid__bid      { order: 1; }
    /* Owner sale-approval card (specs/features/31-lot-owner-sale-approval.md):
       mve's placement — BENEATH the bid panel and ABOVE the lot-description
       panel, which on this column means it slots between the bid unit and the
       lot-notes card. Same mechanism as the notes card below: it IS the flex
       item, so the [hidden] attribute renderLotOwnerPanel() toggles collapses
       it with no column gap for the (overwhelmingly common) viewer who does not
       own the current lot. */
    .dealer-grid__owner    { order: 2; }
    /* Lot-notes card (#53): its OWN panel in the left column, rendered BELOW the
       bid + bid-history unit and ABOVE the vehicle description. It is the grid/
       flex item itself, so its [hidden] attribute (toggled by renderLotNotesPanel)
       collapses it with no column gap when the lot has no note. */
    .dealer-grid__notes    { order: 3; }
    .dealer-grid__vehicle  { order: 4; }
    .dealer-grid__messages { order: 1; }
    .dealer-grid__lots     { order: 2; }
    .dealer-grid__video    { order: 3; }
    /* No-video: drop the orphan video wrapper (syncIvsChrome toggles the
       modifier when the auctioneer's stream goes live/offline). */
    .dealer-grid--no-video .dealer-grid__video { display: none; }

    /* Small screens (the whole single-column regime, i.e. below the 992px
       desktop split): the live-stream player otherwise lands dead-last on the
       stack because it lives in the rail wrapper. Flatten BOTH wrappers with
       display:contents so all five panels become direct grid items of
       .dealer-grid, then order them so the video sits DIRECTLY under the bid
       panel (mve request 2026-07-17). display:contents removes only the wrapper
       box, not selector matching, so the descendant rules above — including
       .dealer-grid--no-video .dealer-grid__video — keep working.
       min-width:0 is REQUIRED on the promoted leaves: the __main/__rail
       wrappers carried it (so their content could shrink below intrinsic
       width), but display:contents dissolves those boxes, so the leaves become
       direct grid items with the default min-width:auto and a wide child (the
       video, the horizontal bid panel, a table) would blow the 1fr column past
       the viewport — the "too wide on mobile" regression. */
    @media (max-width: 991.98px) {
      /* Flatten BOTH column wrappers AND the bid unit's nesting
         (__bid → .dealer-bid-unit → .dealer-bid-split) so the two panels
         inside the bid unit — the "Place your bid" CTA (.dealer-bid-panel)
         and the Bid History card (.dealer-bidhistory-panel) — become
         SEPARATE direct grid items. Without this they stay bundled inside
         .dealer-grid__bid, so ordering "video after bid" put the video after
         the WHOLE bid+history block (video landed below Bid History — the bug
         this fixes). Promoting them lets the video slot BETWEEN the CTA and the
         history. display:contents removes only the wrapper boxes, not selector
         matching, so descendant rules keep working. */
      .dealer-grid__main,
      .dealer-grid__rail,
      .dealer-grid__bid,
      .dealer-bid-unit,
      .dealer-bid-split      { display: contents; }
      /* min-width:0 on the REAL grid items now (the promoted leaves), so a wide
         child can't blow the 1fr column past the viewport. */
      .dealer-bid-panel,
      .dealer-bidhistory-panel,
      .dealer-grid__owner,
      .dealer-grid__notes,
      .dealer-grid__video,
      .dealer-grid__vehicle,
      .dealer-grid__messages,
      .dealer-grid__lots     { min-width: 0; }
      /* Mobile stack order (mve request + #53 + feature 31): CTA → video/audio
         player → Bid History → owner sale-approval → lot-notes → vehicle →
         messages → lots. Both the owner card and the notes card sit directly
         below the bid + bid-history and above the vehicle description,
         mirroring the desktop left-column order. */
      .dealer-bid-panel        { order: 1; }
      .dealer-grid__video      { order: 2; }
      .dealer-bidhistory-panel { order: 3; }
      .dealer-grid__owner      { order: 4; }
      .dealer-grid__notes      { order: 5; }
      .dealer-grid__vehicle    { order: 6; }
      .dealer-grid__messages   { order: 7; }
      .dealer-grid__lots       { order: 8; }
    }

    /* Desktop: two content columns that pack independently from the top.
       LEFT column = bid centre-piece, then the vehicle panel DIRECTLY under
       it (no dead space). RIGHT rail = messages + lots (+ video when live),
       stacked tightly. The two columns are decoupled by pairing each left
       item with a right item ROW-FOR-ROW and letting the right-rail cards
       ABSORB any slack (align-self:stretch + inner card flex:1, see 3.6):
       whichever column is shorter grows its filled cards to match, so there
       is never a visible inter-item gap in either column — both stay
       top-aligned and the same total height. */
    @media (min-width: 992px) {
      /* Two content columns: main (bid → vehicle) on the left, a capped narrow
         rail (messages/lots, + video when the stream is live) on the right.
         Each column is its own flex column (above), so the grid only splits
         them left/right — their heights stay independent (no row coupling).
         The rail is capped to a tidy 300–360px band; the left column (1fr)
         absorbs the extra width. */
      .dealer-grid {
        grid-template-columns: minmax(0, 1fr) minmax(300px, 360px);
      }
      /* Stream live → the video card leads the rail (above messages); on mobile
         it stays last (order:3 above). */
      .dealer-grid:not(.dealer-grid--no-video) .dealer-grid__video { order: -1; }

      /* Cap the bid + vehicle at the same 820px, left-aligned, so their left
         AND right edges line up within the wider left column (matches
         .dealer-bid-unit's 820px cap). align-self:flex-start left-aligns the
         capped panel inside the stretched flex column. */
      .dealer-grid__vehicle {
        max-width: 1180px;
        align-self: flex-start;
        width: 100%;
      }
      /* Owner sale-approval card (feature 31): same cap + left-align as the bid
         unit, notes card and vehicle, so all four panels in the left column
         share the same left AND right edge. */
      .dealer-grid__owner {
        max-width: 1180px;
        align-self: flex-start;
        width: 100%;
      }
      /* Lot-notes card (#53): same cap + left-align as the bid unit and vehicle
         so its left/right edges line up with the panels it sits between (it
         otherwise stretches the full wide 1fr left column and reads misaligned). */
      .dealer-grid__notes {
        max-width: 1180px;
        align-self: flex-start;
        width: 100%;
      }
    }

    /* (6.3) Column heights are now independent (see .dealer-grid__main /
       __rail). The former 2x2 grid-template-areas variants and the
       align-self:stretch right-rail machinery were removed — they existed only
       to fake column decoupling within a shared-row grid and caused the too-tall
       gap under the bid unit + the tall-empty Lots panel. */

    /* IVS overlay: pulsing "live" affordance — draws the dealer's eye to
       the play button when the auctioneer's stream is available. The
       outer red ring expands and fades; the small dot blinks. */
    @keyframes ivsPlayPulse {
      0%   { box-shadow: 0 0 0 0 rgba(220, 53, 69, 0.65); }
      70%  { box-shadow: 0 0 0 22px rgba(220, 53, 69, 0); }
      100% { box-shadow: 0 0 0 0 rgba(220, 53, 69, 0); }
    }
    .ivs-play-pulse { animation: ivsPlayPulse 1.6s infinite; }
    @keyframes ivsLiveDot {
      0%, 100% { opacity: 1; }
      50%      { opacity: 0.35; }
    }
    .ivs-live-dot { animation: ivsLiveDot 1.2s infinite; }

    /* ================================================================
       Mobile (< md / 768 px) — phone-first layout overrides.
       ================================================================
       Desktop layout stays exactly as designed; everything below this
       line only takes effect on a viewport narrower than 768 px.

       Goals:
       - Stack the four cards vertically.
       - Put the bid action card FIRST so the dealer never has to
         scroll past anything to place a bid (`order-1 order-md-0`
         on the markup; this block only governs sizing).
       - Cap each card's internal scrollable area so a long bid feed
         can't push the next card off-screen, but allow the card
         itself to size naturally to its header + scroller stack.
       - Bump tap targets to >= 44 px and primary action type to >= 16 px.
       - Allow the IVS "Auctioneer is live" caption to wrap on narrow
         screens (the original `white-space: nowrap` overflows below
         ~390 px once the play button + dot are accounted for).
       - Put the hammer / withdraw chip strips in a 2x2 grid so each
         chip stays legible at 360 px. */
    @media (max-width: 767.98px) {
      /* The dealer grid is already single-column below 992px (see the
         layout section above), so no flex/stack reset is needed here —
         only the phone-specific tightening below. */
      /* Cap each scrollable inner region so one long list can't
         dominate the page. The bid feed, messages thread, and lots
         tables all keep their internal scrollbars. */
      .dealer-grid #bidTable {
        margin: 0;
      }
      .dealer-grid .card-body > .flex-grow-1 {
        max-height: 240px !important;
        min-height: 120px;
        overflow-y: auto;
      }
      .dealer-grid #askThread {
        max-height: 180px !important;
        min-height: 80px !important;
        overflow-y: auto;
      }
      /* (4.3) The lots panes are NOT capped/scrolled any more — the Lots panel
         grows to fit all rows (no scrollbar) on every breakpoint, including
         mobile. The former max-height:240px + overflow-y:auto here was the
         mobile half of that cap and is intentionally removed. */

      /* Bid action: bigger tap targets, larger primary type. The
         input-group children sit on the same row so we bump heights
         in lock-step. The +/- buttons MUST stay >= 44 px to be
         touch-comfortable; the Bid button uses 16 px+ type so the
         number is readable at arm's length. */
      #dealerBidWrap {
        padding: 1rem !important;
      }
      #dealerBidWrap label[for="dealerBidInput"] {
        font-size: 0.95rem;
      }
      #dealerBidWrap .input-group-sm > .btn,
      #dealerBidWrap .input-group-sm > .form-control {
        min-height: 48px;
        font-size: 1rem;
      }
      #dealerBidWrap #bidMinus,
      #dealerBidWrap #bidPlus {
        min-width: 48px;
        font-size: 1.25rem;
      }
      #dealerBidWrap #dealerBidInput {
        font-size: 1.05rem;
      }
      #dealerBidWrap #bidButton {
        font-size: 1rem;
        padding-left: 0.85rem !important;
        padding-right: 0.85rem !important;
      }
      /* #minBidHint stays inside #dealerBidWrap (inline on the label row). */
      #dealerBidWrap #minBidHint {
        font-size: 0.9rem;
      }

      /* Hammer + withdraw progress: the quiet text-step row (2D) stays
         4-across even on a phone — the short labels fit at 360 px — we just
         tighten the gap, padding and label size a touch. */
      .dealer-progress-strip {
        padding: 0.5rem 0.5rem;
      }
      .hammer-chips .hammer-progress__steps,
      .withdraw-chips .hammer-progress__steps {
        gap: 0.3rem;
      }
      .hammer-chips .hammer-progress__step,
      .withdraw-chips .hammer-progress__step {
        font-size: 0.62rem;
        padding: 0.3rem 0.2rem;
      }

      /* (6.1) Sub-header responsive rules removed with the band. */

      /* IVS card: the 16:9 ratio shrinks the card to ~211 px tall on
         a 375 px phone, so the 72x72 play button still fits. The
         caption was `white-space: nowrap` (set inline) — allow it to
         wrap so the text doesn't bleed past the card edge. */
      #ivsCard #ivsOverlayReady > div[style*="white-space:nowrap"],
      #ivsCard #ivsOverlayReady > div {
        white-space: normal !important;
        text-align: center;
        max-width: 90%;
        line-height: 1.25;
        font-size: 0.78rem;
      }
      /* IVS bottom controls (play/mute) — bump from 26x26 to 36x36
         so they're tappable. The flex container's `width:26px` is
         set inline; we override with !important. */
      #ivsCard #ivsPlayBtn,
      #ivsCard #ivsMuteBtn {
        width: 36px !important;
        height: 36px !important;
        flex: 0 0 36px !important;
      }
      #ivsCard #ivsPlayBtn .fa-xs,
      #ivsCard #ivsMuteBtn .fa-xs {
        font-size: 0.85rem;
      }

      /* Lots tabs — bigger touch targets. */
      .lots-tabs { gap: 0.4rem; flex-wrap: wrap; }
      .lots-tab {
        padding: 0.55rem 0.85rem;
        font-size: 0.9rem;
        min-height: 40px;
      }

      /* Messages compose area: bigger Send button, prevent the
         textarea from being smaller than tap-comfortable. */
      #askAuctioneerCard textarea#askAuctioneerText {
        font-size: 1rem;
        min-height: 56px;
      }
      #askAuctioneerCard #askAuctioneerBtn {
        min-height: 44px;
        padding: 0.5rem 1rem;
        font-size: 0.95rem;
      }

      /* Card title baseline: 16 px for headers so they read clearly. */
      .dealer-grid .card-title.fs-5 {
        font-size: 1rem;
      }

      /* Lots / Bid tables: ensure the row content type is >= 13 px
         (the .fs-sm-custom utility otherwise rides ~12 px). */
      .dealer-grid #bidTable,
      .dealer-grid #lotsAllPane table,
      .dealer-grid #lotsMinePane table {
        font-size: 0.85rem;
      }
      .dealer-grid #bidTable td,
      .dealer-grid #bidTable th,
      .dealer-grid #lotsAllPane td,
      .dealer-grid #lotsAllPane th,
      .dealer-grid #lotsMinePane td,
      .dealer-grid #lotsMinePane th {
        padding: 0.55rem 0.5rem;
      }

      /* Vehicle hero (2F): cap the photo height on phones so the card
         doesn't push other cards off the visible region. */
      .dealer-grid .dealer-vehicle-hero__photo {
        max-height: 200px;
      }

      /* Winner overlay — full-screen feel on phones. The base styles
         live in app/style.css (.winner-overlay / .winner-card) which
         we cannot edit, so override the key dimensions here. */
      .winner-card {
        padding: 1.75rem 1.25rem !important;
        border-radius: 14px !important;
        width: calc(100vw - 1.5rem);
        max-width: 100%;
        box-sizing: border-box;
      }
      .winner-card h2 {
        font-size: 1.6rem;
      }
      .winner-card p {
        font-size: 0.95rem;
      }

      /* Login screen: tap-target on the join button + 16 px input so
         iOS Safari does not zoom the viewport when the field gains
         focus (the 16 px threshold is iOS-specific). */
      .login-screen #dealerNameInput {
        font-size: 1rem;
        min-height: 48px;
      }
      .login-screen .btn-lg {
        min-height: 48px;
        font-size: 1rem;
      }

      /* Toast (private messages from auctioneer) — let it span more
         of the viewport on small screens so the body text isn't
         clipped. The base width comes from style.css. */
      .toast-container .custom-toast {
        max-width: calc(100vw - 1.5rem);
      }

      /* (4.13) When the sticky bid bar is visible, lift the bottom toast stack
         above it so a success/bid toast is never overlapped by the fixed bar.
         The bar is ~52px min-height + ~19px vertical padding + safe-area; 72px
         clears it and +12px keeps the base 12px gap. Only active while
         syncStickyBid() has set body.dealer-sticky-visible, so non-biddable
         states keep the toast at its normal bottom:12px. `.toast-container`
         base bottom (style.css) is a plain rule, so this higher-specificity
         selector wins without !important. Desktop placement is unchanged
         (this whole block is inside the <=767.98px media query). */
      body.dealer-sticky-visible .toast-container {
        bottom: calc(var(--dealer-sticky-height, calc(96px + env(safe-area-inset-bottom, 0px))) + 12px);
      }
    }

    /* Extra-narrow screens (< 360 px) — modest follow-up tightening.
       Most of Apple's smallest phones (iPhone SE 1st gen) report
       320 px. We avoid breaking the chip strip / bid stepper at
       this size by trimming horizontal padding further. */
    @media (max-width: 359.98px) {
      /* The doubled height is kept here too - it is the point of the change -
         but the text and horizontal padding still tighten, because three or
         four chips have to fit across a 320 px iPhone SE without wrapping into
         a taller strip. */
      .hammer-chips .hammer-progress__step,
      .withdraw-chips .hammer-progress__step {
        font-size: 0.62rem;
        padding: 0.3rem 0.15rem;
        min-height: 2.6rem;
      }
      .dealer-bid-cta__amount { font-size: 1.2rem; }
      #dealerBidWrap #bidButton {
        padding-left: 0.55rem !important;
        padding-right: 0.55rem !important;
      }
    }

    /* ===== IVS stream-experience additions (specs/features/16, 17, 19) ===== */

    /* Connection-quality chip — small, secondary, non-blocking (feature 19).
       Lives in the dark #ivsCard footer bar; colour-codes good / weak /
       reconnecting. Never a modal, never blocks the bidding UI. */
    .dealer-quality-chip {
      display: inline-flex;
      align-items: center;
      gap: 0.3rem;
      font-size: 0.62rem;
      font-weight: 600;
      letter-spacing: 0.02em;
      padding: 2px 7px;
      border-radius: 999px;
      line-height: 1.2;
      white-space: nowrap;
      color: #e6e6e6;
      background: rgba(255, 255, 255, 0.08);
      border: 1px solid rgba(255, 255, 255, 0.14);
    }
    .dealer-quality-chip.is-good { color: #4ade80; }
    .dealer-quality-chip.is-good i { color: #22c55e; }
    .dealer-quality-chip.is-weak { color: #fbbf24; }
    .dealer-quality-chip.is-weak i { color: #f59e0b; }
    .dealer-quality-chip.is-reconnecting { color: #fca5a5; }
    .dealer-quality-chip.is-reconnecting i { color: #ef4444; }

    /* RT_AUDIO "🔊 live audio" indicator pulse. Subtle, non-distracting. */
    .ivs-audio-pulse { animation: ivs-audio-pulse 1.8s ease-in-out infinite; }
    @keyframes ivs-audio-pulse {
      0%, 100% { transform: scale(1); opacity: 0.9; }
      50% { transform: scale(1.12); opacity: 1; }
    }
    @media (prefers-reduced-motion: reduce) {
      .ivs-audio-pulse { animation: none; }
    }

    /* Post-auction feedback modal (feature 16). One dismissible, accessible
       dialog. Backdrop + centred card; theme-neutral so it reads on the
       dealer light surface. Scrolls internally on small screens. */
    .dealer-modal-backdrop {
      position: fixed;
      inset: 0;
      z-index: 1090;
      background: rgba(0, 0, 0, 0.5);
      display: flex;
      align-items: center;
      justify-content: center;
      padding: 1rem;
    }
    .dealer-modal-backdrop[hidden] { display: none; }
    .dealer-modal {
      background: var(--theme-white, #fff);
      color: #1a1a1a;
      width: 100%;
      max-width: 480px;
      max-height: calc(100vh - 2rem);
      display: flex;
      flex-direction: column;
      border-radius: 12px;
      box-shadow: 0 12px 40px rgba(0, 0, 0, 0.35);
      overflow: hidden;
    }
    .dealer-modal[hidden] { display: none; }
    .dealer-modal__head {
      display: flex;
      align-items: flex-start;
      justify-content: space-between;
      gap: 0.5rem;
      padding: 1rem 1.1rem 0.6rem;
      border-bottom: 1px solid #eee;
    }
    .dealer-modal__title { font-size: 1.15rem; font-weight: 600; margin: 0; }
    .dealer-modal__close {
      flex: 0 0 auto;
      border: 0;
      background: transparent;
      font-size: 1.1rem;
      line-height: 1;
      color: var(--theme-neutral-gray, #666);
      cursor: pointer;
      padding: 0.2rem 0.35rem;
      border-radius: 6px;
    }
    .dealer-modal__close:hover,
    .dealer-modal__close:focus-visible { background: #f1f3f4; color: #1a1a1a; }
    .dealer-modal__body { padding: 0.9rem 1.1rem; overflow-y: auto; }
    .dealer-modal__intro { font-size: 0.85rem; color: var(--theme-neutral-gray, #555); margin: 0 0 0.75rem; }
    .dealer-modal__foot {
      display: flex;
      justify-content: flex-end;
      gap: 0.5rem;
      padding: 0.75rem 1.1rem;
      border-top: 1px solid #eee;
    }
    .feedback-q { border: 0; padding: 0; margin: 0 0 1rem; display: block; }
    .feedback-q legend {
      font-size: 0.85rem;
      font-weight: 600;
      margin-bottom: 0.4rem;
      padding: 0;
      color: #1a1a1a;
    }
    .feedback-req { color: var(--theme-danger, #dc3545); }
    .feedback-opt {
      display: flex;
      align-items: flex-start;
      gap: 0.5rem;
      font-size: 0.85rem;
      padding: 0.3rem 0.4rem;
      border-radius: 6px;
      cursor: pointer;
    }
    .feedback-opt:hover { background: rgba(var(--theme-primary-rgb, 255, 90, 0), 0.06); }
    .feedback-opt input { margin-top: 0.15rem; flex: 0 0 auto; }
    .feedback-comment { display: block; font-size: 0.85rem; font-weight: 600; }
    .feedback-comment textarea { margin-top: 0.35rem; font-weight: 400; }
    .feedback-error {
      color: var(--theme-danger, #dc3545);
      font-size: 0.82rem;
      font-weight: 600;
      margin-top: 0.25rem;
    }
    .feedback-error[hidden] { display: none; }

    /* ===== (7) Light-weight typography pass — NF_2027 CI =====
       The redesign reads LIGHT. Panel/section titles rest at 300; values,
       labels, tabs, table headers and listed prices drop from 600/700 to the
       body's resting 400. Weight ONLY — no size, spacing, colour or layout
       changes, and semantic state signals are deliberately kept: success/
       danger text colour, the active-tab orange underline, and the hammer/
       withdraw/reserve status chips are untouched. Applies to BOTH screens —
       the auctioneer page also renders under body.dealer-theme. The orange
       Place-bid CTA amount (.dealer-bid-cta__amount) stays bold by design: it
       is the primary action, not a listed price. Bootstrap fw-bold/fw-semibold
       utilities are !important, so those are stripped in the HTML rather than
       fought here. */
    body.dealer-theme .card-title,
    body.dealer-theme .dealer-vehicle-hero__title,
    body.dealer-theme .dealer-modal__title { font-weight: 300; }

    body.dealer-theme thead th,
    body.dealer-theme .lots-tab,
    body.dealer-theme .dealer-bid2__veh-title,
    body.dealer-theme .dealer-sticky-bid__veh-title,
    body.dealer-theme .dealer-bid2__status-amt,
    body.dealer-theme .dealer-sticky-bid__status-amt,
    body.dealer-theme .dealer-bidrow__amount { font-weight: 400; }

    /* Neutral count pills ("0 bids", "0 new", "0 lots", "0 dealers",
       "0 messages") — Bootstrap .badge defaults to 700; drop them to the
       resting 400. Scoped by .border so the monospace keyboard-shortcut pills
       (Alt+1 …, no .border) and every coloured semantic/status badge (not
       .bg-light) are untouched. */
    body.dealer-theme .badge.bg-light.text-dark.border { font-weight: 400; }

/* ============================================================
   (8) Responsive header (mobile) — NF_2027
   The mobile hamburger + collapse menu were removed. The profile
   menu, language selector and connection status now sit directly in
   the top bar at every width; the SAJ Design / CI Prototype / Log out
   links fold into the profile dropdown (the "expandable menu").
   Below the lg breakpoint the connection status collapses to just its
   coloured dot: the pill chrome (padding / background / border) and
   the "Connecting…" text drop away so the action row stays compact.
   #statusText is kept in the DOM (visually hidden) for screen readers.
   The auctioneer page now shares this pattern (its hamburger + collapse
   were removed so its command buttons, connection dot, sound, language
   and profile menu sit in the top bar at every width, same as the
   dealer); planned-lots has no live connection status and keeps its
   actions hidden on small screens, so it is unaffected.
   ============================================================ */
@media (max-width: 991.98px) {
  body.dealer-theme .app-header .header-actions { gap: 0.5rem; }
  body.dealer-theme .app-header .connection-status {
    padding: 0; background: none; border: none;
  }
  body.dealer-theme .app-header .connection-status #statusText {
    position: absolute; width: 1px; height: 1px;
    padding: 0; margin: -1px; overflow: hidden;
    clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
  }
}

/* Sticky lot-alarm toast close button (#74). The toast is the positioning
   context; the "×" sits top-right and the toast reserves right-padding so the
   body text never runs under it. Replaces the old float:right + negative-margin
   btn-close that overlapped the text and mis-placed its click target. */
.toast-container .custom-toast--sticky { position: relative; padding-right: 2.4rem; }
.toast-container .custom-toast__close {
  position: absolute; top: 0.35rem; right: 0.4rem; z-index: 1;
  width: 1.75rem; height: 1.75rem;
  display: flex; align-items: center; justify-content: center;
  border: 0; border-radius: 4px; background: transparent;
  color: #fff; font-size: 1.4rem; line-height: 1; cursor: pointer;
  opacity: 0.9; padding: 0;
}
.toast-container .custom-toast__close:hover,
.toast-container .custom-toast__close:focus-visible { opacity: 1; background: rgba(255, 255, 255, 0.18); }

/* ----- Lots panel width containment -----
   The dealer Lots table is laid out for English. Danish and German statuses are
   much longer ("Kommende", "Tilbagetrukket" vs "Upcoming", "Withdrawn"), and a
   non-wrapping status pill forced the whole table wider than its card - the
   rows visibly ran past the rounded border (mve's screenshot).

   Two layers, deliberately:
     1. the status pill WRAPS, so a long word stacks inside the pill instead of
        widening the column. This is the fix - the table still fits.
     2. the pane scrolls as a LAST RESORT, so no future translation can make the
        table escape its card again. A horizontal scrollbar inside the panel is
        a poor experience; spilling outside it is a broken one. */
#lotsAllPane,
#lotsMinePane {
  overflow-x: auto;
}
#lotsAllPane .badge,
#lotsMinePane .badge {
  white-space: normal;
}
/* Realised-price line under the vehicle details in the Lots panel.
   Deliberately a WRAPPING flex row rather than the nowrap cell it replaced:
   it sits in the flexible Vehicle column, so on a narrow viewport it wraps
   instead of pushing the table wider than its panel (the #310 overflow). */
.lots-result-line {
  /* Stacked rows, not a wrapping flex line. See the comment in resultLine():
     a flex separator ends up alone on its own line the moment the text is long
     enough to wrap, which is what happened in Danish. */
  margin-top: 0.15rem;
  font-size: 0.75rem;
  line-height: 1.3;
}
.lots-result-line__row {
  display: block;
}
/* A finalised row is dimmed (`.dealer-lot-row--done td { opacity: .55 }`), and
   opacity on the cell CANNOT be undone by a child - it creates its own
   stacking context. So the realised price gets a deliberately DARK colour
   instead of the muted grey it started with: at 0.55 opacity a muted grey was
   barely readable, which defeats the point of the feature (dealers bid in
   order to learn the price - specs/features/03-bidding.md → "bid to learn the
   price"). Darker text, then dimmed, lands at roughly normal muted contrast. */
.lots-result-line {
  color: #212529;
}
