/* ─────────────────────────────────────────────────────────────────────────────
 * 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 (the menu scrolls itself, invisibly). */
.site-header {
  position: sticky;
  top: 0;
  z-index: 50;
}

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

/* ── Dashboard: the CONTENT rides the page, the MENU scrolls itself ──────────
   The page is still the only VISIBLE scroller — the content pane has no bar of
   its own, and the footer stays on the window's. What changed (2026-09-08) is
   the sidebar menu: it is now its own scroll container.

   ⚠️ THIS REVERSES THE 2026-08-12 CALL, and the reason that call was made still
   has to hold. Back then the sidebar had a VISIBLE bar, so a second one sat
   beside the window's and the wheel felt arbitrary. The menu now scrolls with
   NO bar drawn at all — so the screen still shows exactly one scrollbar, and
   the menu no longer has to ride the page to reach its last item.

   Two rules make it behave the way a pointer expects:

   • The scroller is `.site-sidebar-inner`, the box that is already pinned under
     the header — NOT the `<aside>`. The aside is stretched by the flex row to
     the full height of the page, so scrolling it would mean scrolling a column
     that is already as tall as its content.
   • `overscroll-behavior: contain` — the wheel moves the menu while the pointer
     is over it and STOPS at the menu's own end instead of handing the rest to
     the page. A menu shorter than the viewport is not a scroll container at
     all, so the page keeps scrolling normally under the cursor there. */
.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;
}

/* Invisible in every engine: Firefox/standards, WebKit/Blink, old Edge. The
   menu still scrolls by wheel, trackpad, touch, and keyboard. */
.site-sidebar,
.site-sidebar-inner {
  scrollbar-width: none;
  -ms-overflow-style: none;
}

.site-sidebar::-webkit-scrollbar,
.site-sidebar-inner::-webkit-scrollbar {
  width: 0;
  height: 0;
}

@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);
    /* Header to viewport bottom, less a hair so the last item is not jammed
       against the edge of the screen. */
    max-height: calc(100dvh - var(--site-header-height, 84px) - 16px);
    overflow-y: auto;
    overscroll-behavior: contain;
  }
}

@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 the drawer itself is the scroller here. Its bar is hidden by the
     shared rule above; /css/style.css's own thin red one is overridden. */
  .site-sidebar {
    position: fixed;
    height: auto;
    max-height: 100dvh;
    overflow-y: auto;
    overscroll-behavior: contain;
  }

  /* …and the inner box goes back to being plain content, or the drawer would
     hold a second scroller inside the one that is already scrolling. */
  .site-sidebar-inner {
    max-height: none;
    overflow: visible;
  }
}

/* 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;
}

/* ⚠️ A SCROLL CONTAINER HAS NO "ONE AXIS ONLY" MODE.
   `.site-modal-body` (style.css) sets `overflow-y: auto` and leaves overflow-x
   at `visible` — and per spec a `visible` axis paired with a scrolling one
   computes to `auto`, so the body scrolls horizontally too whether or not
   anyone asked it to.

   What actually spilled: `.btn:hover` is `translateY(-3px) scale(1.02)`, and
   these modal buttons are `w-full`. Hovering one makes it 2% wider than the
   box it sits in, and a horizontal scrollbar appeared under it — on hover
   only, which is why it read as a hover bug rather than a layout one.

   Declared for EVERY modal body, not just the offer form: the same pair of
   rules meets on every one of them, so fixing the screen it was reported on
   would leave the identical bug in the rest. `.is-bid-scroll` below already
   did this for the bid modal; this is that fix, generalised.

   The 1%-per-side overhang of a hovered button is clipped instead of scrolled.
   That is the intended trade: the lift still reads as hover, and a few pixels
   of a red button's edge is not worth a permanent scrollbar. */
.site-modal-body {
  overflow-x: 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;
}

/* ── Admin-authored LISTING copy ───────────────────────────────────────────
   Product, equipment and auction-lot descriptions, written in the CRM's editor
   and injected here as raw HTML.

   ⚠️ THIS IS NOT `.cms-content`, AND THAT IS THE WHOLE POINT. That class is a
   FLEX COLUMN with `gap: clamp(26px, 2vw, 36px)` — correct for a Terms page
   built from a handful of <h2>+<p> blocks, and ruinous here: a product
   description is thirty short lines, so every one of them got ~30px of dead
   space above it (plus the 12px paragraph margin) and the copy scattered down
   the page. The admin's own Show Product screen renders the identical HTML
   through Tailwind's `prose`, which is ordinary flow — which is why the two
   screens looked like different documents.

   ⚠️ `word-break: normal` UNDOES `body { word-break: break-word }` in
   /css/style.css, which was splitting words mid-syllable ("Designed specifi /
   cally"). `overflow-wrap: break-word` is kept so a long SKU or URL still wraps
   instead of pushing the page sideways. Same trap `.role-switch__label` hit.

   ⚠️ <strong> TAKES THE BOLD FACE BY NAME. `.cms-content p` sets the SemiBold
   family, whose only @font-face is declared at weight 600 — so a <strong>
   asking for 700 resolved to that same 600 file and rendered identical to the
   text around it. Every bold run in an admin's description simply vanished.
   Naming `--font-segoe-ui-bold` (a real 700 file) is what makes it visible. */
.listing-richtext {
  display: block;
  font-family: var(--font-segoe-ui-regular);
  font-size: 16px;
  line-height: 24px;
  color: #68645d;
  word-break: normal;
  overflow-wrap: break-word;
}

.listing-richtext>*:first-child {
  margin-top: 0;
}

.listing-richtext>*:last-child {
  margin-bottom: 0;
}

.listing-richtext p {
  margin: 0 0 10px;
}

/* ⚠️ A TRULY EMPTY `<p></p>` IS AN ARTEFACT, and this catalogue's descriptions
   carry eight of them — they come in with copy pasted out of Word. They hold
   nothing, so they are hidden rather than each given a paragraph's margin.

   A blank line the AUTHOR typed is `<p><br></p>`, which is not `:empty` and is
   left alone: deliberate spacing is not ours to delete. */
.listing-richtext p:empty {
  display: none;
}

.listing-richtext h1,
.listing-richtext h2,
.listing-richtext h3,
.listing-richtext h4,
.listing-richtext h5,
.listing-richtext h6 {
  font-family: var(--font-segoe-ui-bold);
  font-weight: 700;
  color: #1a1a17;
  line-height: 1.35;
  margin: 18px 0 8px;
  text-transform: none;
}

.listing-richtext h1 {
  font-size: 20px;
}

.listing-richtext h2 {
  font-size: 18px;
}

.listing-richtext h3 {
  font-size: 17px;
}

.listing-richtext h4,
.listing-richtext h5,
.listing-richtext h6 {
  font-size: 16px;
}

.listing-richtext strong,
.listing-richtext b {
  font-family: var(--font-segoe-ui-bold);
  font-weight: 700;
  color: #1a1a17;
}

/* The italic FACE, with `font-style: normal` so the browser does not slant an
   already-slanted file on top. */
.listing-richtext em,
.listing-richtext i {
  font-family: var(--font-segoe-ui-italic);
  font-style: normal;
}

.listing-richtext ul,
.listing-richtext ol {
  margin: 0 0 12px;
  padding-left: 22px;
}

.listing-richtext ul {
  list-style: disc;
}

.listing-richtext ol {
  list-style: decimal;
}

.listing-richtext li {
  margin: 0 0 4px;
  list-style: inherit;
}

.listing-richtext a {
  color: #ff1313;
  text-decoration: underline;
}

.listing-richtext img {
  max-width: 100%;
  height: auto;
}

.listing-richtext hr {
  border: 0;
  border-top: 1px solid #e0e3e6;
  margin: 16px 0;
}

.listing-richtext blockquote {
  margin: 0 0 12px;
  padding-left: 14px;
  border-left: 3px solid #e0e3e6;
}

/* ⚠️ Its own scroller. A pasted table wider than the column would otherwise
   scroll the whole PAGE sideways. */
.listing-richtext table {
  display: block;
  width: 100%;
  overflow-x: auto;
  border-collapse: collapse;
  margin: 0 0 12px;
}

.listing-richtext th,
.listing-richtext td {
  border: 1px solid #e0e3e6;
  padding: 6px 10px;
  text-align: left;
}

/* 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 {
  /**
   * ⚠️ THE ROW SIZES ITSELF TO ITS OWN COLUMN, NOT TO THE WINDOW.
   *
   * The breakpoints below used to be viewport media queries, but this row does
   * not live at viewport width: from `xl` up, the Bid tape sits beside it and
   * takes 330px plus the gap, and the dashboard sidebar takes more again. So a
   * 1400px window gave the row roughly 890px while the six-column rule — keyed
   * to 1180px of WINDOW — had already switched on. The controls ran off the
   * right edge, which is exactly what "not responsive" looked like.
   *
   * A container query asks the only question that matters: how wide is the
   * space this row actually has. Same support profile as the `:has()` menus and
   * `dvh` this stylesheet already relies on.
   */
  container-type: inline-size;
  container-name: lot;
  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;
}

