/* ─────────────────────────────────────────────────────────────────────────────
 * developer.css — the ONLY CSS file we edit in apps/web.
 * Designer files (fonts.css, color.css, style.css, responsive.css) stay verbatim.
 * Add new / override styles here. Loaded after style.css, before responsive.css.
 * ───────────────────────────────────────────────────────────────────────────── */

/* Sticky header + dashboard sidebar (body scrolls; sidebar has no own scrollbar). */
.site-header {
  position: sticky;
  top: 0;
  z-index: 50;
}

html {
  scroll-padding-top: var(--site-header-height, 84px);
}

/* ── Dashboard: ONE scroller — the PAGE ──────────────────────────────────────
   Deliberately NOT two internal panes any more. A fixed-height row with its own
   overflow put a scrollbar on the content pane AND left the window scrollbar
   for the footer, so the screen showed two bars side by side and the wheel did
   different things depending on which one the cursor happened to be over.

   Now the row simply grows with its content and the window is the only thing
   that scrolls. The sidebar keeps its full-height white column (it is stretched
   by the flex row) while its MENU is sticky inside it: short menus pin under the
   header, a menu taller than the viewport rides the page and pins at its own
   end — every item stays reachable without a second scrollbar. */
.dashboard-row {
  /* Fills the viewport on short pages; grows past it on long ones. */
  min-height: calc(100dvh - var(--site-header-height, 84px));
}

.dashboard-content {
  /* No height / overflow here on purpose — see above. */
  min-height: calc(100dvh - var(--site-header-height, 84px));
}

.site-sidebar {
  /* The column itself is a plain stretched flex child: no sticky, no scrollbar. */
  position: static;
  top: auto;
  height: auto;
  max-height: none;
  overflow: visible;
}

@media (min-width: 1024px) {
  .site-sidebar-inner {
    position: sticky;
    /* Stops right below the sticky header rather than sliding under it. */
    top: var(--site-header-height, 84px);
  }
}

@media (max-width: 1023px) {

  /* Below the toggle breakpoint the sidebar is a FIXED drawer (position and
     transform come from /css/style.css) — a fixed overlay cannot rely on page
     scroll, so this one keeps its own internal scrollbar. */
  .site-sidebar {
    position: fixed;
    height: auto;
    max-height: 100dvh;
    overflow-y: auto;
    overscroll-behavior: contain;
  }
}

/* Place Bid / Confirm Bid / Stripe: panel body scrolls, footer stays in view. */
.site-modal {
  overflow-y: auto;
  overscroll-behavior: contain;
}

.site-modal:has(.is-bid-scroll) {
  overflow: hidden;
}

.site-modal-panel.is-bid-scroll {
  overflow: hidden;
  min-height: 0;
}

.site-modal-panel.is-bid-scroll>.site-modal-scroll-slot {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
  overflow: hidden;
}

.site-modal-panel.is-bid-scroll .site-modal-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-x: hidden;
  overflow-y: auto;
  overscroll-behavior: contain;
  -ms-overflow-style: none;
  scrollbar-width: none;
}

.site-modal-panel.is-bid-scroll .site-modal-body::-webkit-scrollbar {
  display: none;
  width: 0;
  height: 0;
}

/* Global loading overlay (BrandLoader). */
.brand-loader-overlay {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(10, 10, 10, 0.94);
  backdrop-filter: blur(6px);
  opacity: 0;
  transition: opacity 220ms ease;
}

.brand-loader-overlay.is-visible {
  opacity: 1;
}

.brand-loader__mark {
  animation: brand-loader-breathe 2s ease-in-out infinite;
  will-change: transform, opacity;
}

@keyframes brand-loader-breathe {

  0%,
  100% {
    transform: scale(0.97);
    opacity: 0.85;
  }

  50% {
    transform: scale(1);
    opacity: 1;
  }
}

.brand-loader__track {
  position: relative;
  width: 180px;
  height: 3px;
  overflow: hidden;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.14);
}