/**
 * ⚠️ 690, chosen from measurement rather than roundness.
 *
 * Four columns need 670px of container in their widest state (a paused lot's
 * longer button labels). A 1412px window — the width this was reported at —
 * gives the row 699px once the dashboard sidebar and the bid tape have taken
 * theirs, so a 720 threshold stacked a row that had room for four columns.
 * 690 clears the real requirement with headroom and still catches that case.
 */
@container lot (min-width: 690px) {
  .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. */
/**
 * ⚠️ 1240, NOT 1180 — the old number was 50px short of what this layout needs.
 *
 * Measured against the WIDEST state a row can reach: a paused lot (whose button
 * says "Request resume", a character longer than "Request pause") carrying an
 * admin rejection note. Six columns need 1230px; below that the controls were
 * clipped even at full window width, before the bid tape took its share.
 */
@container lot (min-width: 1240px) {
  .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;
  }

  /**
   * ⚠️ THE CONTROLS ARE PINNED TO THE LAST TRACK, NOT LEFT TO FLOW.
   *
   * "Admin action" only renders for a lot that HAS an outstanding or decided
   * request, so a board holds two kinds of row: six children on some, five on
   * others. With `grid-column: auto` the controls simply took the next free
   * track — the sixth on a row with an admin chip, the FIFTH on a row without
   * one — so View landed in a different column depending on a neighbouring
   * cell's content, and no two rows lined up.
   *
   * `-2 / -1` is the last track whatever the count, so View sits in the same
   * place on every row and the admin-action track is simply left empty when
   * there is nothing to say.
   */
  .board-row__actions {
    grid-column: -2 / -1;
    justify-content: flex-end;
    padding-top: 0;
    border-top: 0;
  }

  .board-row__admin {
    grid-column: 5;
    padding-top: 0;
    /* The track caps it now (210px), so the standalone ceiling would only
       fight it. */
    max-width: none;
  }
}

/* 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;
}

/**
 * ⚠️ The `@media (min-width: 1180px)` blocks that used to sit here are GONE,
 * and that was the second half of the alignment bug.
 *
 * `.board-row` was converted to a container query (see its comment above)
 * precisely because the row does not live at viewport width — the bid tape and
 * the dashboard sidebar take theirs first. These two rules were missed in that
 * conversion and kept asking the WINDOW. So between roughly 1180px of window
 * and 1240px of row, the six-column template had not switched on while the
 * cells had already been told to stop spanning: the controls were placed into a
 * fifth track that did not exist, and the browser invented an implicit column
 * for them. Both rules now live inside the same container query as the template
 * they belong to, so one condition governs the whole layout.
 */

/* 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;
}

/* ── Bid-checks bell ────────────────────────────────────────────────────────
 *
 * Sits on the board's DARK instrument panel beside Reload, so its resting
 * colours are the panel's, not the page's.
 */
.board-bell {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  height: 32px;
  padding: 0 12px;
  border: 1px solid rgba(255, 255, 255, 0.22);
  border-radius: 6px;
  background: rgba(255, 255, 255, 0.06);
  color: #ffffff;
  transition: border-color 160ms ease, background-color 160ms ease;
}

.board-bell:hover {
  border-color: rgba(255, 255, 255, 0.4);
  background: rgba(255, 255, 255, 0.12);
}

/* The count of checks still being reviewed. Red because it is the one number
   here that is about something unresolved — it is not an error, so it never
   gets the alarm treatment beyond the dot itself. */
.board-bell__count {
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  border-radius: 9px;
  background: #ff1313;
  color: #ffffff;
  font-size: 11px;
  line-height: 18px;
  text-align: center;
  font-variant-numeric: tabular-nums;
}

/**
 * A check that lands while they are watching.
 *
 * ⚠️ The ring is drawn on a PSEUDO-ELEMENT, not by animating the button's own
 * border or box-shadow — the button sits in a flex row with Reload, and
 * animating its box would nudge its neighbour on every pulse.
 */
.board-bell[data-arrived="1"]::after {
  content: '';
  position: absolute;
  inset: -3px;
  border-radius: 8px;
  border: 2px solid #ff1313;
  animation: board-bell-pulse 850ms ease-out 3;
  pointer-events: none;
}

@keyframes board-bell-pulse {
  0% { opacity: 1; transform: scale(1); }
  100% { opacity: 0; transform: scale(1.14); }
}

@media (prefers-reduced-motion: reduce) {
  .board-bell[data-arrived="1"]::after {
    animation: none;
    opacity: 1;
  }
}

/* ── Bid history drawer ─────────────────────────────────────────────────────
 *
 * One lot's ledger, over the board. Portalled to <body>, so these rules must
 * not depend on anything in the dashboard's layout — the drawer is not inside
 * it at render time.
 */
.board-drawer {
  position: fixed;
  inset: 0;
  z-index: 70;
  display: flex;
  justify-content: flex-end;
}

.board-drawer__scrim {
  position: absolute;
  inset: 0;
  border: 0;
  padding: 0;
  background: rgba(14, 15, 17, 0.42);
  cursor: pointer;
  animation: board-drawer-fade 160ms ease;
}

.board-drawer__panel {
  position: relative;
  display: flex;
  flex-direction: column;
  width: min(420px, 100%);
  /**
   * ⚠️ `100dvh`, not `100vh`. On mobile Safari the collapsing address bar makes
   * `vh` taller than the visible viewport, which would put the drawer's own
   * pagination footer underneath the browser chrome — on the one control that
   * moves you through the pages.
   */
  height: 100dvh;
  background: #ffffff;
  box-shadow: -18px 0 40px rgba(14, 15, 17, 0.18);
  animation: board-drawer-in 200ms cubic-bezier(0.22, 0.61, 0.36, 1);
}

/* The list is the ONLY part that scrolls: the header naming the lot and the
   footer paging through it both have to stay put, or a long ledger scrolls its
   own controls away. */
.board-drawer__scroll {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
}

@keyframes board-drawer-fade {
  from { opacity: 0; }
  to { opacity: 1; }
}

@keyframes board-drawer-in {
  from { transform: translateX(16px); opacity: 0; }
  to { transform: translateX(0); opacity: 1; }
}

/* Someone who has asked for less motion still gets the panel, just not thrown
   at them. */
@media (prefers-reduced-motion: reduce) {
  .board-drawer__scrim,
  .board-drawer__panel {
    animation: none;
  }
}



/* 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 — residual CSS only
 *
 * Layout / colours / typography live as Tailwind on BiddingConsolePage +
 * BidHistoryPanel. What remains here is what utilities cannot express cleanly:
 * keyframe flashes, and the vertical rail's timeline bead/connector
 * (::before + first/last/only-child), which only paint from 1280px up.
 * ───────────────────────────────────────────────────────────────────────────── */

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

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

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

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

/* Timeline bead + interval tick: hidden on the horizontal strip. */
.console-rail-bead,
.console-rail-tick {
  display: none;
}

/* Horizontal closing-order strip: 4px-tall scrollbar only. */
.console-rail-list {
  scrollbar-width: thin;
  scrollbar-color: #3a3f46 transparent;
}

.console-rail-list::-webkit-scrollbar {
  height: 4px;
}

.console-rail-list::-webkit-scrollbar-track {
  background: transparent;
}

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

@media (min-width: 1280px) {
  .console-rail-list::-webkit-scrollbar {
    width: 6px;
    height: 6px;
  }

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

  /* Timeline track — brighter/thicker so it reads on the dark rail. */
  .console-rail-list > .console-rail-lot::before,
  .console-rail-tick::before {
    content: '';
    position: absolute;
    left: 16px;
    top: 0;
    bottom: 0;
    width: 2px;
    background: #6b737c;
  }

  .console-rail-list > .console-rail-lot:first-child::before {
    top: 24px;
  }

  .console-rail-list > .console-rail-lot:last-child::before {
    bottom: auto;
    height: 24px;
  }

  .console-rail-list > .console-rail-lot:only-child::before {
    display: none;
  }

  .console-rail-bead {
    display: block;
    position: absolute;
    left: 11px;
    top: 18px;
    width: 12px;
    height: 12px;
    border-radius: 999px;
    background: #16181b;
    border: 2px solid #a8b0b8;
    box-sizing: border-box;
  }

  .console-rail-bead.is-current {
    background: #ff1313;
    border-color: #ff1313;
    box-shadow: 0 0 0 4px rgba(255, 19, 19, 0.28);
  }

  .console-rail-bead.is-settled {
    background: #5c646e;
    border-color: #8b929b;
  }

  .console-rail-tick {
    display: block;
    position: relative;
    padding: 3px 0 3px 34px;
    font-family: 'Segoe UI Regular', system-ui, sans-serif;
    font-size: 9px;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: #9aa0a8;
  }
}

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

/* ── Sold Items: a row that needs somebody now ─────────────────────────────── */
/**
 * ⚠️ THE STRIPE IS AN INSET SHADOW ON THE FIRST CELL, NOT A BORDER ON THE ROW.
 *
 * Every `td` in this table paints its own background (the zebra striping), so a
 * `border-left` or a background set on the `<tr>` is painted over and never
 * shows — the same trap the admin enquiry queues hit (CLAUDE.md §9am). The tint
 * therefore goes on the cells and the red edge is drawn inside the first one.
 *
 * Reserved for OVERSOLD rows only: the listing could not cover the line when the
 * card was charged, so a unit is owed that was never on the shelf. A listing
 * that simply reached zero is not tinted — on used equipment that is the normal
 * end of one machine's life, and colouring it would put a warning on almost
 * every completed sale (§5.8: colour is ranked, not sprinkled).
 */
.sold-row--urgent > td {
  background: #fff6f5;
}

.sold-row--urgent > td:first-child {
  box-shadow: inset 3px 0 0 0 #d9251d;
}

/* ── A button that cannot be pressed must not look like one ────────────────── */
/**
 * ⚠️ `.btn` HAD NO DISABLED STATE AT ALL.
 *
 * `style.css` gives every `.btn` a hover lift, an active press and a sweeping
 * highlight, and none of it is gated on `:disabled` — so a disabled
 * `.btn.btn-primary` rendered as a full-strength red call to action that rose
 * to meet the cursor. Out of Stock looked like the most clickable thing on the
 * product page.
 *
 * Fixed here rather than at each call site, because every disabled `.btn` in
 * the app had the same problem.
 *
 * `[aria-disabled="true"]` is matched too: a disabled anchor is not a thing, so
 * link-shaped controls say it that way.
 */
.btn:disabled,
.btn[aria-disabled="true"] {
  cursor: not-allowed;
  /* The lift and the press are what read as "this responds to you". */
  transform: none;
}

.btn:disabled:hover,
.btn[aria-disabled="true"]:hover,
.btn:disabled:active,
.btn[aria-disabled="true"]:active {
  transform: none;
}

/* The sweep is the other half of the illusion — it runs on hover regardless of
   state, so it is pinned out of frame rather than left to travel. */
.btn:disabled:hover::before,
.btn[aria-disabled="true"]:hover::before {
  transform: translateX(-130%) skewX(-18deg);
}

/**
 * OUT OF STOCK — its own quiet colour, not a dimmed primary.
 *
 * A greyed-out red still reads as "the buy button, temporarily broken". This is
 * not a broken button: the item is genuinely unavailable, and that is a state of
 * the LISTING rather than a failure of the control. So it leaves the primary
 * palette entirely for a neutral surface, and the sentence underneath says when
 * to come back (§9at).
 */
.btn-unavailable {
  color: #6b7280;
  background: #f2f3f5;
  border: 1px solid #e0e3e6;
  cursor: not-allowed;
  transform: none;
}

.btn-unavailable:hover,
.btn-unavailable:active {
  color: #6b7280;
  background: #f2f3f5;
  transform: none;
}

/* ═══════════════════════════════════════════════════════════════════════════
   SKELETONS — the shape of the page, while the page is on its way (§9av)

   Every route is code-split, so a navigation now has two waits stacked back to
   back: fetch the chunk, then fetch its data. Left alone that reads as a blank
   screen followed by a jump. These stand in for the real layout through both.

   ⚠️ THE 140ms DELAY IS THE WHOLE POINT, AND IT IS CSS, NOT JS.
   A warm chunk resolves in well under that, so `animation-delay` + `both` holds
   the skeleton fully transparent and it is never seen — the page simply
   appears, exactly as it does today. Only a wait long enough to notice draws
   anything.

   It has to be CSS because a Suspense fallback cannot be held: React unmounts
   it the instant the chunk lands, so a JS minimum-visible timer (the trick
   useDeferredLoading uses for the overlay) has nothing left to run on. Opacity
   that has not started yet cannot flash.
   ═══════════════════════════════════════════════════════════════════════════ */

.skeleton-page {
  animation: skeleton-appear 240ms ease both;
  animation-delay: 140ms;
}

@keyframes skeleton-appear {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* One bar. Everything else is these, sized. */
.skeleton {
  position: relative;
  overflow: hidden;
  background: #ececef;
  border-radius: 6px;
  /* Nothing inside a skeleton is text, but callers pass real strings for
     sizing; this keeps them invisible without collapsing the box. */
  color: transparent;
  user-select: none;
}

.skeleton::after {
  content: '';
  position: absolute;
  inset: 0;
  transform: translateX(-100%);
  background-image: linear-gradient(
    90deg,
    rgba(255, 255, 255, 0) 0%,
    rgba(255, 255, 255, 0.55) 50%,
    rgba(255, 255, 255, 0) 100%
  );
  animation: skeleton-shimmer 1.4s ease-in-out infinite;
}

@keyframes skeleton-shimmer {
  100% { transform: translateX(100%); }
}

.skeleton--circle { border-radius: 999px; }
.skeleton--pill   { border-radius: 999px; }

/* A photo well is darker than a text line: at the same grey a big block reads
   as a rendering fault rather than as a picture that has not arrived. */
.skeleton--media {
  background: #e2e3e7;
  border-radius: 10px;
}

/* Text lines never all end in the same place — a paragraph of equal-length
   bars looks like a table, which is the wrong promise about what is coming. */
.skeleton-line + .skeleton-line { margin-top: 8px; }
.skeleton-line--short { width: 62%; }
.skeleton-line--mid   { width: 80%; }

/* The card shell around a skeleton keeps the grid's real geometry, so the row
   does not reflow when the actual cards land. */
.skeleton-card {
  border: 1px solid #eceef0;
  border-radius: 12px;
  background: #fff;
  overflow: hidden;
}

/* ⚠️ MOTION OFF MEANS THE SWEEP STOPS, NOT THAT THE SKELETON GOES.
   The grey shapes are information — they say where content will be. Removing
   them would leave a blank screen for the very users least well served by one.
   Only the travelling highlight is animation, so only it is dropped. */
@media (prefers-reduced-motion: reduce) {
  .skeleton::after { animation: none; opacity: 0; }
  .skeleton-page { animation: none; }
}

/* ─────────────────────────────────────────────────────────────────────────
   Home hero figure

   ⚠️ CORRECTED — this used to probe every hero image (§9aw) and pick
   between a floating cut-out and a framed/mounted rectangle, depending on
   whether the artwork carried transparency. That meant the SAME uploaded
   banner could render with or without the white-bordered frame and red
   corner ticks depending on how fast the probe resolved relative to its
   1.2s timeout — reported directly as "sometimes shows with the square and
   sometimes it doesn't". The framed treatment was also disliked on its own
   terms ("the square doesn't look good").

   The probe-and-frame logic is still gone for good — `useImageBackdrop` /
   `classifyBackdrop` stay deleted, and there is still exactly ONE code path
   for every hero image, with no branch on what was uploaded or how fast it
   loaded.

   ⚠️ SECOND CORRECTION, SAME DAY — "one treatment, always float uncropped"
   is not the same thing as "never show a square". The design's own asset
   (`banner-img.png`) is a true cut-out with alpha-0 corners, so it floats
   cleanly with nothing to hide. An admin-uploaded hero photo is routinely
   an ordinary product shot with an OPAQUE rectangular (often near-white)
   background — `object-fit: contain` draws that background faithfully, and
   `drop-shadow` then outlines its rectangle with a shadow, which is exactly
   a "square with a shadow". That is what was reported a second time, on a
   real uploaded photo, after the probe/frame removal had already shipped.

   ⚠️ A CSS-ONLY FIX WAS TRIED HERE AND REVERTED, SAME DAY, FOR A REAL REASON.
   A radial `mask-image` fading every image's edge to transparent was briefly
   shipped on this rule — it does soften a hard square, but it has no idea
   where the IMAGE'S OWN CONTENT sits inside the box, only where the box's
   edge is. A composed banner graphic that intentionally reaches close to its
   own edges (the "Explore Auction" hero — small thumbnail cards near the
   top, the rig's tracks near the bottom) got its real content faded out by
   the same rule meant to hide a stranger's studio backdrop: the thumbnails
   and the tracks visibly disappeared, which is a worse bug than the square it
   was trying to fix. A mask keyed to the ELEMENT'S BOX can never tell "this
   pixel near the edge is background" from "this pixel near the edge is the
   thing the admin composed on purpose" — only looking at the PIXELS
   themselves can, which is what `useAutoCutoutBackground`
   (`lib/autoCutoutBackground.ts`) does instead: it samples the image's own
   border ring and only removes it when that ring reads as a flat, uniform
   backdrop, declining outright on anything it is not confident is background
   (including every genuine cut-out, whose border is already transparent, and
   this exact auction banner, whose content runs to the edge). That is why
   this rule carries no mask any more — the backdrop removal lives entirely
   in the hook, is pixel-accurate, and cannot crop real content the way a
   box-relative CSS fade did.

   ⚠️ THE GEOMETRY IS FIXED, unchanged from before: the slide's height must
   not depend on what was uploaded, or every slide in the carousel settles at
   a different height.
   ───────────────────────────────────────────────────────────────────────── */

.hero-figure {
  position: relative;
  box-sizing: border-box;
  width: 100%;
  max-width: 420px;
  height: 280px;
  margin: 0;
  /* Held back until the image has actually loaded, so a broken/slow URL does
     not flash an empty box — the box is already reserved, so this costs a
     fade and never a reflow. */
  opacity: 0;
  transition: opacity 320ms ease;
}

.hero-figure.is-ready { opacity: 1; }

/* ⚠️ `contain`, never `cover`. The box is fixed, so `cover` would slice the
   artwork to fill it — which is exactly what the old markup did, on top of
   hanging it on a translucent grey pane (`bg-black/20`) that showed through
   everywhere the artwork was transparent. The shadow is a `drop-shadow`
   FILTER rather than a box-shadow, so it follows the silhouette (real alpha,
   from the image or from `useAutoCutoutBackground`) rather than drawing a
   rectangle around it.

   ⚠️ NO `mask-image` HERE, DELIBERATELY — see the comment block above this
   rule. A box-relative CSS fade cannot tell real edge content from a flat
   backdrop; only reading the actual pixels can, which is what the JS hook on
   `HeroFigure` does before this ever paints. */
.hero-figure__img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
  object-position: center;
  filter: drop-shadow(0 20px 30px rgba(0, 0, 0, 0.45));
}