.brand-loader__bar {
  position: absolute;
  inset-block: 0;
  width: 45%;
  border-radius: inherit;
  background: linear-gradient(90deg, transparent, #D70501 18%, #ff3b3b 50%, #D70501 82%, transparent);
  animation: brand-loader-sweep 1.15s cubic-bezier(0.65, 0, 0.35, 1) infinite;
}

@keyframes brand-loader-sweep {
  0% {
    transform: translateX(-110%);
  }

  100% {
    transform: translateX(322%);
  }
}

@media (prefers-reduced-motion: reduce) {
  .brand-loader-overlay {
    transition: none;
  }

  .brand-loader__mark {
    animation: none;
    opacity: 1;
  }

  .brand-loader__bar {
    animation: none;
    width: 100%;
    transform: none;
  }
}

/* CMS content wrapper — bare HTML injected via dangerouslySetInnerHTML (Terms &
   Conditions, About Us blocks) has no Tailwind classes of its own, since it comes
   straight from the backend as a raw HTML string. These descendant selectors give
   bare <h2>/<p>/<ul>/<li> tags the same typography the hardcoded Terms/About copy
   used before it moved to the CMS (§9 CMS pages). */
.cms-content {
  display: flex;
  flex-direction: column;
  gap: clamp(26px, 2vw, 36px);
}

.cms-content>*:first-child {
  margin-top: 0;
}

.cms-content h2 {
  font-family: var(--font-impact-regular);
  font-size: 18px;
  line-height: normal;
  color: #1a1a17;
  text-transform: none;
  margin: 0 0 12px;
}

.cms-content p {
  font-family: var(--font-segoe-ui-semibold);
  font-size: 16px;
  line-height: normal;
  color: #68645d;
  margin: 0 0 12px;
}

.cms-content ul,
.cms-content ol {
  font-family: var(--font-segoe-ui-semibold);
  font-size: 16px;
  line-height: normal;
  color: #68645d;
  margin: 0 0 12px;
  padding-left: 22px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.cms-content li {
  list-style: disc;
}

.cms-content a {
  color: #ff1313;
  text-decoration: underline;
}

/* FAQ accordion (driven by js/custom.js port) */
.faq-panel {
  height: 0;
  overflow: hidden;
  transition: height 0.35s cubic-bezier(0.22, 1, 0.36, 1);
}

/* Storefront device preview (apps/hdd-auction/preview.php).
   Frame is sized to the full storefront height and scaled to width so the
   dashboard page scrolls — the iframe itself must not show a scrollbar. */
.preview-device-scaler {
  position: relative;
  width: 100%;
  min-height: 480px;
  overflow: hidden;
}

.preview-device-frame {
  display: block;
  border: 0;
  background: #fff;
  transform-origin: top left;
  pointer-events: auto;
  overflow: hidden;
  scrollbar-width: none;
}

.preview-device-frame::-webkit-scrollbar {
  display: none;
}

.header-search-popup {
  scrollbar-width: thin;
  scrollbar-color: #ded8cc transparent;
}

.header-search-popup mark {
  background: rgba(255, 19, 19, 0.15);
  color: #c40f0f;
}

html:has(.storefront-preview-embed),
body:has(.storefront-preview-embed) {
  overflow: hidden !important;
  height: auto !important;
}

.storefront-preview-embed {
  min-height: 0;
  overflow: hidden;
}

/* Auction landing — Figma compact scale (Featured Event / Live Activity).
   The PHP reference used Tailwind clamps up to 56px / 487px card height; the
   delivered Figma is smaller. These classes are the source of truth. */
.auction-landing-title {
  font-family: var(--font-impact-regular);
  font-weight: 400;
  text-transform: uppercase;
  font-size: clamp(22px, 2.2vw, 32px);
  line-height: 1.07;
  color: #171717;
  margin: 0;
}

.auction-landing-title-invert {
  color: #fff;
}

.auction-landing-hero-title {
  font-family: var(--font-impact-regular);
  font-weight: 400;
  text-transform: uppercase;
  font-size: clamp(28px, 3.6vw, 52px);
  line-height: 1.08;
  color: #fff;
  margin: 0;
}

.auction-landing-section {
  padding-top: clamp(32px, 4vw, 64px);
  padding-bottom: clamp(32px, 4vw, 64px);
}

.auction-featured-block,
.auction-activity-block {
  display: flex;
  flex-direction: column;
  gap: 16px;
  min-width: 0;
}

@media (min-width: 1280px) {

  .auction-featured-block,
  .auction-activity-block {
    gap: 20px;
  }
}

.auction-featured-card {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  min-height: 280px;
  flex-grow: 1;
  border-radius: 8px;
  overflow: hidden;
  padding: 16px 18px;
}

@media (min-width: 768px) {
  .auction-featured-card {
    min-height: 320px;
    padding: 20px;
  }
}

@media (min-width: 1280px) {
  .auction-featured-card {
    min-height: 340px;
    flex-grow: 1;
  }
}

.auction-featured-card__media,
.auction-featured-card__shade {
  position: absolute;
  inset: 0;
}

.auction-featured-card__media {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.auction-featured-card__shade {
  background: linear-gradient(246.75deg, rgba(0, 0, 0, 0) 5.16%, #000 53.18%);
}

.auction-featured-badge {
  position: relative;
  z-index: 1;
  align-self: flex-start;
  display: inline-flex;
  align-items: center;
  background: #ff1313;
  color: #fff;
  font-family: var(--font-segoe-ui-semibold);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.04em;
  line-height: 1;
  padding: 5px 8px;
  border-radius: 2px;
}

.auction-featured-body {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.auction-featured-title {
  font-family: var(--font-impact-regular);
  font-weight: 400;
  text-transform: uppercase;
  font-size: clamp(20px, 1.8vw, 26px);
  line-height: 1.15;
  color: #fff;
  margin: 0;
  max-width: 460px;
}

.auction-featured-meta {
  margin: 6px 0 0;
  font-family: var(--font-segoe-ui-semibold);
  font-size: 13px;
  font-weight: 600;
  line-height: 1.35;
  color: #fff;
}

.auction-featured-footer {
  display: flex;
  flex-direction: column;
  gap: 14px;
}

@media (min-width: 640px) {
  .auction-featured-footer {
    flex-direction: row;
    align-items: flex-end;
    justify-content: space-between;
  }
}

.auction-featured-ends {
  font-family: var(--font-segoe-ui-semibold);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.04em;
  color: #fff;
}

.auction-countdown {
  display: flex;
  align-items: stretch;
  margin-top: 6px;
}

.auction-countdown__unit {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 0 12px;
  border-left: 1px solid rgba(255, 255, 255, 0.3);
}

.auction-countdown__unit:first-child {
  padding-left: 0;
  border-left: 0;
}

.auction-countdown__unit:last-child {
  padding-right: 0;
}

.auction-countdown__num {
  font-family: var(--font-impact-regular);
  font-size: 24px;
  line-height: 1;
  color: #fff;
}

.auction-countdown__label {
  font-family: var(--font-segoe-ui-semibold);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.04em;
  color: rgba(255, 255, 255, 0.8);
}

.auction-featured-card .btn,
.auction-activity-card .btn {
  min-height: 40px;
  height: 40px;
  padding: 8px 18px;
  font-size: 14px;
  border-radius: 6px;
}

.auction-activity-block {
  width: 100%;
}

@media (min-width: 1024px) {
  .auction-activity-block {
    width: 380px;
    flex-shrink: 0;
  }
}

.auction-activity-card {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: 20px;
  flex-grow: 1;
  background: #fff;
  padding: 12px;
  border-radius: 8px;
  border: 1px solid #e8e8e8;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.04);
}

.auction-activity-row {
  display: flex;
  align-items: center;
  gap: 12px;
  text-decoration: none;
}

.auction-activity-row+.auction-activity-row {
  margin-top: 10px;
}

.auction-activity-thumb {
  width: 72px;
  height: 54px;
  border-radius: 6px;
  object-fit: cover;
  flex-shrink: 0;
}

.auction-activity-name {
  font-family: var(--font-impact-regular);
  font-size: 13px;
  line-height: 1.2;
  color: #000;
}

.auction-activity-bids {
  font-family: var(--font-segoe-ui-regular);
  font-size: 12px;
  color: #68645d;
}

.auction-activity-price {
  font-family: var(--font-impact-regular);
  font-size: 15px;
  line-height: 1.2;
  color: #000;
}

.auction-activity-live {
  font-family: var(--font-segoe-ui-semibold);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.04em;
  color: #ff1313;
}

.auction-ready-title {
  font-family: var(--font-impact-regular);
  font-weight: 400;
  text-transform: uppercase;
  font-size: clamp(18px, 1.6vw, 24px);
  line-height: 1.1;
  color: #1a1c1c;
}

.auction-ready-copy {
  font-family: var(--font-segoe-ui-regular);
  font-size: 14px;
  line-height: 20px;
  color: #68645d;
}

.auction-activity-cta {
  width: 100%;
}

/* Trending Lots — dark carousel (Figma) */
.trending-lots-section {
  background: #000;
}

.trending-lots-section .auction-landing-title {
  color: #fff;
}

.trending-lots-carousel {
  position: relative;
  padding: 0 44px;
}

.trending-lots-swiper {
  overflow: hidden;
}

.trending-lots-swiper .swiper-slide {
  height: auto;
}

.trending-lots-nav {
  position: absolute;
  top: 50%;
  z-index: 3;
  transform: translateY(-50%);
  width: 36px;
  height: 36px;
  border: none;
  border-radius: 50%;
  background: #2c2c2c;
  color: #cfcfcf;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}

.trending-lots-nav:hover:not(:disabled) {
  background: #3a3a3a;
  color: #fff;
}

.trending-lots-nav.swiper-button-disabled,
.trending-lots-nav:disabled {
  opacity: 0.35;
  cursor: default;
}

.trending-lots-prev {
  left: 0;
}

.trending-lots-next {
  right: 0;
}

@media (min-width: 640px) {
  .trending-lots-carousel {
    padding: 0 48px;
  }

  .trending-lots-prev {
    left: 0;
  }

  .trending-lots-next {
    right: 0;
  }
}

.auction-card-dark {
  display: flex;
  flex-direction: column;
  height: 100%;
  background: #111111;
  border-radius: 10px;
  overflow: hidden;
  border: 1px solid #1f1f1f;
  transition: border-color 200ms ease, box-shadow 200ms ease;
}

.auction-card-dark.is-pulse {
  border-color: #4ade80;
  box-shadow: 0 0 0 3px rgba(74, 222, 128, 0.35);
}

.auction-card-dark__media {
  position: relative;
  aspect-ratio: 300 / 190;
  overflow: hidden;
  background: #0a0a0a;
}

.auction-card-dark__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.auction-card-dark__overlay {
  position: absolute;
  top: 12px;
  right: 12px;
  z-index: 1;
  display: flex;
  align-items: center;
  gap: 8px;
}

.auction-card-dark__timer {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 28px;
  padding: 0 8px;
  color: #fff;
  font-family: var(--font-segoe-ui-semibold);
  font-size: 12px;
  font-weight: 600;
  line-height: 1;
  background: rgba(0, 0, 0, 0.35);
  border: 1px solid rgba(255, 255, 255, 0.32);
  border-radius: 999px;
  backdrop-filter: blur(16px);
}

.auction-card-dark__heart {
  display: flex;
  align-items: center;
  justify-content: center;
  color: #fff;
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
}

.auction-card-dark__heart:disabled {
  opacity: 0.6;
}

.auction-card-dark__body {
  display: flex;
  flex-direction: column;
  gap: 14px;
  padding: 16px;
  flex: 1;
}

.auction-card-dark__head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 10px;
}

.auction-card-dark__title {
  margin: 0;
  font-family: var(--font-impact-regular);
  font-size: 16px;
  line-height: 1.2;
  text-transform: uppercase;
  color: #fff;
}

.auction-card-dark__sub {
  margin: 4px 0 0;
  font-family: var(--font-segoe-ui-regular);
  font-size: 13px;
  line-height: 18px;
  color: #8a8a8a;
}

.auction-card-dark__badge {
  display: inline-flex;
  align-items: center;
  flex-shrink: 0;
  padding: 4px 8px;
  border-radius: 6px;
  font-family: var(--font-segoe-ui-semibold);
  font-size: 11px;
  font-weight: 600;
  line-height: 1.2;
}

.auction-card-dark__badge.is-met {
  color: #22c55e;
  border: 1px solid #22c55e;
  background: transparent;
}

.auction-card-dark__badge.is-unmet {
  color: #ff1313;
  border: 1px solid #ff1313;
  background: transparent;
}

.auction-card-dark__specs {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
}

.auction-card-dark__spec {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 8px;
  border-radius: 8px;
  background: #1c1c1c;
}

.auction-card-dark__spec-label {
  font-family: var(--font-segoe-ui-regular);
  font-size: 11px;
  text-transform: capitalize;
  color: #8a8a8a;
}

.auction-card-dark__spec-value {
  font-family: var(--font-segoe-ui-bold);
  font-size: 13px;
  line-height: 1.2;
  color: #fff;
}

.auction-card-dark__footer {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: auto;
}

.auction-card-dark__bid-label {
  font-family: var(--font-segoe-ui-regular);
  font-size: 12px;
  color: #ff1313;
}

.auction-card-dark__bid-amount {
  font-family: var(--font-impact-regular);
  font-size: 16px;
  line-height: 1.15;
  color: #fff;
}

.auction-card-dark__footer .btn {
  min-height: 36px;
  height: 36px;
  padding: 6px 14px;
  font-size: 13px;
  border-radius: 6px;
}

.auction-card-dark__share {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  flex-shrink: 0;
  border-radius: 50%;
  border: 1px solid #333;
  background: #0a0a0a;
  color: #fff;
  cursor: pointer;
}

.auction-card-dark__share:hover {
  border-color: #ff1313;
  color: #ff1313;
}

/* ── VIEWING AS switch ───────────────────────────────────────────────────────
   A real two-state control: a thumb slides between the halves. Previously the
   only feedback was text weight plus a dot that was `hidden` when unselected —
   so nothing moved, and the row reflowed as the dot popped in and out. */
.role-switch {
  position: relative;
  display: flex;
  align-items: stretch;
  align-self: stretch;
  padding: 5px;
  border-radius: 8px;
  background: #262626;
  overflow: hidden;
}

.role-switch__option {
  position: relative;
  /* above the thumb */
  z-index: 1;
  flex: 1 1 0;
  /* ⚠️ equal halves — see the note in DashboardSidebar.tsx */
  min-width: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 2px 4px;
  cursor: pointer;
  background: none;
  border: 0;
}

.role-switch__label {
  transition: color 200ms ease;
  /* ⚠️ `body { word-break: break-word }` in /css/style.css was splitting
     "Buyer/Seller" into "Buyer/Sell er" once the half got narrow. */
  white-space: nowrap;
  word-break: normal;
}

/* Centred because both halves are equal width and it is the middle child. */
.role-switch__divider {
  position: relative;
  z-index: 1;
  width: 1px;
  align-self: stretch;
  flex: 0 0 auto;
  background: rgba(255, 255, 255, 0.2);
}

/* Both dots are always present; only the fill changes. */
.role-indicator {
  border: 1px solid rgba(255, 255, 255, 0.35);
  background: transparent;
  transition: background-color 200ms ease, border-color 200ms ease, transform 200ms ease;
}

.role-indicator__core {
  background: transparent;
  transition: background-color 200ms ease;
}

.role-indicator.is-selected {
  background: #ff1313;
  border-color: #ff1313;
  transform: scale(1.05);
}

.role-indicator.is-selected .role-indicator__core {
  background: #ffffff;
}

/* Motion-sensitive users get the state change with no sliding or scaling. */
@media (prefers-reduced-motion: reduce) {

  .role-indicator,
  .role-indicator__core,
  .role-switch__label {
    transition: none;
  }

  .role-indicator.is-selected {
    transform: none;
  }
}

/* Sidebar sections replay this on a role switch (the wrapper is keyed on
   activeRole in DashboardSidebar.tsx, so React remounts it). Short and subtle —
   the point is to confirm "the menu just changed", not to make the user wait. */
/* ── Role switch: the DASHBOARD is what animates ─────────────────────────────
   The VIEWING AS control itself is intentionally plain (a filled dot, nothing
   else). The feeling of "I just switched dashboards" comes from here: both the
   sidebar sections and the main content replay an enter animation, because each
   is keyed on `activeRole` and therefore remounted on a switch.

   Content leads slightly (a longer travel, a beat later) so the eye follows the
   menu change across to the panel rather than seeing one flat repaint. */

/* ── Bidding nudge ───────────────────────────────────────────────────────────
   Shown under the account pill to a signed-in user who has not verified a card
   yet (`users.bidder_verified` is false, §9b).

   LIGHT, not black. It hangs off the black header bar, and a dark panel there
   just merged into it — a flat shadow with no separation. A light card reads as
   a distinct layer floating above the bar, matches the white nav strip directly
   below, and lets the brand red carry the action.

   Deliberately quiet overall: one sentence and an underlined link. No button, no
   warning icon, no coloured edge — nothing has gone wrong, and a loud element
   here would compete with Search and Cart sitting right beside it. */
.finish-setup {
  display: flex;
  align-items: center;
  gap: 14px;
  border-radius: 10px;
  background: #ffffff;
  border: 1px solid #e6e6e6;
  box-shadow: 0 14px 34px rgba(0, 0, 0, 0.22), 0 2px 6px rgba(0, 0, 0, 0.06);
}

.finish-setup__text {
  margin: 0;
  flex: 1 1 auto;
  font-family: var(--font-segoe-ui-regular);
  font-size: 13.5px;
  line-height: 1.45;
  color: #3f3f3f;
  /* One line: the sentence and its link belong together, and a two-line tooltip
     in the header reads as a notice rather than a passing hint. The popover is
     width:max-content, so it simply grows to fit. */
  white-space: nowrap;
}

.finish-setup__link {
  font-family: var(--font-segoe-ui-bold);
  color: #ff1313;
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-thickness: 1.5px;
  white-space: nowrap;
  transition: color 160ms ease;
}

.finish-setup__link:hover {
  color: #d70501;
}

.finish-setup__close {
  width: 15px;
  height: 15px;
  flex-shrink: 0;
  padding: 0;
  border: 0;
  background: none;
  color: #b0b0b0;
  cursor: pointer;
  transition: color 160ms ease;
}

.finish-setup__close:hover {
  color: #171717;
}

/* Anchored under the header account pill. */
.finish-setup--popover {
  position: absolute;
  top: calc(100% + 12px);
  right: 0;
  z-index: 60;
  width: max-content;
  /* Wide enough for the sentence + link on one line; still clamped on narrow
     viewports, where the nowrap above yields to the media query below. */
  max-width: min(460px, calc(100vw - 24px));
  padding: 11px 14px;
  animation: finish-setup-in 240ms cubic-bezier(0.22, 1, 0.36, 1) both;
}

/* Arrow matches the card, so it reads as one shape attached to the pill. */
.finish-setup--popover::before {
  content: '';
  position: absolute;
  top: -5px;
  right: 22px;
  width: 9px;
  height: 9px;
  background: #ffffff;
  border-left: 1px solid #e6e6e6;
  border-top: 1px solid #e6e6e6;
  transform: rotate(45deg);
}

@keyframes finish-setup-in {
  from {
    opacity: 0;
    transform: translateY(-6px);
  }

  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.finish-setup--bar {
  padding: 13px 16px;
  align-self: stretch;
}

@media (max-width: 520px) {
  .finish-setup__text {
    white-space: normal;
  }
}

@media (prefers-reduced-motion: reduce) {
  .finish-setup--popover {
    animation: none;
  }

  .finish-setup__link,
  .finish-setup__close {
    transition: none;
  }
}

/* ── Verification success screen ─────────────────────────────────────────────
   features/bidder/VerificationSuccess.tsx. Green is scoped to this screen only —
   it is the one place in the app that means "completed". */
.verify-done {
  border-radius: 14px;
  background:
    /* faint dot grid, top-left */
    radial-gradient(circle at 1px 1px, rgba(22, 128, 60, 0.10) 1px, transparent 0) top left / 16px 16px no-repeat,
    linear-gradient(180deg, #ffffff 0%, #f7faf8 100%);
  background-size: 220px 200px, 100% 100%;
}

/* Rays behind the badge. Rotation is set inline per ray; this only sizes them. */
.verify-done__ray {
  position: absolute;
  width: 3px;
  height: 18px;
  border-radius: 999px;
  background: #79c98d;
  transform-origin: center bottom;
}

/* The ONE looping animation on this screen — slow and low-contrast, so it reads
   as the badge being lit rather than as something asking to be clicked. */
.verify-done__halo {
  position: absolute;
  inset: -10px;
  border-radius: 9999px;
  background: radial-gradient(circle, rgba(34, 197, 94, 0.28) 0%, rgba(34, 197, 94, 0) 70%);
  animation: verify-done-halo 4s ease-in-out infinite;
}

@keyframes verify-done-halo {

  0%,
  100% {
    transform: scale(1);
    opacity: 0.5;
  }

  50% {
    transform: scale(1.15);
    opacity: 0.9;
  }
}

/* CTAs use the BRAND palette, not the success green.
   Green on this screen is feedback ("done"); red is action. Making the buttons
   green too meant the page had no colour hierarchy — everything shouted at the
   same volume, and the buttons stopped looking like the site's buttons. */
.verify-done__cta {
  display: inline-flex;
  width: 100%;
  align-items: center;
  justify-content: center;
  gap: 8px;
  min-height: 48px;
  padding: 0 18px;
  border-radius: 8px;
  font-family: var(--font-segoe-ui-bold);
  font-size: 15px;
  text-decoration: none;
  transition: background-color 160ms ease, color 160ms ease, border-color 160ms ease;
}

.verify-done__cta--primary {
  background: #ff1313;
  color: #ffffff;
  border: 1.5px solid #ff1313;
}

.verify-done__cta--primary:hover {
  background: #d70501;
  border-color: #d70501;
}

.verify-done__cta--ghost {
  background: #ffffff;
  color: #171717;
  border: 1.5px solid #d9d9d9;
}

.verify-done__cta--ghost:hover {
  border-color: #171717;
  background: #fafafa;
}

@media (prefers-reduced-motion: reduce) {
  .verify-done__halo {
    animation: none;
  }

  .verify-done__cta {
    transition: none;
  }
}

/* ── Auction detail gallery ──────────────────────────────────────────────────
   Sticks below the header while the bid panels scroll beside it.

   ⚠️ Offset comes from `--site-header-height` (published by
   useHeaderScrollShadow), NOT a hard-coded pixel value. The old `top: 170px`
   was measured against the PUBLIC site header, so inside the dashboard the
   gallery pinned at an arbitrary point while the Back button and title scrolled
   past it. One variable is correct in both layouts.

   Sticky here is also what closes the gap the user sees: the gallery is much
   shorter than the bid column, so without it the row leaves a tall empty block
   under the image before the tabs. */
@media (min-width: 1280px) {
  .auction-gallery {
    position: sticky;
    top: calc(var(--site-header-height, 84px) + 16px);
    align-self: flex-start;
  }
}

/* ── Bid placed — inline animated confirmation ───────────────────────────────
   Replaces the full-screen "Bid placed successfully" modal on the auction
   DETAIL page only. There the bid panel stays on screen, so a modal covers the
   very numbers the bidder wants to see move (current bid, bid count, timer) and
   has to be dismissed before they can bid again. The listing-grid flow still
   uses the modal, because PlaceBidModal closes itself on success and would
   otherwise confirm nothing.

   Motion is layered so it reads as one event rather than five: panel lands →
   ring draws → tick draws → confetti bursts → sheen sweeps → bar counts out.
   All of it collapses under prefers-reduced-motion at the bottom. */

.bid-flash {
  position: relative;
  overflow: hidden;
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 16px 18px 20px;
  border-radius: 12px;
  border: 1px solid #9ff08f;
  /* Both hosts are flex COLUMNS (the modal body and the detail bid panel), so
     without these the panel gets shrunk to a sliver and `overflow: hidden`
     clips the badge and the text right out of view. */
  flex: 0 0 auto;
  align-self: stretch;
  width: 100%;
  min-height: 104px;
  box-sizing: border-box;
  background:
    radial-gradient(120% 160% at 0% 0%, #e6ffe0 0%, rgba(230, 255, 224, 0) 60%),
    linear-gradient(90deg, #f1fff0 0%, #ffffff 100%);
  animation:
    bid-flash-in 560ms cubic-bezier(0.22, 1.4, 0.36, 1) both,
    bid-flash-glow 1500ms ease-out both;
}

/* Light sweeping across once, so the eye is pulled over even if the bidder was
   watching the clock when it appeared. */
.bid-flash::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(100deg, transparent 20%, rgba(255, 255, 255, 0.9) 50%, transparent 80%);
  transform: translateX(-100%);
  animation: bid-flash-sheen 1200ms ease-out 260ms both;
  pointer-events: none;
}

.bid-flash--leaving {
  animation: bid-flash-out 340ms cubic-bezier(0.4, 0, 1, 1) both;
}

/* Auto-dismiss made visible — the bar drains for exactly as long as the panel
   is going to stay, so nothing vanishes without warning. Duration comes from
   --bid-flash-hold, set inline by the component from its own holdMs. */
.bid-flash__timer {
  position: absolute;
  left: 0;
  bottom: 0;
  height: 3px;
  width: 100%;
  transform-origin: left center;
  background: linear-gradient(90deg, #0aa200, #7be06b);
  animation: bid-flash-drain var(--bid-flash-hold, 5000ms) linear both;
}

.bid-flash__badge {
  position: relative;
  /* Explicit basis, not `auto` — a flex child with absolutely-positioned
     children measures as zero-width and collapses under the text. */
  flex: 0 0 42px;
  display: grid;
  place-items: center;
  width: 42px;
  height: 42px;
  align-self: center;
}

.bid-flash__badge-fill {
  position: absolute;
  inset: 5px;
  border-radius: 999px;
  background: radial-gradient(circle at 30% 30%, #35c02c, #0aa200);
  animation: bid-flash-pop 620ms cubic-bezier(0.34, 1.7, 0.5, 1) 300ms both;
}

/* Expanding halo, like a drop landing. One pulse, then gone. */
.bid-flash__badge::after {
  content: '';
  position: absolute;
  inset: 5px;
  border-radius: 999px;
  border: 2px solid #0aa200;
  animation: bid-flash-ring 1000ms ease-out 420ms both;
}

/* The ring traces itself around the badge before the tick lands inside it. */
.bid-flash__ring {
  position: absolute;
  inset: 0;
  transform: rotate(-90deg);
}

.bid-flash__ring circle {
  fill: none;
  stroke: #0aa200;
  stroke-width: 2.5;
  stroke-linecap: round;
  stroke-dasharray: 113;
  stroke-dashoffset: 113;
  animation: bid-flash-trace 520ms cubic-bezier(0.4, 0, 0.2, 1) both;
}

.bid-flash__tick {
  position: relative;
  stroke-dasharray: 22;
  stroke-dashoffset: 22;
  animation: bid-flash-draw 340ms cubic-bezier(0.4, 0, 0.2, 1) 620ms forwards;
}

/* Confetti. Each particle carries its own --tx/--ty/--rot/--delay/--hue from
   the component, so one keyframe drives all of them along different arcs. */
/* Anchored to the badge centre: 16px padding + half of the 42px badge. */
.bid-flash__confetti {
  position: absolute;
  left: 39px;
  top: 50%;
  width: 0;
  height: 0;
  pointer-events: none;
}

.bid-flash__bit {
  position: absolute;
  width: 7px;
  height: 10px;
  border-radius: 2px;
  background: var(--bit-color, #0aa200);
  opacity: 0;
  animation: bid-flash-burst 1000ms cubic-bezier(0.15, 0.7, 0.3, 1) var(--bit-delay, 0ms) both;
}

.bid-flash__text {
  display: flex;
  flex-direction: column;
  gap: 3px;
  flex: 1 1 auto;
  min-width: 0;
}

.bid-flash__title {
  color: #0b7a2c;
  animation: bid-flash-rise 460ms ease-out 260ms both;
}

.bid-flash__meta {
  color: #3f7a52;
  animation: bid-flash-rise 460ms ease-out 360ms both;
}

.bid-flash__amount {
  color: #0b7a2c;
  animation: bid-flash-rise 460ms ease-out 420ms both;
}

@keyframes bid-flash-in {
  0% {
    opacity: 0;
    transform: translateY(-14px) scale(0.94);
  }

  60% {
    opacity: 1;
  }

  100% {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
}

@keyframes bid-flash-glow {
  0% {
    box-shadow: 0 0 0 0 rgba(10, 162, 0, 0.45);
  }

  35% {
    box-shadow: 0 0 0 8px rgba(10, 162, 0, 0.12);
  }

  100% {
    box-shadow: 0 0 0 0 rgba(10, 162, 0, 0);
  }
}

@keyframes bid-flash-out {
  from {
    opacity: 1;
    transform: translateY(0) scale(1);
  }

  to {
    opacity: 0;
    transform: translateY(-8px) scale(0.98);
  }
}

@keyframes bid-flash-sheen {
  to {
    transform: translateX(100%);
  }
}

@keyframes bid-flash-drain {
  from {
    transform: scaleX(1);
  }

  to {
    transform: scaleX(0);
  }
}

@keyframes bid-flash-trace {
  to {
    stroke-dashoffset: 0;
  }
}

@keyframes bid-flash-pop {
  0% {
    transform: scale(0);
    opacity: 0;
  }

  65% {
    transform: scale(1.15);
    opacity: 1;
  }

  100% {
    transform: scale(1);
    opacity: 1;
  }
}

@keyframes bid-flash-ring {
  0% {
    opacity: 0.6;
    transform: scale(1);
  }

  100% {
    opacity: 0;
    transform: scale(2.4);
  }
}

@keyframes bid-flash-draw {
  to {
    stroke-dashoffset: 0;
  }
}

@keyframes bid-flash-burst {
  0% {
    opacity: 0;
    transform: translate(0, 0) rotate(0deg) scale(0.4);
  }

  15% {
    opacity: 1;
  }

  70% {
    opacity: 1;
  }

  100% {
    opacity: 0;
    transform: translate(var(--bit-tx, 0), var(--bit-ty, 0)) rotate(var(--bit-rot, 180deg)) scale(0.9);
  }
}

@keyframes bid-flash-rise {
  from {
    opacity: 0;
    transform: translateY(6px);
  }

  to {
    opacity: 1;
    transform: translateY(0);
  }
}

/* Outbid tone — same panel, same motion, warning colours. Shown INLINE when
   someone else bids on the auction the user is currently looking at; the modal
   is reserved for an outbid they would otherwise miss (AuctionAlertsHost). */
.bid-flash--outbid {
  border-color: #ffc9a3;
  background:
    radial-gradient(120% 160% at 0% 0%, #fff1e4 0%, rgba(255, 241, 228, 0) 60%),
    linear-gradient(90deg, #fff6ee 0%, #ffffff 100%);
  animation:
    bid-flash-in 560ms cubic-bezier(0.22, 1.4, 0.36, 1) both,
    bid-flash-glow-warn 1500ms ease-out both;
}

.bid-flash--outbid .bid-flash__badge-fill {
  background: radial-gradient(circle at 30% 30%, #ff8a3d, #e05a00);
}

.bid-flash--outbid .bid-flash__badge::after {
  border-color: #e05a00;
}

.bid-flash--outbid .bid-flash__ring circle {
  stroke: #e05a00;
}

.bid-flash--outbid .bid-flash__timer {
  background: linear-gradient(90deg, #e05a00, #ffb37a);
}

.bid-flash--outbid .bid-flash__title {
  color: #b34700;
}

.bid-flash--outbid .bid-flash__amount {
  color: #b34700;
}

.bid-flash--outbid .bid-flash__meta {
  color: #8a5a37;
}

@keyframes bid-flash-glow-warn {
  0% {
    box-shadow: 0 0 0 0 rgba(224, 90, 0, 0.45);
  }

  35% {
    box-shadow: 0 0 0 8px rgba(224, 90, 0, 0.12);
  }

  100% {
    box-shadow: 0 0 0 0 rgba(224, 90, 0, 0);
  }
}

/* The current-bid figure ticks up green for a beat, so the confirmation and the
   number that changed read as the same event rather than two. */
.bid-amount-bump {
  animation: bid-amount-bump 1100ms cubic-bezier(0.34, 1.56, 0.64, 1) both;
}

@keyframes bid-amount-bump {
  0% {
    color: #0aa200;
    transform: scale(1);
  }

  30% {
    color: #0aa200;
    transform: scale(1.12);
  }

  100% {
    color: inherit;
    transform: scale(1);
  }
}

@media (prefers-reduced-motion: reduce) {

  .bid-flash,
  .bid-flash--outbid,
  .bid-flash--leaving,
  .bid-flash__badge-fill,
  .bid-flash__title,
  .bid-flash__meta,
  .bid-flash__amount,
  .bid-flash__timer,
  .bid-amount-bump {
    animation: none;
  }

  .bid-flash::after,
  .bid-flash__badge::after,
  .bid-flash__confetti {
    display: none;
  }

  .bid-flash__ring circle {
    stroke-dashoffset: 0;
    animation: none;
  }

  .bid-flash__tick {
    stroke-dashoffset: 0;
    animation: none;
  }
}

/* ── Toasts ──────────────────────────────────────────────────────────────────
   A plain white card that borrows the design system's own geometry rather than
   inventing its own: 8px radius (`.btn`), the neutral #DED8CC-family border and
   the same type ramp as the rest of the site.

   ⚠️ TONE LIVES ON THE ICON ONLY. An earlier pass tinted the border, the title
   AND a full-height left bar — three greens on one success toast, and the bar's
   square corners fought the card's rounded ones. One small colour cue against
   neutral chrome reads faster and matches the site.

   ⚠️ Anchored BOTTOM-RIGHT. Pinned to the top it covered the search field and
   the account pill, and the bottom corner is the quietest part of the viewport
   — furthest from the header, the mega menu and the primary CTA on most pages.

   Because the stack is anchored at its bottom edge and lays out top-to-bottom,
   the NEWEST toast sits closest to the corner and older ones ride upward. That
   is the order the eye returns to. Motion handles enter/exit; this sheet only
   styles. */
.toast-stack {
  position: fixed;
  bottom: 20px;
  right: 16px;
  z-index: 120;
  display: flex;
  flex-direction: column;
  gap: 10px;
  width: min(92vw, 356px);
  pointer-events: none;
}

.toast {
  pointer-events: auto;
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 12px 12px 12px 13px;
  border: 1px solid #e6e3dd;
  border-radius: 8px;
  background: #ffffff;
  /* Two layers: a tight contact shadow to seat the card, and a wide soft one
     for lift. A single heavy blur read as a drop-shadow sticker. */
  box-shadow: 0 1px 2px rgba(23, 23, 23, 0.06), 0 8px 24px rgba(23, 23, 23, 0.10);
}

.toast__icon {
  flex: none;
  width: 18px;
  height: 18px;
  margin-top: 1px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  color: #ffffff;
}

.toast--success .toast__icon {
  background: #14994a;
}

.toast--error .toast__icon {
  background: #d9251d;
}

.toast--info .toast__icon {
  background: #171717;
}

.toast__body {
  min-width: 0;
  flex: 1 1 auto;
}

.toast__title {
  display: block;
  font-family: var(--font-segoe-ui-semibold);
  font-size: 14px;
  line-height: 20px;
  color: #171717;
}

.toast__message {
  display: block;
  margin-top: 1px;
  font-family: var(--font-segoe-ui-regular);
  font-size: 13px;
  line-height: 18px;
  color: #68645d;
}

.toast__close {
  flex: none;
  margin-top: 1px;
  color: #a3a09a;
  border-radius: 4px;
  transition: color 140ms ease, background-color 140ms ease;
}

.toast__close:hover {
  color: #171717;
  background: #f2f0ec;
}

.toast__close:focus-visible {
  outline: 2px solid #171717;
  outline-offset: 1px;
}

@media (max-width: 640px) {
  .toast-stack {
    right: 12px;
    left: 12px;
    bottom: 14px;
    width: auto;
  }
}

/* ── Auction win celebration ──────────────────────────────────────────────
   Confetti and the trophy bounce. Kept here rather than inline so the
   keyframes exist once; developer.css is the only sheet we author. */
@keyframes win-confetti-fall {
  0% {
    transform: translateY(-12vh) rotate(0deg);
    opacity: 0;
  }

  8% {
    opacity: 1;
  }

  100% {
    transform: translateY(105vh) rotate(540deg);
    opacity: 0;
  }
}

.win-confetti {
  position: absolute;
  top: -10vh;
  border-radius: 2px;
  animation-name: win-confetti-fall;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}

@keyframes win-trophy-pop {
  0% {
    transform: scale(0.4) rotate(-12deg);
    opacity: 0;
  }

  55% {
    transform: scale(1.18) rotate(6deg);
    opacity: 1;
  }

  75% {
    transform: scale(0.96) rotate(-3deg);
  }

  100% {
    transform: scale(1) rotate(0deg);
    opacity: 1;
  }
}

.win-trophy {
  display: inline-block;
  animation: win-trophy-pop 0.75s cubic-bezier(0.22, 1, 0.36, 1) both;
  filter: drop-shadow(0 6px 14px rgba(255, 197, 49, 0.45));
}

/* Respect a reduced-motion preference: the modal still appears, it just does
   not throw confetti at somebody who asked the OS not to animate things. */
@media (prefers-reduced-motion: reduce) {
  .win-confetti {
    display: none;
  }

  .win-trophy {
    animation: none;
  }
}

/* ── Auction detail: live bidding feed ───────────────────────────────────────
   The newest row flashes once when it changes. Keyed on the row's own mount:
   the list is re-fetched whenever a bid lands, so React remounts the top <li>
   with a new key and the animation replays without any timer of its own. */
@keyframes bid-feed-flash {
  0% {
    background-color: rgba(26, 122, 26, 0.16);
  }

  100% {
    background-color: transparent;
  }
}

.bid-feed-latest {
  animation: bid-feed-flash 1.1s ease-out;
}

@media (prefers-reduced-motion: reduce) {
  .bid-feed-latest {
    animation: none;
  }
}

/* ── Storefront testimonials: auto-scrolling column ─────────────────────────
 *
 * The quote column sits beside a fixed-height image strip. With more quotes
 * than fit, the column scrolls itself instead of either stretching the section
 * or clipping quotes nobody can reach.
 *
 * The track holds the list TWICE and translates by exactly -50%, which is what
 * makes the loop seamless — at -50% the second copy sits exactly where the
 * first started, so the reset is invisible. The clone is aria-hidden, so a
 * screen reader still reads each quote once.
 *
 * Duration is set per-instance from JS (--marquee-duration) so the speed stays
 * constant regardless of how many quotes there are.
 */
.storefront-marquee-track.is-scrolling {
  animation: storefront-marquee-scroll var(--marquee-duration, 30s) linear infinite;
}

/* Pause on hover AND on keyboard focus inside: a quote you are trying to read
 * (or tab through) must not slide out from under you. */
.storefront-marquee:hover .storefront-marquee-track.is-scrolling,
.storefront-marquee:focus-within .storefront-marquee-track.is-scrolling {
  animation-play-state: paused;
}

@keyframes storefront-marquee-scroll {
  from {
    transform: translateY(0);
  }

  to {
    transform: translateY(-50%);
  }
}

/* Belt and braces: the component already switches to a hand-scrollable column
 * when this preference is set (it never adds `is-scrolling` then), but if the
 * preference changes after paint this stops the animation immediately. */
@media (prefers-reduced-motion: reduce) {
  .storefront-marquee-track.is-scrolling {
    animation: none;
  }

  /* ── Connect embedded components (Banking, §9x Phase 2) ──────────────────────
   ⚠️ WRAPPER ONLY. The components render inside a CROSS-ORIGIN IFRAME, so not
   one rule here reaches their internals — styling them is done exclusively via
   the `appearance` object in features/connect/connectInstance.ts. Do not add
   selectors targeting Stripe's markup; they cannot match.

   Stripe's guidance: the components behave like block elements, take 100% of
   the parent's width, and grow in height with their content. Height cannot be
   set directly — only bounded with max-height + overflow. We let them grow. */
  .connect-embed {
    width: 100%;
    min-width: 0;
    /* The iframe inherits font-family and background from its parent container,
     so these two are what carry the site's typography into the panel. */
    font-family: var(--font-segoe-ui-regular);
    background: #ffffff;
  }

  .connect-embed__loading {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 24px 0;
  }

  .connect-embed__spinner {
    width: 16px;
    height: 16px;
    flex: none;
    border-radius: 50%;
    border: 2px solid #e0ddd7;
    border-top-color: #ff1313;
    animation: connect-embed-spin 700ms linear infinite;
  }

  @keyframes connect-embed-spin {
    to {
      transform: rotate(360deg);
    }
  }

  @media (prefers-reduced-motion: reduce) {
    .connect-embed__spinner {
      animation: none;
    }
  }
}
/* ─────────────────────────────────────────────────────────────────────────────
 * LIVE BIDDING BOARD (seller) — /bidding-board
 *
 * A monitoring screen, not a browsing screen: dense rows, a dark instrument
 * panel at the top, and numbers that move. The dark treatment is deliberate and
 * scoped to this page only — it echoes the site header and it is the one screen
 * where a market-board read is the right one.
 * ───────────────────────────────────────────────────────────────────────────── */

.board-num {
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}

/* ── Instrument panel ─────────────────────────────────────────────────────── */
.board-panel {
  background:
    radial-gradient(120% 160% at 88% -30%, rgba(255, 19, 19, 0.16), transparent 62%),
    #0e0f11;
  border-radius: 12px;
  color: #ffffff;
  overflow: hidden;
}

.board-kpis {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1px;
  background: #22252a;
  border-top: 1px solid #22252a;
}

@media (min-width: 900px) {
  .board-kpis {
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }
}

.board-kpi {
  background: #0e0f11;
  padding: 10px 18px 12px;
}

/* ── Lot rows ─────────────────────────────────────────────────────────────── */
.board-row {
  display: grid;
  grid-template-columns: 3px 1fr;
  align-items: stretch;
  border: 1px solid #e6e8ea;
  border-radius: 10px;
  background: #ffffff;
  overflow: hidden;
  transition: border-color 180ms ease, box-shadow 180ms ease, background-color 700ms ease;
}

.board-row:hover {
  border-color: #d2d6da;
  box-shadow: 0 2px 10px rgba(14, 15, 17, 0.06);
}

.board-row__stripe {
  background: #d8dbe0;
}

.board-row[data-state="live"] .board-row__stripe { background: #ff1313; }
.board-row[data-state="closing"] .board-row__stripe { background: #ff1313; }
.board-row[data-state="paused"] .board-row__stripe { background: #e07000; }
.board-row[data-state="upcoming"] .board-row__stripe { background: #2563eb; }

.board-row__body {
  display: grid;
  gap: 14px 18px;
  padding: 14px 18px;
  grid-template-columns: minmax(0, 1fr);
  align-items: center;
}

@media (min-width: 720px) {
  .board-row__body {
    grid-template-columns: minmax(200px, 1fr) 128px 120px minmax(124px, max-content);
  }
}

/* The control column is `max-content`, never a fixed width. A paused lot swaps
   Pause for Resume and gains a character; at a fixed 200px that was enough to
   break the labels onto two lines ("Vie w" / "Resum e"). Sizing to the widest
   label set means the row cannot be broken by its own state changing. */
@media (min-width: 1180px) {
  .board-row__body {
    /* Six tracks: lot · price · clock · state · admin action · controls.
       ⚠️ The admin-action track is CAPPED, not `max-content`. It can hold a
       rejection note whose length is the admin's choice, and `max-content`
       sized the column to the longest unwrapped line — one paragraph swallowed
       half the board. A ceiling makes the note wrap and the clamp in
       `AdminNote` do the rest. */
    grid-template-columns:
      minmax(220px, 1fr) 140px 124px minmax(136px, max-content) minmax(170px, 210px) max-content;
  }
}

/* Admin action spans the row until the board is wide enough to give it a
   column, exactly as the controls below it do. Without this it would land in
   the controls' track and push them into an undeclared sixth column. */
.board-row__admin {
  grid-column: 1 / -1;
  padding-top: 4px;
  /* Below the six-column breakpoint this spans the full row, so the note needs
     its own ceiling or it runs the width of the board on one line. */
  max-width: 460px;
  min-width: 0;
}

@media (min-width: 1180px) {
  .board-row__admin {
    grid-column: auto;
    padding-top: 0;
  }
}

/* Actions wrap under the row until there is room for a column of their own. */
.board-row__actions {
  display: flex;
  align-items: center;
  gap: 8px;
  grid-column: 1 / -1;
  padding-top: 4px;
  border-top: 1px solid #f1f2f4;
}

/* A control label never wraps and never shrinks. `.btn` sets no white-space, so
   a squeezed flex item breaks mid-word — which is what produced "End l ot". */
.board-row__actions .btn {
  flex: none;
  white-space: nowrap;
}

/* Frozen-clock caption on a paused lot ("27d 23h when resumed"). It is the
   longest string in that column and the only one that can force a second line,
   so it keeps its own tighter leading instead of stretching the whole row. */
.board-row__held {
  white-space: nowrap;
  line-height: 1.3;
}

@media (min-width: 1180px) {
  .board-row__actions {
    grid-column: auto;
    justify-content: flex-end;
    padding-top: 0;
    border-top: 0;
  }
}

/* A price that just moved. Same green the public grid flashes, held long
   enough to notice without turning the board into a light show. */
.board-row[data-pulse="1"] {
  background: #f0faf4;
  border-color: #bfe6cf;
}

/* ── Bid tape ─────────────────────────────────────────────────────────────── */
.board-tape {
  max-height: 560px;
  overflow-y: auto;
  scrollbar-width: thin;
}

.board-tape::-webkit-scrollbar { width: 6px; }
.board-tape::-webkit-scrollbar-thumb { background: #d8dbe0; border-radius: 3px; }

.board-tape__item {
  animation: board-tape-in 320ms ease-out both;
}

@keyframes board-tape-in {
  from { opacity: 0; transform: translateY(-6px); }
  to { opacity: 1; transform: none; }
}

/* ── Live lamp ────────────────────────────────────────────────────────────── */
.board-lamp {
  animation: board-lamp-pulse 1800ms ease-in-out infinite;
}

@keyframes board-lamp-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.35; }
}

@media (prefers-reduced-motion: reduce) {
  .board-lamp,
  .board-tape__item {
    animation: none;
  }
  .board-row {
    transition: none;
  }
}

/* ─────────────────────────────────────────────────────────────────────────────
 * BIDDING CONSOLE — /bid/:id
 *
 * One screen, four zones: the lot, the decision, the closing order, the
 * catalogue. Dark, because it is an operating surface rather than a browsing
 * one, and because the clock has to be the loudest thing on it — in a timed
 * sale the clock is the auctioneer.
 *
 * The layout is expressed here rather than in Tailwind classes because the
 * three-column grid collapses in two distinct steps, and encoding that as
 * a dozen responsive utilities per element is how the steps drift apart.
 * ───────────────────────────────────────────────────────────────────────────── */

.console {
  --c-night: #0e0f11;
  --c-night-2: #16181b;
  --c-line: #2a2e34;
  --c-dim: #8b9098;
  --c-dimmer: #767c84;
  --c-red: #ff1313;
  background: var(--c-night);
  color: #fff;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
}

/* ⚠️ THE RULE THE WHOLE LAYOUT HANGS ON, from the tablet step up:
   the console IS the viewport and the page never scrolls.

   Every part of this screen is something a bidder needs while deciding — the
   clock, the price, the control, the running order, the lot table. Putting any
   of them below the fold means finding them costs a scroll during the sixty
   seconds when finding them matters most. So the shell is locked to the screen
   and the parts that can overflow scroll INSIDE themselves. */
@media (min-width: 768px) {
  .console {
    height: 100dvh;
    min-height: 0;
    overflow: hidden;
  }
}

/* ── Top bar ──────────────────────────────────────────────────────────────── */
.console-top {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
  padding: 10px 16px;
  border-bottom: 1px solid var(--c-line);
}

.console-top__cell {
  padding: 0 14px;
  border-left: 1px solid var(--c-line);
  white-space: nowrap;
}

/* Below the tablet step the wallet figures wrap onto their own row, so the
   dividers would draw against nothing. */
@media (max-width: 767px) {
  .console-top__cell {
    border-left: 0;
    padding: 0 10px 0 0;
  }
}

/* ── Body grid ────────────────────────────────────────────────────────────── */
.console-body {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  flex: 1 1 auto;
  min-height: 0;
}

@media (min-width: 768px) {
  .console-body {
    /* Takes whatever the top bar and the lot table leave, and no more — the
       three columns then scroll within themselves rather than growing the page. */
    flex: 1 1 auto;
    min-height: 0;
    overflow: hidden;
    /* ⚠️ `minmax(0, 1fr)`, never a bare `1fr` or the implicit `auto` row.
       A grid row sized to content is sized to the TALLEST child, so the rail's
       lot list would set the row height and the whole thing would be clipped by
       the `overflow: hidden` above — columns cut off mid-row, scrolling nothing.
       Pinning the row to the container is what lets the children scroll. */
    grid-template-rows: minmax(0, 1fr);
  }

  /* ⚠️ And the same trap one level down: a grid ITEM defaults to
     `min-height: auto`, which refuses to shrink below its content. Without this
     `overflow-y: auto` on the columns does nothing at all — they grow, the row
     grows with them, and the clip happens instead of the scroll. */
  .console-lot,
  .console-decision,
  .console-rail {
    min-height: 0;
  }

  .console-lot,
  .console-decision {
    overflow-y: auto;
  }

  .console-rail {
    overflow: hidden;
  }
}

/* Tablet: the decision column takes the width; the lot sits above it and the
   rail moves below, still a rail but horizontal. */
@media (min-width: 768px) and (max-width: 1279px) {
  .console-body {
    grid-template-columns: 340px minmax(0, 1fr);
    /* TWO rows here, not one: the rail drops below the other two and spans them.
       With a single `minmax(0, 1fr)` it landed in an IMPLICIT row the container
       had no height left for, and the clip from `overflow: hidden` ate it. */
    grid-template-rows: minmax(0, 1fr) auto;
  }

  .console-rail {
    grid-column: 1 / -1;
    grid-row: 2;
    border-left: 0;
    border-top: 1px solid var(--c-line);
    /* Content height — it is a horizontal strip at this width, and its list
       scrolls sideways inside it. */
    overflow: visible;
  }
}

/* Wider flanks, shorter middle.
   The centre column holds one clock, one price and one button; given a third of
   a wide screen it stretched them across a line they never needed, and the
   timer floated in space. The lot and the rail have real content to spend width
   on — the lot's spec list and history, the rail's running order. */
@media (min-width: 1280px) {
  .console-body {
    grid-template-columns: 380px minmax(0, 1fr) 360px;
  }
}

@media (min-width: 1600px) {
  .console-body {
    grid-template-columns: 420px minmax(0, 1fr) 400px;
  }
}

/* The decision column stops growing long before the screen does. Centred, so
   the clock sits where the eye already is rather than hard against the lot. */
.console-decision__inner {
  display: flex;
  flex-direction: column;
  width: 100%;
  max-width: 620px;
  margin: 0 auto;
}

.console-lot {
  border-right: 1px solid var(--c-line);
  display: flex;
  flex-direction: column;
  min-width: 0;
}

@media (max-width: 767px) {
  .console-lot {
    border-right: 0;
    border-bottom: 1px solid var(--c-line);
  }
}

.console-decision {
  background: #fff;
  color: #16181b;
  display: flex;
  flex-direction: column;
  padding: 20px;
  min-width: 0;
}

@media (min-width: 768px) {
  .console-decision {
    /* Tight on the vertical, generous on the horizontal. The column is locked
       to the viewport (see the shell rule above), so every pixel of padding
       here is a pixel the content has to give back — and it was giving back
       enough to put a scrollbar on the one column that must never need one. */
    padding: 16px 24px;
  }
}

.console-rail {
  background: var(--c-night-2);
  border-left: 1px solid var(--c-line);
  display: flex;
  flex-direction: column;
  min-width: 0;
}

@media (max-width: 767px) {
  .console-rail {
    border-left: 0;
    border-top: 1px solid var(--c-line);
  }
}

/* Vertical list on desktop, horizontal swipe strip everywhere narrower —
   a wrapping grid would push the lot itself off the screen. */
.console-rail__list {
  display: flex;
  flex-direction: row;
  gap: 8px;
  overflow-x: auto;
  padding: 10px 12px;
  scroll-snap-type: x proximity;
}

.console-rail__list > * {
  flex: 0 0 208px;
  scroll-snap-align: start;
}

@media (min-width: 1280px) {
  .console-rail__list {
    flex-direction: column;
    gap: 0;
    overflow-x: visible;
    overflow-y: auto;
    padding: 0;
    scroll-snap-type: none;
  }

  .console-rail__list > * {
    flex: none;
  }
}

.console-rail__list::-webkit-scrollbar {
  height: 6px;
  width: 6px;
}

.console-rail__list::-webkit-scrollbar-thumb {
  background: #3a3f46;
  border-radius: 3px;
}

/* The rail ends with its own control, pinned to the bottom of the column, so
   the lot list can grow without pushing it out of reach. */
.console-rail__foot {
  margin-top: auto;
  border-top: 1px solid #2a2e34;
}

@media (min-width: 1280px) {
  .console-rail__list {
    /* Shrinkable, and `min-height: 0` for the same reason as the columns above:
       a flex item will not go below its content height without it, so the list
       would push the Follow control off the bottom of the rail. */
    flex: 0 1 auto;
    min-height: 0;
  }
}

/* ── The one control ──────────────────────────────────────────────────────
   The amount is printed ON the button, so there is no field to fill in and no
   increment to work out. It is the largest interactive thing on the screen and
   the only one that spends money. */
.console-bid {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  min-height: 62px;
  border: 0;
  border-radius: 8px;
  background: var(--c-red);
  color: #fff;
  font-family: var(--font-impact-regular), Impact, sans-serif;
  font-size: 21px;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  cursor: pointer;
  transition: background 160ms ease, box-shadow 160ms ease;
}

.console-bid:hover:not(:disabled) {
  background: #d70501;
}

.console-bid:disabled {
  cursor: not-allowed;
}

/* Not a dimmed red bar.
   Disabling the loudest element on the screen by fading it reads as broken —
   the eye still goes there, and what it finds says nothing. When there is
   nothing to press, the control steps back instead. */
.console-bid--calm,
.console-bid--calm:hover:not(:disabled) {
  background: #fff;
  color: #767c84;
  border: 1px solid #c9cdd3;
  box-shadow: none;
}

.console-bid:disabled:not(.console-bid--calm) {
  opacity: 0.55;
}

/* The clock moves onto the button in the last thirty seconds. */
.console-bid[data-urgent="1"] {
  box-shadow: 0 0 0 3px rgba(255, 19, 19, 0.24);
}

/* Mobile pins the bid control to the bottom of the viewport: on a phone in a
   yard the one thing that must never require finding is the bid. */
.console-sticky {
  position: sticky;
  bottom: 0;
  z-index: 20;
  background: #fff;
  border-top: 1px solid #e8e8e8;
  padding: 12px 16px calc(12px + env(safe-area-inset-bottom, 0px));
  margin: 0 -20px -20px;
}

@media (min-width: 768px) {
  .console-sticky {
    position: static;
    border-top: 0;
    padding: 0;
    margin: 0;
  }
}

/* ── Catalogue strip ──────────────────────────────────────────────────────── */
.console-catalogue {
  background: #fff;
  color: #16181b;
  border-top: 1px solid var(--c-line);
  overflow: auto;
}

@media (min-width: 768px) {
  .console-catalogue {
    /* A slot, not a tail. Capped so the decision column keeps the room it
       needs, and scrolled internally so a 47-lot sale still fits. */
    flex: 0 0 auto;
    max-height: 34vh;
  }
}

/* The header stays put while the rows scroll under it — a money column with no
   heading above it is a column of unlabelled numbers. */
.console-catalogue thead th {
  position: sticky;
  top: 0;
  z-index: 1;
}

.console-catalogue table {
  width: 100%;
  /* Eight columns. Below this the cells start truncating the very figures the
     table exists to compare, so it scrolls sideways in its own container
     instead — the page body never does. */
  min-width: 880px;
  border-collapse: collapse;
  font-size: 13px;
}

.console-catalogue th {
  text-align: left;
  padding: 7px 14px;
  background: #fafafb;
  border-bottom: 1px solid #e8e8e8;
  font-size: 10px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: #767c84;
  font-weight: 600;
  white-space: nowrap;
}

.console-catalogue td {
  /* Every row this table gives back is a row the decision column keeps. On a
     laptop the difference between 10px and 8px here is the difference between
     the middle column fitting and growing a scrollbar. */
  padding: 8px 14px;
  border-bottom: 1px solid #f1f2f4;
  white-space: nowrap;
}

/* ⚠️ `.console-catalogue th` (0,1,1) OUTRANKS Tailwind's `.text-right` (0,1,0),
   so every money heading rendered left-aligned above a right-aligned figure —
   "Current bid" sat a column-width away from the price it named, and the
   numbers read as belonging to the heading to their right. Restating the
   alignment here is what makes the utility on those `th`s take effect; do not
   rely on the class alone inside this block. */
.console-catalogue th.text-right,
.console-catalogue td.text-right {
  text-align: right;
}

/* ── Where the spare width goes ───────────────────────────────────────────
   An eight-column table on a 1900px screen has several hundred pixels nobody
   asked for. Left to itself the browser shares that out across EVERY column in
   proportion, which is the worst possible answer: the lot name ends up half a
   screen from its own model, and two prices a row apart no longer line up
   close enough to compare — the one thing this table exists to let you do.

   So every column is sized to its content, and ONE column is told to swallow
   the remainder. Model is the one, because it sits on the seam between the two
   halves of the row: what the lot IS on the left, what it COSTS on the right.
   The empty space lands between those two groups, where it reads as a divider
   rather than as a hole punched through a column. */
.console-catalogue th.console-col--fit,
.console-catalogue td.console-col--fit {
  width: 1%;
}

.console-catalogue th.console-col--flex,
.console-catalogue td.console-col--flex {
  width: 100%;
}

/* The seam made visible. Without it the gap above just reads as the table
   having run out of content; a hairline says the figures are a group. */
.console-catalogue th.console-col--group-start,
.console-catalogue td.console-col--group-start {
  border-left: 1px solid #f1f2f4;
}

.console-catalogue th.console-col--group-start {
  border-left-color: #e8e8e8;
}

.console-catalogue tr:last-child td {
  border-bottom: 0;
}

@keyframes console-lamp {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.3; }
}

.console-lamp {
  animation: console-lamp 1.4s ease-in-out infinite;
}

/* ── The quote block: ask price / high bid / high bidder ──────────────────
   Label left, value right, hairline between — the shape a spec sheet already
   has, so the eye reads down the values without being taught the layout. */

.console-quote {
  margin: 10px 0 0;
}

.console-quote__row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  padding: 6px 0;
  border-bottom: 1px solid #ececec;
}

.console-quote__row:last-child {
  border-bottom: 0;
}

.console-quote__label {
  font-family: 'Segoe UI Regular', system-ui, sans-serif;
  font-size: 11px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: #767c84;
  white-space: nowrap;
}

.console-quote__value {
  font-family: 'Segoe UI Bold', system-ui, sans-serif;
  font-size: 17px;
  font-variant-numeric: tabular-nums;
  color: #16181b;
  text-align: right;
}

/* The one number on this screen that moves by itself. */
.console-quote__hero {
  font-family: 'Impact Regular', Impact, system-ui, sans-serif;
  font-size: clamp(26px, 4vw, 34px);
  line-height: 1;
  font-variant-numeric: tabular-nums;
  color: #16181b;
  text-align: right;
}

.console-quote__value--you {
  color: #15803d;
}

/* ── Status band ──────────────────────────────────────────────────────────
   One at a time, directly above the control it is talking about. */

.console-band {
  display: flex;
  align-items: center;
  gap: 10px;
  /* Bottom margin too, not just top: the band sits directly above the bid
     control, and with them touching the two read as one stacked block — the
     status looked like a label ON the button rather than a separate thing
     saying what just happened. */
  margin: 10px 0;
  padding: 9px 12px;
  border-radius: 6px;
  font-family: 'Segoe UI Semibold', system-ui, sans-serif;
  font-size: 13px;
  line-height: 1.3;
}

/* Yellow, not green: a submitted bid is a RECEIPT, not a win. Green would say
   the lot is yours, which the next bid can undo a second later. */
.console-band--submitted {
  background: #ffd54a;
  color: #16181b;
  box-shadow: inset 0 -3px 0 #f0a500;
}

.console-band--outbid {
  background: #c0392b;
  color: #ffffff;
}

.console-band--winning {
  background: #e7f5ec;
  color: #15803d;
}

/* The one outcome worth a solid colour. A lot won is the end of the whole
   errand, not another status update. */
.console-band--won {
  background: #15803d;
  color: #ffffff;
}

.console-band--closed {
  background: #f2f4f7;
  color: #4b5058;
}

.console-band__check {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 22px;
  height: 22px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.22);
  font-size: 13px;
}

.console-band--error {
  background: #fff5f5;
  color: #d70501;
  border: 1px solid #ffd6d6;
}

.console-band__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 22px;
  height: 22px;
  border-radius: 999px;
  border: 2px solid currentColor;
  font-family: 'Segoe UI Bold', system-ui, sans-serif;
  font-size: 13px;
}

/* ── A price that just moved ──────────────────────────────────────────────
   The rail and the lot table are live, but a number changing silently while
   the reader is looking somewhere else is indistinguishable from a number that
   never changed. The flash is what makes "live" legible.

   Green, not red: on this page red is the sale's own colour — the current lot,
   the bid button, the closing warning. Another red would read as an alert. */

@keyframes console-live-flash {
  0%   { background-color: rgba(47, 208, 122, 0.26); }
  100% { background-color: transparent; }
}

.console-rail__lot.is-live {
  animation: console-live-flash 1.4s ease-out;
}

/* The row, not the cell: the price cell alone is 90px of a 1400px table and
   the flash would be missed at a glance, which defeats the point of it. */
.console-catalogue tr.console-row--live td {
  animation: console-live-flash 1.4s ease-out;
}

@media (prefers-reduced-motion: reduce) {
  .console-lamp {
    animation: none;
  }

  .console-bid {
    transition: none;
  }

  .console-rail__lot.is-live,
  .console-catalogue tr.console-row--live td {
    animation: none;
  }
}