@media (min-width: 640px) { .hero-figure { height: 360px; } }
@media (min-width: 1024px) { .hero-figure { max-width: 500px; height: 440px; } }
@media (min-width: 1280px) { .hero-figure { max-width: 560px; height: 480px; } }

/* The fade is the only motion here, and it is there to hide a swap rather
   than to decorate. With motion off the figure simply appears. */
@media (prefers-reduced-motion: reduce) {
  .hero-figure { transition: none; }
}

/* =========================================================================
   Vendor product description editor (§9bq)
   -------------------------------------------------------------------------
   Quill's snow theme (/vendor/quill.snow.css) draws two separate grey-bordered
   boxes — toolbar and editor — in Helvetica. On this form every other field is
   one soft-bordered box in the site's own face, so the two are merged into ONE
   box here, the toolbar sits on a light strip, and the active tool takes the
   site's red instead of Quill's blue.

   ⚠️ Scoped to `.vendor-rich-editor`, never `.ql-*` alone: the snow sheet is
   linked globally, and a bare `.ql-toolbar` rule would restyle any future
   Quill instance on the site that wants its own look.
   ========================================================================= */
.vendor-rich-editor {
  display: flex;
  flex-direction: column;
  border: 1px solid #e8e8e8;
  border-radius: 8px;
  overflow: hidden;
  background: #fff;
}

.vendor-rich-editor:focus-within {
  border-color: #c9c4ba;
}

.vendor-rich-editor.is-invalid {
  border-color: #d9251d;
}

.vendor-rich-editor .ql-toolbar.ql-snow {
  border: 0;
  border-bottom: 1px solid #efece6;
  background: #fafaf8;
  padding: 8px 10px;
  font-family: var(--font-segoe-ui-regular, Arial, sans-serif);
}

.vendor-rich-editor .ql-container.ql-snow {
  border: 0;
  font-family: var(--font-segoe-ui-regular, Arial, sans-serif);
  font-size: 14px;
}

.vendor-rich-editor .ql-editor {
  min-height: 20rem;
  max-height: 36rem;
  overflow-y: auto;
  padding: 15px;
  color: #171717;
  line-height: 1.6;
  /* Words wrap at spaces; only a genuinely long token (a SKU, a URL) breaks
     mid-word. `body { word-break: break-word }` in style.css would otherwise
     chop ordinary words, the same fault §9bf fixed on the listing page. */
  word-break: normal;
  overflow-wrap: break-word;
}

.vendor-rich-editor .ql-editor.ql-blank::before {
  color: #8a857c;
  font-style: normal;
  left: 15px;
  right: 15px;
}

.vendor-rich-editor .ql-snow .ql-stroke {
  stroke: #3d3a35;
}

.vendor-rich-editor .ql-snow .ql-fill {
  fill: #3d3a35;
}

.vendor-rich-editor .ql-snow .ql-picker {
  color: #3d3a35;
}

.vendor-rich-editor .ql-snow button:hover .ql-stroke,
.vendor-rich-editor .ql-snow button.ql-active .ql-stroke,
.vendor-rich-editor .ql-snow .ql-picker-label:hover .ql-stroke,
.vendor-rich-editor .ql-snow .ql-picker-label.ql-active .ql-stroke {
  stroke: #d9251d;
}

.vendor-rich-editor .ql-snow button:hover .ql-fill,
.vendor-rich-editor .ql-snow button.ql-active .ql-fill {
  fill: #d9251d;
}

.vendor-rich-editor .ql-snow .ql-picker-label:hover,
.vendor-rich-editor .ql-snow .ql-picker-label.ql-active,
.vendor-rich-editor .ql-snow .ql-picker-item:hover,
.vendor-rich-editor .ql-snow .ql-picker-item.ql-selected {
  color: #d9251d;
}

.vendor-rich-editor .ql-snow a {
  color: #d9251d;
}

.vendor-rich-editor__count {
  border-top: 1px solid #efece6;
  background: #fafaf8;
  padding: 6px 12px;
  text-align: right;
  font-family: var(--font-segoe-ui-regular, Arial, sans-serif);
  font-size: 12px;
  color: #8a857c;
  font-variant-numeric: tabular-nums;
}

.vendor-rich-editor__count.is-over {
  color: #d9251d;
}

/* A read-only editor has nothing to click in the toolbar. */
.vendor-rich-editor.is-disabled .ql-toolbar {
  display: none;
}

/* ── Listing photo watermark (.listing-watermark) ──────────────────────────
   Drawn over a product / equipment / auction photo in the gallery and in the
   lightbox. Nothing is burned into the uploaded file — see
   components/listings/ImageWatermark.tsx for what that does and does not buy.

   ⚠️ `pointer-events: none` is load-bearing: the frame underneath is clickable
   (a card navigates, a detail photo opens the lightbox) and an overlay across
   `inset: 0` would swallow every one of those clicks. */
.listing-watermark {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  z-index: 2;
}

.listing-watermark img {
  width: 58%;
  max-width: 320px;
  height: auto;
  opacity: 0.32;
  /* ⚠️ TWO drop-shadows, not one — a single white halo (the old rule) reads
     fine on a dark photo and disappears into a light one, which is most of
     this catalogue's product shots. Stacking a soft DARK shadow underneath
     the existing white halo is what makes stock-photo watermarks (the
     reference the client sent) hold up on either background without
     repainting the logo or shipping a second asset. */
  filter:
    drop-shadow(0 2px 5px rgba(0, 0, 0, 0.4))
    drop-shadow(0 0 5px rgba(255, 255, 255, 0.55));
}

.listing-watermark--detail img {
  max-width: 480px;
}

/* ── Storefront "Shop by Category" tiles — one consistent circular frame ────
 *
 * `.vendor-cat-thumb img` (style.css) is `object-fit: contain` sized to 100%
 * of a fixed 146px circle, so how full a tile LOOKS depends entirely on how
 * much empty space happens to be baked into that one picture: a typical
 * catalog product shot (padded well inside its own frame) sits small in the
 * middle, while a photo with content edge-to-edge — or a custom tile image
 * uploaded through §9ch — fills the circle right up to its border. Six tiles
 * in one row then read as six different treatments, not one design.
 *
 * ⚠️ PADDING GOES ON THE CIRCLE, NOT ON `object-fit`. Switching to `cover`
 * would make the inconsistency worse, not better — it would crop the very
 * product photos that currently look fine, to match the minority that fill
 * the frame. Padding the circle instead gives EVERY picture the same visible
 * margin from the edge regardless of how it was framed when it was taken,
 * without cropping a single one of them.
 */
/* .vendor-cat-thumb {
  padding: 20px;
} */

/* ── Vendor storefront hero slider (§9ds) ──────────────────────────────────
 *
 * The hero's banner layer is absolutely positioned at `inset-0`, so the Swiper
 * inside it has no height of its own to inherit. Swiper only stretches its
 * wrapper, never the slides, so without these three rules the slides collapse
 * to their images' intrinsic height and the gradient covers a hero that has
 * gone flat.
 *
 * ⚠️ SCOPED TO `.vendor-hero-swiper`. `.swiper-slide` is a global class from the
 * linked swiper bundle — an unscoped height rule here would stretch the slides
 * of the category rail, the product galleries and the home carousel too.
 */
.vendor-hero-swiper,
.vendor-hero-swiper .swiper-wrapper,
.vendor-hero-swiper .swiper-slide {
    height: 100%;
}

.vendor-hero-swiper .swiper-slide img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: center;
}

/*
 * The copy block is remounted on every slide change (React `key`), which
 * replays this animation — the heading and subheading fade in as the picture
 * behind them slides, instead of snapping to the next slide's words while the
 * image is still moving.
 *
 * ⚠️ Motion off means NO fade, not a hidden heading: `prefers-reduced-motion`
 * drops the animation entirely, and because the block's resting state is fully
 * opaque it simply appears. An animation that starts at `opacity: 0` and is
 * then disabled would leave the hero's own title invisible.
 */
.vendor-hero-copy {
    animation: vendor-hero-copy-in 600ms cubic-bezier(0.22, 1, 0.36, 1) both;
}

@keyframes vendor-hero-copy-in {
    from {
        opacity: 0;
        transform: translateY(12px);
    }

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

@media (prefers-reduced-motion: reduce) {
    .vendor-hero-copy {
        animation: none;
    }
}

/* ── Storefront builder: the hero slider's slide cards ─────────────────────
 *
 * The hero is the one section whose fields repeat per image, so it is also the
 * one that stops reading as a form and starts reading as a wall. Each slide is
 * therefore a card that states its own number and its own headline, and the
 * shared copy below it is a visibly different block — a vendor has to be able
 * to tell "this text belongs to slide 2" from "this text is the default" at a
 * glance, without reading a word of help text.
 */
.sf-slides {
    display: flex;
    flex-direction: column;
    gap: 15px;
    padding: 18px;
    border: 1px solid #e8e8e8;
    border-radius: 12px;
    background: #fbfbfc;
}

.sf-slides__head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 12px;
}

.sf-slides__count {
    flex-shrink: 0;
    padding: 4px 12px;
    border-radius: 999px;
    background: #ffffff;
    border: 1px solid #e8e8e8;
    font-family: "Segoe UI Semibold", sans-serif;
    font-size: 13px;
    line-height: 20px;
    color: #68645d;
}

.sf-slides__empty {
    margin: 0;
    padding: 18px;
    border: 1px dashed #dcdfe3;
    border-radius: 10px;
    background: #ffffff;
    font-family: "Segoe UI", sans-serif;
    font-size: 14px;
    line-height: 21px;
    color: #68645d;
    text-align: center;
}

.sf-slide {
    display: flex;
    flex-direction: column;
    border: 1px solid #e4e6e9;
    border-radius: 10px;
    background: #ffffff;
    overflow: hidden;
}

/* The red edge is what ties a card to the slider it belongs to — the same
 * #FF1313 the hero's own accents use on the live page. */
.sf-slide__head {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 12px 14px;
    background: #f7f8fa;
    border-bottom: 1px solid #e4e6e9;
    border-left: 3px solid #ff1313;
}

.sf-slide__badge {
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 26px;
    height: 26px;
    border-radius: 999px;
    background: #ff1313;
    color: #ffffff;
    font-family: "Segoe UI Semibold", sans-serif;
    font-size: 13px;
    line-height: 1;
}

.sf-slide__tools {
    margin-left: auto;
    display: flex;
    align-items: center;
    gap: 6px;
    flex-shrink: 0;
}

.sf-slide__tool {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    border-radius: 8px;
    border: 1px solid #e0e3e6;
    background: #ffffff;
    color: #68645d;
    cursor: pointer;
    transition: color 0.2s ease, border-color 0.2s ease, background-color 0.2s ease;
}

.sf-slide__tool:hover:not(:disabled) {
    color: #1a1c1c;
    border-color: #c9ced3;
}

.sf-slide__tool--danger:hover:not(:disabled) {
    color: #ffffff;
    background: #ff1313;
    border-color: #ff1313;
}

/* ⚠️ Disabled, not hidden: the first slide has no "move up" and the last has no
 * "move down", and a button that vanishes as a row moves is harder to aim at
 * than one that greys out in place. */
.sf-slide__tool:disabled {
    opacity: 0.35;
    cursor: not-allowed;
}

.sf-slide__body {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: 15px;
    padding: 15px;
}

/* The picture sits beside its own copy from the tablet step up — the two
 * belong to one slide, and stacking them at every width makes a five-slide
 * hero scroll for pages. */
@media (min-width: 768px) {
    .sf-slide__body {
        grid-template-columns: minmax(0, 260px) minmax(0, 1fr);
        gap: 20px;
    }
}

/* The vendor-name card that sits above the Hero Section. Same shell as the
 * slider block so the two read as one family, on white because it is identity
 * rather than a list of things. */
.sf-identity {
    display: flex;
    flex-direction: column;
    gap: 15px;
    padding: 18px;
    border: 1px solid #e8e8e8;
    border-radius: 12px;
    background: #ffffff;
}

/* A `.form-control` the ported stylesheet never had to style, because nothing
 * on the original design was ever locked. Used on the Storefront Name field
 * (`.sf-identity`) — read-only rather than blank, so the vendor sees the name
 * that is actually theirs, just cannot type over it here. */
.sf-identity .form-control:disabled {
    background: #f4f4f4;
    color: #68645d;
    cursor: not-allowed;
    border-color: #e8e8e8;
}

/* ── Bidding console (/bid/:id) — theme ────────────────────────────────────
   Every colour on the console reads one of these variables, so the Day / Night
   switch is ONE attribute on the root (`data-theme`). Night is the default.
   The classes below are plain CSS (not Tailwind) because this sheet is a
   <link>, so they exist whether or not a utility was ever used in a .tsx. */
.bc {
  --bc-bg: #0e0f11;
  --bc-panel: #16181b;
  --bc-panel-2: #1c1f24;
  --bc-border: #2a2e34;
  --bc-text: #ffffff;
  --bc-soft: #c4c8ce;
  --bc-muted: #8b9098;
  --bc-chip: #262a30;
  --bc-row: #16181b;
  --bc-row-current: #2a171a;
}

.bc[data-theme='day'] {
  --bc-bg: #eef0f3;
  --bc-panel: #ffffff;
  --bc-panel-2: #f6f7f9;
  --bc-border: #dde1e6;
  --bc-text: #16181b;
  --bc-soft: #3f444b;
  --bc-muted: #6b7178;
  --bc-chip: #eceef1;
  --bc-row: #ffffff;
  --bc-row-current: #fff1f1;
}

.bc-bg { background-color: var(--bc-bg); }
.bc-panel { background-color: var(--bc-panel); }
.bc-panel-2 { background-color: var(--bc-panel-2); }
.bc-chip { background-color: var(--bc-chip); }
.bc-border { border-color: var(--bc-border); }
.bc-text { color: var(--bc-text); }
.bc-soft { color: var(--bc-soft); }
.bc-muted { color: var(--bc-muted); }

/* One lot in the closing order. The lot on screen carries the red edge. */
.bc-row {
  background-color: var(--bc-row);
  transition: background-color 160ms ease, border-color 160ms ease;
}

.bc-row:hover {
  border-color: #ff1313;
}

.bc-row.is-current {
  background-color: var(--bc-row-current);
  border-color: #ff1313;
  box-shadow: inset 3px 0 0 #ff1313;
}

/* The list scrolls inside itself from 1280px; keep its bar thin and on-theme. */
.bc-scroll {
  scrollbar-width: thin;
  scrollbar-color: var(--bc-border) transparent;
}

.bc-scroll::-webkit-scrollbar {
  width: 8px;
}

.bc-scroll::-webkit-scrollbar-thumb {
  background: var(--bc-border);
  border-radius: 8px;
}

/* ── Dashboard tables: the same behaviour as the admin tables ───────────────
   `.site-table` sits in an `overflow-x-auto` box.
   - Drag anywhere on a wide table to scroll it sideways (lib/dragScroll.ts
     adds the two classes below; a plain click is unchanged).
   - `.site-table--pin-last` keeps the LAST column (Action / Payout) in view
     while the rest scrolls, so the control for a row is never off-screen. */
.is-drag-scrollable {
  cursor: grab;
}

.is-drag-scrolling,
.is-drag-scrolling * {
  cursor: grabbing !important;
  user-select: none !important;
}

.site-table--pin-last > thead > tr > th:last-child,
.site-table--pin-last > tbody > tr > td:last-child {
  position: sticky;
  right: 0;
  z-index: 1;
  /* The row paints the stripe, so the pinned cell takes the row's own colour —
     without it the columns sliding underneath would show through. */
  background: inherit;
  box-shadow: -10px 0 10px -10px rgba(0, 0, 0, 0.28);
}

/* An urgent Sold Items row tints its cells itself (see .sold-row--urgent). */
.site-table--pin-last > tbody > tr.sold-row--urgent > td:last-child {
  background: #fff6f5;
}

/* A row with one full-width cell (loading / empty message) is not a pinned column. */
.site-table--pin-last > tbody > tr > td[colspan]:last-child {
  position: static;
  box-shadow: none;
}

/* ── Bidding console: closing-order rows (§9em follow-up, 2026-10-21) ──────
 *
 * ⚠️ A GRID CHOSEN BY THE LIST'S WIDTH, not flex-wrap. Each row used to wrap
 * at its own point (a long title, a "+N min" tick, the current row's red
 * border), so neighbouring rows never lined up. The container query reads the
 * LIST, so every row in it takes the same one of three layouts.
 *
 *   < 560px   id / (time | bids) / actions, stacked
 *   560-899   id on top, then time | bids | actions on one line
 *   >= 900    id | time | bids | actions on one line
 */
.console-rail {
  container-type: inline-size;
  container-name: rail;
}

.console-rail-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  grid-template-areas:
    'id id'
    'time bids'
    'act act';
  align-items: center;
  gap: 12px 16px;
}

.console-rail-row__id { grid-area: id; }
.console-rail-row__time { grid-area: time; }
.console-rail-row__bids { grid-area: bids; }
.console-rail-row__act { grid-area: act; justify-content: flex-end; }
.console-rail-row__cta { flex: 1 1 auto; }

@container rail (min-width: 560px) {
  .console-rail-row {
    grid-template-columns: minmax(0, 1fr) 190px auto;
    grid-template-areas:
      'id id id'
      'time bids act';
  }
  .console-rail-row__cta { flex: 0 0 168px; }
}

@container rail (min-width: 900px) {
  .console-rail-row {
    grid-template-columns: minmax(0, 1fr) 150px 190px auto;
    grid-template-areas: 'id time bids act';
  }
}

/* The strip's facts lane scrolls sideways without a visible bar. */
.console-strip-facts {
  scrollbar-width: none;
}
.console-strip-facts::-webkit-scrollbar {
  display: none;
}

/*
 * The lot photo on a short landscape screen (a phone turned sideways). At
 * 16:9 and full column width it was taller than the whole viewport, pushing
 * the bid controls off the screen.
 */
@media (orientation: landscape) and (max-height: 640px) {
  /*
   * ⚠️ Small enough that the section holding the sticky "Your bid amount"
   * bar STARTS inside the viewport. A sticky element cannot rise above its own
   * container's top, so with a taller photo the bid bar never appeared at all
   * until the reader scrolled past the whole picture.
   */
  .console-lot-photo {
    max-height: 40dvh;
  }
  /* The photo's own arrows still page through the gallery. Two classes deep so
     it outranks Tailwind's `flex`, whose sheet is injected after this file. */
  .bc .console-lot-thumbs {
    display: none;
  }
}

/* ── Mobile nav drawer on a SHORT screen (a phone turned sideways) ──────────
 *
 * The drawer pins its logo row, search, Cart and account block and lets only
 * the link list scroll. At 390px of height those fixed parts used up the whole
 * drawer, leaving the links a scroll box of zero height — the menu looked
 * empty. On a short screen the WHOLE drawer scrolls instead, with tighter
 * spacing, so every link is reachable.
 *
 * `nav.mobile-drawer` (two selectors deep) so these outrank Tailwind's
 * utilities, whose sheet is injected after this file.
 */
@media (max-height: 560px) {
  nav.mobile-drawer .mobile-drawer-body {
    overflow-y: auto;
    overscroll-behavior: contain;
    padding-top: 14px;
    padding-bottom: 18px;
  }
  nav.mobile-drawer .mobile-drawer-top {
    margin-bottom: 12px;
    padding-bottom: 10px;
  }
  nav.mobile-drawer .mobile-drawer-top img {
    height: 36px;
  }
  nav.mobile-drawer .mobile-drawer-links {
    flex: 0 0 auto;
    min-height: auto;
    overflow: visible;
    margin-top: 6px;
  }
  nav.mobile-drawer .mobile-drawer-foot {
    margin-top: 14px;
    padding-top: 14px;
  }
  /*
   * The account menu opens DOWNWARD here. Opening upward inside a scrolling
   * drawer would put its top rows above the scroll origin, where they can
   * never be scrolled into view.
   */
  nav.mobile-drawer .mobile-drawer-account-panel {
    position: static;
    padding-bottom: 0;
    padding-top: 8px;
  }
}

/* ── Place Bid popup on a SHORT screen (a phone turned sideways) ────────────
 *
 * At ~375-430px of height the big title, the item box and the 48px footer
 * buttons left the scrolling middle about one line tall — the bid section
 * could barely be read. Everything fixed is made smaller; the middle keeps
 * scrolling as before. Selectors are two-plus classes deep so they outrank the
 * Tailwind utilities on the same elements, whose sheet loads after this file.
 */
@media (orientation: landscape) and (max-height: 640px) {
  .site-modal:has(.is-bid-scroll) {
    padding: 8px 12px;
  }
  .site-modal-panel.is-bid-scroll {
    max-height: calc(100dvh - 16px) !important;
    padding: 12px 16px;
    gap: 8px;
    border-radius: 12px;
  }
  .site-modal-panel.is-bid-scroll .site-modal-title {
    font-size: 20px;
    padding-right: 36px;
  }
  .site-modal-panel.is-bid-scroll .site-modal-close {
    top: 8px;
    right: 8px;
  }
  .site-modal-panel.is-bid-scroll .site-modal-body {
    gap: 12px;
  }
  .site-modal-panel.is-bid-scroll .bid-modal-item {
    padding: 8px 10px;
    gap: 10px;
    margin-bottom: 0;
  }
  .site-modal-panel.is-bid-scroll .bid-modal-item img {
    width: 56px;
    height: 40px;
  }
  .site-modal-panel.is-bid-scroll .bid-modal-foot {
    padding-top: 0;
    gap: 10px;
  }
  .site-modal-panel.is-bid-scroll .bid-modal-foot .btn {
    height: 40px !important;
    min-height: 40px !important;
  }
}

/* ── Auction detail page on a phone turned sideways ─────────────────────────
 *
 * Single column below xl, so at ~390px of height the 750:420 photo alone was
 * taller than the screen and the bid panel sat a full scroll below it. On a
 * short landscape screen the gallery and the bid panel go side by side — the
 * same two-column layout xl already uses — with the gallery sticky beside it.
 */
@media (orientation: landscape) and (max-height: 640px) and (min-width: 640px) {
  .auction-detail-cols {
    flex-direction: row !important;
    align-items: flex-start;
    gap: 20px !important;
  }
  .auction-detail-cols > .auction-gallery {
    width: 42% !important;
    flex: 0 0 42%;
    gap: 10px !important;
    position: sticky;
    top: calc(var(--site-header-height, 84px) + 8px);
    align-self: flex-start;
  }
  .auction-detail-cols > :not(.auction-gallery) {
    flex: 1 1 0;
    min-width: 0;
  }
}

/* ══════════════════════════════════════════════════════════════════════════════
   Phones — one compact system (mobile audit, 2026-10-21)

   The designer sheets are written at desktop scale: 48px buttons, 51px inputs,
   a 220px photo per card, one card per row, 70px banner padding. On a 390px
   phone that is a screen of padding. This block is the ONE place the phone
   scale is set, and it acts only below 640px — nothing at tablet or desktop
   width reads a rule from it.

   ⚠️ `.form-control` is 16px here, not smaller. iOS Safari zooms the page when a
   focused field is under 16px and does not zoom back out.
   ══════════════════════════════════════════════════════════════════════════════ */
@media (max-width: 639px) {
  .container {
    padding-left: 12px;
    padding-right: 12px;
  }

  /* controls */
  .btn {
    min-height: 42px;
    padding: 9px 16px;
    font-size: 14px;
    gap: 6px;
  }

  .form-control {
    height: 44px;
    padding: 10px 12px;
    font-size: 16px;
  }

  textarea.form-control {
    height: auto;
  }

  .form-label {
    font-size: 13px;
  }

  /* page title bands (the red "Products / Your cart / …" banners) */
  section[class*="other-banner-bg"] {
    padding-top: 24px !important;
    padding-bottom: 24px !important;
  }

  section[class*="other-banner-bg"] h1 {
    font-size: 24px !important;
    line-height: 28px !important;
  }

  section[class*="other-banner-bg"] p {
    font-size: 14px !important;
    line-height: 20px !important;
  }

  /* modals */
  .site-modal {
    padding: 12px 10px;
  }

  .site-modal-panel {
    padding: 20px 14px 16px;
    border-radius: 12px;
    max-height: calc(100dvh - 24px);
  }

  .site-modal-title {
    font-size: 24px;
  }

  /* listing cards: two to a row, tighter inside */
  .grid:has(> .product-card:not(.auction-card)) {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 10px;
  }

  .product-card .p-5 {
    padding: 10px;
  }

  .product-card .gap-5 {
    gap: 10px;
  }

  .product-card .gap-2\.5 {
    gap: 6px;
  }

  .product-card .pb-2\.5 {
    padding-bottom: 2px;
  }

  .product-card h3 {
    font-size: 14px;
    line-height: 18px;
  }

  .product-card p {
    font-size: 12px;
    line-height: 16px;
  }

  .product-card .btn {
    min-height: 36px;
    padding: 6px 10px;
    font-size: 13px;
  }

  .product-card [class*="font-impact-regular"][class*="text-base"] {
    font-size: 15px;
    line-height: 20px;
  }

  /* the HDD # / category / manufacturer chips */
  .product-card .h-7 {
    height: 22px;
    padding-left: 6px;
    padding-right: 6px;
    font-size: 10px;
  }

  /* spec boxes: one column of short rows beats two cramped ones at this width */
  .product-card .grid.grid-cols-2 {
    gap: 4px;
  }

  .product-card .grid.grid-cols-2 > div {
    padding: 4px 6px;
  }

  .product-card .grid.grid-cols-2 > div > span:first-child {
    font-size: 10px;
  }

  .product-card .grid.grid-cols-2 > div > span:last-child {
    font-size: 12px;
    line-height: 15px;
  }
}

/* ── Phones: dashboard pages (same block as above, kept apart for readability) ── */
@media (max-width: 639px) {
  .dashboard-content > div {
    padding: 10px !important;
  }

  .dashboard-content .p-8 {
    padding: 16px;
  }

  .dashboard-content .p-6 {
    padding: 14px;
  }

  .dashboard-content .p-5 {
    padding: 12px;
  }

  .dashboard-content .gap-8 {
    gap: 16px;
  }

  .dashboard-content .gap-6 {
    gap: 14px;
  }

  .dashboard-content .gap-5 {
    gap: 12px;
  }

  /* the auction cards keep one to a row (their bid row needs the width) but lose
     the 210px photo that made each one a screen tall */
  .auction-card [class*="aspect-["] {
    aspect-ratio: 16 / 9;
  }
}

/* ── Phones: data tables as cards ─────────────────────────────────────────────
   My Products / My Equipments, My Auctions, Saved Quotes and every Reports tab
   are `.site-table`s 800–2,130px wide, so on a phone the useful columns were
   somewhere to the right. Each row becomes a small record sheet: the labels come
   from the table's own header (lib/stackTables.ts), the first cell (identity)
   and the last (the row's actions) take the full row, everything between pairs
   up. Nothing is hidden — every cell and every control is still there.
   A table opts out with `site-table--keep`. */
@media (max-width: 639px) {
  .site-table:not(.site-table--keep) {
    min-width: 0 !important;
    display: block;
    border: 0;
  }

  .site-table:not(.site-table--keep) > thead {
    display: none;
  }

  .site-table:not(.site-table--keep) > tbody {
    display: block;
  }

  .site-table:not(.site-table--keep) > tbody > tr {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    column-gap: 12px;
    margin: 8px;
    padding: 6px 12px;
    border: 1px solid #e8e8e8;
    border-radius: 10px;
    background: #fff;
  }

  .site-table:not(.site-table--keep) > tbody > tr > td {
    display: block;
    min-width: 0;
    padding: 6px 0;
    border: 0;
    border-bottom: 1px dashed #ececec;
    overflow-wrap: anywhere;
  }

  .site-table:not(.site-table--keep) > tbody > tr > td:first-child,
  .site-table:not(.site-table--keep) > tbody > tr > td:last-child,
  .site-table:not(.site-table--keep) > tbody > tr > td[colspan] {
    grid-column: 1 / -1;
  }

  .site-table:not(.site-table--keep) > tbody > tr > td:last-child {
    border-bottom: 0;
  }

  .site-table:not(.site-table--keep) > tbody > tr > td::before {
    content: attr(data-label);
    display: block;
    margin-bottom: 2px;
    font-size: 10px;
    font-weight: 700;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: #68645d;
  }

  .site-table:not(.site-table--keep) > tbody > tr > td[data-label=""]::before,
  .site-table:not(.site-table--keep) > tbody > tr > td:not([data-label])::before,
  .site-table:not(.site-table--keep) > tbody > tr > td[colspan]::before {
    display: none;
  }

  /* cells that pinned a width for the TABLE */
  .site-table:not(.site-table--keep) td [class*="min-w-["] {
    min-width: 0 !important;
  }

  /* the pinned last column is just a cell again */
  .site-table--pin-last:not(.site-table--keep) > tbody > tr > td:last-child {
    position: static;
    box-shadow: none;
    background: transparent;
  }
}

@media (max-width: 639px) {
  /* a card has the width of a card: text that was told not to wrap, or to cut
     itself off with "…", wraps instead — a title is what identifies the row */
  .site-table:not(.site-table--keep) > tbody > tr > td,
  .site-table:not(.site-table--keep) > tbody > tr > td [class*="whitespace-nowrap"],
  .site-table:not(.site-table--keep) > tbody > tr > td .truncate {
    white-space: normal !important;
  }

  .site-table:not(.site-table--keep) > tbody > tr > td .truncate {
    overflow: visible;
    text-overflow: clip;
  }

  /* values that are long by nature keep the whole row */
  .site-table:not(.site-table--keep) > tbody > tr > td:is(
    [data-label="name" i], [data-label="title" i], [data-label="equipment" i], [data-label="item name" i],
    [data-label="item" i], [data-label="product" i], [data-label="comments" i], [data-label="description" i],
    [data-label="email" i], [data-label="listing" i], [data-label="customer" i]
  ) {
    grid-column: 1 / -1;
  }
}

@media (max-width: 639px) {
  /* form grids: short fields (first / last name, phone / email, city / state)
     pair up instead of one field per screen-width. A text area, or a field the
     page marked `col-span-*`, keeps the whole row. */
  .grid.grid-cols-1:is(.sm\:grid-cols-2, .md\:grid-cols-2, .sm\:grid-cols-3, .md\:grid-cols-3):has(.form-control):not(:has(textarea, .form-control-textarea, input[type="checkbox"])) {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 10px;
  }

  .grid.grid-cols-1:is(.sm\:grid-cols-2, .md\:grid-cols-2, .sm\:grid-cols-3, .md\:grid-cols-3):has(.form-control) > * {
    min-width: 0;
  }

  .grid.grid-cols-1:is(.sm\:grid-cols-2, .md\:grid-cols-2, .sm\:grid-cols-3, .md\:grid-cols-3):has(.form-control) > :is([class*="col-span-"], :has(textarea)) {
    grid-column: 1 / -1;
  }
}

@media (max-width: 639px) {
  /* marketing sections were padded 40–100px top and bottom */
  section:is([class*="py-10"], [class*="py-14"], [class*="py-16"], [class*="py-24"], [class*="py-[clamp("], [class*="py-[50px]"]) {
    padding-top: 28px;
    padding-bottom: 28px;
  }
}

/* a phone turned sideways: keep the sticky header and the modals inside 390px of height */
@media (max-height: 500px) and (orientation: landscape) and (max-width: 1023px) {
  .site-header > div:first-of-type {
    padding-top: 4px;
    padding-bottom: 4px;
  }

  .site-header img[alt="HDD Depot"] {
    height: 34px;
  }

  .site-modal-panel {
    max-height: calc(100dvh - 16px);
    padding-top: 16px;
    padding-bottom: 12px;
  }
}

/* touch targets: the small controls that measured 20–24px tall on a phone */
@media (max-width: 639px) {
  .role-switch__option {
    padding-top: 9px;
    padding-bottom: 9px;
  }

  .sidebar-section-toggle {
    min-height: 40px;
  }

  button[aria-label="Show password"],
  button[aria-label="Hide password"] {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 40px;
    min-height: 40px;
    right: 2px !important;
  }
}

/* ── Listing card (client's simplified layout): brand · ID · category → name → price → button ── */
.product-card__title {
  /* ⚠️ ONE LINE, with an ellipsis. Every name is the same height by construction, so every
     card is too, with no reserved blank space. The full name is in the `title` attribute
     and on the detail page. */
  line-height: 1.5;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

@media (max-width: 639px) {
  .product-card .product-card__body {
    padding: 10px;
    gap: 8px;
  }

  .product-card .product-card__title {
    font-size: 14px;
    line-height: 1.3;
  }

  .product-card .product-card__chips {
    min-height: 28px;
    gap: 4px;
  }

  .product-card .product-card__chips > div {
    height: 28px;
  }

  /* the ID is short and is the one chip that must stay whole; the others give way */
  .product-card .product-card__id {
    flex: 0 0 auto;
  }
}

/* ── Search page Top Matches: mixed cards in one grid (phone) ──────────────
   Product cards sit two to a row on a phone (rule above). Top Matches mixes
   them with auction, event and vendor cards, which cannot live in half a
   row — those take the whole row. */
@media (max-width: 639px) {
  .grid:has(> .product-card:not(.auction-card)) > :not(.product-card),
  .grid:has(> .product-card:not(.auction-card)) > .auction-card {
    grid-column: 1 / -1;
  }

  /* Top Matches mixes kinds in a short list, so it is ONE column on a phone —
     a lone half-width product card next to an empty half reads as broken. */
  .grid.search-top-grid {
    grid-template-columns: minmax(0, 1fr) !important;
  }
}

/* Sale strip (top bar of the bidding console): in Day mode it was pure white
   and faded into the page. A touch darker gives it an edge; Night is unchanged. */
.bc[data-theme='day'] .console-sale-strip {
  background-color: #dfe3e8;
  border-bottom-color: #c5cad1;
  box-shadow: 0 1px 0 rgba(0, 0, 0, 0.04);
}

/* ── Phone: headings and grid children may never push a page wider ─────────
   A grid/flex item's minimum width is its content, so one long Impact
   heading ("HORIZONTAL DIRECTIONAL DRILL", "FIND YOUR NEXT HDD MACHINE")
   stretched its column past the screen and the right side was cut off.
   min-width:0 lets the column shrink; overflow-wrap lets a long word break
   rather than overflow. Phone only — tablet and desktop are untouched. */
@media (max-width: 639px) {
  .container .grid > *,
  .container .flex > *,
  .hero-swiper .swiper-slide > * {
    min-width: 0;
  }

  h1, h2, h3, h4, .font-impact-regular {
    overflow-wrap: anywhere;
    word-break: normal;
  }

  .hero-swiper h1,
  .hero-swiper h2 {
    font-size: clamp(26px, 8vw, 34px) !important;
    line-height: 1.1 !important;
  }

  .hero-swiper p {
    max-width: 100% !important;
  }
}

/* ROOT CAUSE of the clipped hero / category headings on a phone: a
   `grid-cols-12 gap-8` grid spends 11 × 32px = 352px on gaps alone, so on a
   320px screen every column collapses to 0 and a `col-span-12` item is as wide
   as the gaps (352px) — wider than the screen. On a phone every child of these
   grids spans the whole row, so there is nothing to put a column gap between.
   Row gaps (stacking) are untouched. */
@media (max-width: 639px) {
  .grid.grid-cols-12 {
    column-gap: 0;
  }
}

/* Phone: every grid cell may shrink below its content (countdown cells,
   chips, stat tiles) instead of forcing the row wider than the screen. */
@media (max-width: 639px) {
  .grid > * {
    min-width: 0;
  }
}

/* ── Phone turned sideways (landscape, ≤500px tall) ────────────────────────
   Width is 640–950px here, so none of the portrait phone rules (<640px)
   apply, and the desktop scale puts a 72px heading and 100px section padding
   into 390px of height. Height — not width — is the scarce thing, so this
   block shrinks type and vertical spacing only. A landscape tablet is taller
   than 500px and is untouched. */
@media (orientation: landscape) and (max-height: 500px) {
  html { font-size: 14px; }

  body { font-size: 14px; }

  .container {
    padding-left: max(16px, env(safe-area-inset-left));
    padding-right: max(16px, env(safe-area-inset-right));
  }

  h1, .font-impact-regular { overflow-wrap: anywhere; }

  h1 { font-size: clamp(22px, 7vh, 30px) !important; line-height: 1.12 !important; }
  h2 { font-size: clamp(20px, 6vh, 26px) !important; line-height: 1.15 !important; }
  h3 { font-size: clamp(16px, 5vh, 21px) !important; line-height: 1.2 !important; }

  /* hero carousel: a fixed 628px min-height is taller than the screen */
  .hero-swiper .swiper-slide > div { min-height: 0 !important; }
  .hero-swiper .grid { padding-top: 20px !important; padding-bottom: 48px !important; gap: 16px !important; }
  .hero-swiper p { max-width: 100% !important; font-size: 14px !important; }
  .hero-figure { height: 220px; max-width: 320px; }
  .hero-swiper + div, .hero-pagination { bottom: 8px; }

  /* banners and marketing sections */
  section[class*="other-banner-bg"] { padding-top: 20px !important; padding-bottom: 20px !important; }
  section:is([class*="py-10"], [class*="py-14"], [class*="py-16"], [class*="py-24"], [class*="py-[clamp("], [class*="py-[50px]"], [class*="py-[60px]"], [class*="py-[80px]"], [class*="py-[100px]"]) {
    padding-top: 24px !important;
    padding-bottom: 24px !important;
  }

  .btn { min-height: 38px; padding-top: 6px; padding-bottom: 6px; font-size: 14px; }
  .form-control { min-height: 40px; font-size: 16px; }

  /* the vertical gaps that were sized for a tall desktop page */
  .grid { row-gap: 16px; }
  .flex-col.gap-8, .flex-col.gap-10, .flex-col.gap-12 { gap: 16px; }

  /* the floating helper sits over content on a short screen */
  .site-modal-panel { max-height: calc(100dvh - 12px); padding: 14px 16px; }

  /* the page is two rows tall at most: dashboard sidebar must not eat it */
  .site-sidebar-inner { max-height: calc(100dvh - var(--site-header-height, 56px) - 8px); }

  .product-card .product-card__title { font-size: 14px; }
}

/* Landscape phone: listing cards three across, with a short photo, so a page
   of products is not a tall column of 250px pictures. Auction / event / vendor
   cards keep their own layout (they need the width). */
@media (orientation: landscape) and (max-height: 500px) {
  .grid:has(> .product-card:not(.auction-card)) {
    grid-template-columns: repeat(3, minmax(0, 1fr)) !important;
  }

  .product-card:not(.auction-card) :is(.swiper, .listing-gallery, [class*="aspect-"]) {
    max-height: 150px;
  }

  .product-card:not(.auction-card) img { max-height: 150px; object-fit: contain; }
  .product-card:not(.auction-card) .product-card__title { -webkit-line-clamp: 1; }
}

/* Hero on a small tablet / big phone (640–1023px wide): the artwork used to
   stack under the text in a 520px+ box, leaving a tall empty band. Text and
   picture sit side by side from 640px, the slide is only as tall as its
   content, and the picture box is sized to the column, not to a desktop. */
@media (min-width: 640px) and (max-width: 1023px) {
  .hero-swiper .swiper-slide > div { min-height: 0 !important; }

  .hero-swiper .grid.grid-cols-12 {
    grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
    align-items: center;
    column-gap: 20px;
    padding-top: 28px !important;
    padding-bottom: 56px !important;
  }

  .hero-swiper .grid.grid-cols-12 > * { grid-column: auto !important; }

  .hero-swiper h1,
  .hero-swiper h2 { font-size: clamp(26px, 4.6vw, 38px) !important; }

  .hero-swiper p { max-width: 100% !important; }

  .hero-figure { height: clamp(200px, 30vw, 280px); max-width: 100%; }
}

/* Landscape phone hero = the desktop hero, scaled: the slide fills the screen
   under the header, text block left, framed picture right, both centred on the
   same line, pagination bottom-left. Everything is sized from the screen's
   HEIGHT (vh), because that is what is scarce sideways. */
@media (orientation: landscape) and (max-height: 500px) {
  .hero-swiper .swiper-slide > div {
    min-height: calc(100dvh - var(--site-header-height, 64px)) !important;
    align-items: center;
  }

  .hero-swiper .container {
    padding-left: max(7vw, env(safe-area-inset-left)) !important;
    padding-right: max(5vw, env(safe-area-inset-right)) !important;
  }

  .hero-swiper .grid.grid-cols-12 {
    grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr);
    column-gap: 4vw !important;
    align-items: center;
    padding-top: 12px !important;
    padding-bottom: 40px !important;
  }

  .hero-swiper .grid.grid-cols-12 > * { grid-column: auto !important; }

  .hero-swiper h1,
  .hero-swiper h2 { font-size: clamp(26px, 10vh, 44px) !important; line-height: 1.05 !important; }

  .hero-swiper p { font-size: clamp(13px, 3.6vh, 15px) !important; max-width: 36ch !important; }

  .hero-figure {
    height: min(68vh, 280px);
    max-width: 100%;
    margin-left: auto;
  }

  .hero-swiper .hero-pagination { justify-content: flex-start; }
  .hero-swiper + div,
  .hero-swiper .absolute.bottom-6 { bottom: 14px !important; }
  .hero-swiper .absolute.bottom-6 .container { padding-left: max(7vw, env(safe-area-inset-left)) !important; }
}

/* Hero in a wide-but-short window below 640px (e.g. a phone sideways in a
   resized browser, 480–639px wide and wider than it is tall): it fell between
   the portrait rules and the 640px+ rules and stacked the picture under the
   text. Same side-by-side hero as above, for any landscape window 480–1023px. */
@media (orientation: landscape) and (min-width: 480px) and (max-width: 639px) {
  .hero-swiper .swiper-slide > div { min-height: 0 !important; }

  .hero-swiper .grid.grid-cols-12 {
    grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
    align-items: center;
    column-gap: 16px !important;
    padding-top: 24px !important;
    padding-bottom: 52px !important;
  }

  .hero-swiper .grid.grid-cols-12 > * { grid-column: auto !important; }

  .hero-swiper h1,
  .hero-swiper h2 { font-size: clamp(22px, 5vw, 30px) !important; }

  .hero-swiper p { font-size: 14px !important; max-width: 100% !important; }

  .hero-figure { height: clamp(170px, 34vw, 230px); max-width: 100%; margin-left: auto; }
}

/* Landscape phone / small tablet (640–1023px wide, ≤500px tall): two-column
   layouts that only switch on at the `lg` (1024px) breakpoint stayed one
   column, so a product photo took the whole width (~500px tall — more than
   the screen) with the details far below it. Side by side from 640px. */
@media (orientation: landscape) and (max-height: 500px) and (min-width: 640px) and (max-width: 1023px) {
  .grid[class*="lg:grid-cols-2"] {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    column-gap: 20px !important;
    row-gap: 16px;
  }

  .grid[class*="lg:grid-cols-2"] > * { min-width: 0; }
}

/* Page-to-page fade (useRouteFade). Opacity only: a transform would make this
   box the containing block for fixed-position modals while it runs. */
.route-fade { animation: route-fade-in 200ms ease-out both; }
@keyframes route-fade-in { from { opacity: 0; } to { opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .route-fade { animation: none; } }
