/*
 * Auctioneer — public visual system.
 *
 * Propshaft serves this file directly (no preprocessing). Organized as: tokens →
 * base/type → shell (header/footer/flash) → controls (buttons/forms) → catalog →
 * auction detail (gallery + sale instrument + bid ledger) → auth/account → motion.
 *
 * Design intent lives in specs scratchpad design-notes: a live community
 * auction. Brand color is INJECTED per deployment (--brand-primary,
 * default forest #1f5130) and used as the org's house color; a separate reserved
 * --live red signals TIME urgency only (closing window / anti-sniping), never brand.
 */

/* ----- Self-hosted fonts ------------------------------------------------ */
/* No external font requests (Principle VIII — offline-capable, self-hostable).
   Files live in app/assets/fonts (on the asset load path); Propshaft fingerprints
   them and rewrites these bare-filename url()s. Archivo Expanded + Inter are
   variable (weight ranges); Space Mono ships static 400/700. */
@font-face { font-family: "Archivo Expanded"; font-style: normal; font-weight: 600 800; font-display: swap; src: url("/assets/archivo-expanded-74876bef.woff2") format("woff2"); }
@font-face { font-family: "Inter"; font-style: normal; font-weight: 400 600; font-display: swap; src: url("/assets/inter-496a588b.woff2") format("woff2"); }
@font-face { font-family: "Space Mono"; font-style: normal; font-weight: 400; font-display: swap; src: url("/assets/space-mono-400-2fb0d22d.woff2") format("woff2"); }
@font-face { font-family: "Space Mono"; font-style: normal; font-weight: 700; font-display: swap; src: url("/assets/space-mono-700-27e084bd.woff2") format("woff2"); }

/* ----- Tokens ----------------------------------------------------------- */
:root {
  /* --brand-primary is INJECTED inline from SystemConfiguration in the <head>,
     which loads BEFORE this stylesheet. Declaring a default value here would win
     on source order (equal :root specificity, later wins) and silently override
     the injected color, so the default lives in the var() fallback instead. */
  --brand: var(--brand-primary, #1f5130);
  --on-brand: #ffffff;

  --paper: #e9e8e2;                    /* page: warm-grey background */
  --card: #fbfaf6;                     /* item card stock */
  --surface: #ffffff;                  /* elevated panels */
  --ink: #1a1b18;                      /* near-black warm text */
  --ink-soft: #56574e;                 /* muted labels / secondary */
  --line: #d6d5c9;                     /* hairline */
  --brand-tint: color-mix(in srgb, var(--brand) 12%, var(--card));
  --live: #c2261b;                     /* TIME URGENCY ONLY */
  --live-tint: #fbe9e6;
  --live-ink: #7c1d15;                 /* AA-contrast dark red: readable text on --live-tint */

  --r-tag: 3px;
  --r: 8px;
  --r-lg: 14px;
  --shadow: 0 1px 2px rgba(26, 27, 24, .04), 0 6px 18px rgba(26, 27, 24, .06);
  --shadow-lift: 0 2px 4px rgba(26, 27, 24, .06), 0 14px 30px rgba(26, 27, 24, .10);

  --font-display: "Archivo Expanded", "Archivo", system-ui, sans-serif;
  --font-body: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --font-mono: "Space Mono", ui-monospace, "SFMono-Regular", Menlo, monospace;

  --maxw: 1120px;
}

/* ----- Base & type ------------------------------------------------------ */
*, *::before, *::after { box-sizing: border-box; }

body {
  margin: 0;
  color: var(--ink);
  background: var(--paper);
  font-family: var(--font-body);
  font-size: 1rem;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

main {
  display: block;
  max-width: var(--maxw);
  margin: 0 auto;
  padding: 2rem 1.25rem 3rem;
}

h1, h2, h3 {
  font-family: var(--font-display);
  font-weight: 800;
  line-height: 1.08;
  letter-spacing: -0.01em;
  margin: 0 0 .5rem;
  overflow-wrap: break-word;   /* long lot titles wrap rather than overflow on narrow screens */
}
h1 { font-size: clamp(1.75rem, 4vw, 2.6rem); }
h2 { font-size: clamp(1.25rem, 2.4vw, 1.6rem); font-weight: 700; }
h3 { font-size: 1.1rem; font-weight: 700; }

p { margin: 0 0 1rem; }

a {
  color: var(--brand);
  text-decoration: underline;
  text-underline-offset: 2px;
  text-decoration-thickness: 1px;
}
a:hover { text-decoration-thickness: 2px; }

/* Eyebrow / kicker: the poster-bill label voice. */
.eyebrow {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: .72rem;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--ink-soft);
  margin: 0 0 .35rem;
}

.hint { color: var(--ink-soft); font-size: .9rem; }
/* A resend-verification button_to sits inline in a .hint sentence; drop the form's box
   (it is display:block by default) so the button flows in the line instead of wrapping
   onto its own row (same technique as .header-nav .button_to). */
.hint .button_to { display: contents; }

:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
  border-radius: 2px;
}

/* ----- Lot-tag: the notched auction ticket chip ------------------------- */
.lot-tag {
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: .68rem;
  letter-spacing: .07em;
  text-transform: uppercase;
  padding: .3rem .6rem;
  background: var(--ink);
  color: #fff;
  border-radius: var(--r-tag);
  /* one clipped corner = the string-tied auction tag */
  clip-path: polygon(8px 0, 100% 0, 100% 100%, 0 100%, 0 8px);
}
.lot-tag[data-state="open"]      { background: var(--brand); color: var(--on-brand); }
.lot-tag[data-state="scheduled"] { background: var(--surface); color: var(--ink); box-shadow: inset 0 0 0 1px var(--line); }
.lot-tag[data-state="closed"],
.lot-tag[data-state="cancelled"],
.lot-tag[data-state="no_sale"]   { background: var(--ink-soft); color: #fff; }  /* terminal, non-sold */
.lot-tag[data-state="sold"]      { background: var(--ink); color: #fff; }       /* distinct from no_sale */

/* ----- Shell: header / footer / flash ----------------------------------- */
.site-header {
  background: var(--brand);
  color: var(--on-brand);
}
.site-nav {
  max-width: var(--maxw);
  margin: 0 auto;
  padding: .85rem 1.25rem;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
}
.site-header a, .site-header .brand-name { color: var(--on-brand); text-decoration: none; }
.brand { display: inline-flex; align-items: center; }
.brand-name {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 1.3rem;
  letter-spacing: .01em;
}
.brand-logo { display: block; max-height: 44px; width: auto; }

/* Only present when a logo is attached, so a name-only header is untouched by any of
   this. .site-nav already wraps, so the lockup drops onto its own row before it can
   crowd the auth links; what wrapping there does NOT solve is the lockup itself being
   wider than a phone, which is easy to hit (a 200x60 logo renders ~147px wide at the
   44px cap, and org names run long). flex-wrap here lets the name fall under the logo
   rather than push the header sideways into a horizontal scroll, and the phone rule
   below trims both so it usually still fits on one line.

   The name is never display:none at any width. Once the logo went decorative (alt=""
   in layouts/_branding) this text is the home link's ONLY accessible name, so hiding it
   on small screens would ship an unnamed link (WCAG 2.4.4 / 4.1.2). Shrinking is the
   lever available here; hiding is not. */
.brand-lockup { gap: .6rem; flex-wrap: wrap; }
@media (max-width: 600px) {
  .brand-lockup .brand-logo { max-height: 32px; }
  .brand-lockup .brand-name { font-size: 1.05rem; }
}

.header-nav { display: flex; align-items: center; gap: 1.25rem; flex-wrap: wrap; }
.header-nav a, .header-nav .link-button {
  font-weight: 500;
  font-size: .95rem;
  color: var(--on-brand);
}
.header-nav a:hover, .header-nav .link-button:hover { text-decoration: underline; }
.header-nav .link-button { text-decoration: none; }
/* Sign out is a button_to — a <form> wrapping the button. display:contents drops the
   form's box so the button becomes a direct flex child, aligning on the same baseline
   and gap as the sibling <a> links instead of sitting in its own nested box. */
.header-nav .button_to { display: contents; }

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  background: var(--surface);
  color: var(--brand);
  padding: .5rem 1rem;
  z-index: 1000;
  border-radius: 0 0 var(--r) 0;
}
.skip-link:focus { left: 0; }

.flash {
  margin: 0 0 1.25rem;
  padding: .7rem 1rem;
  border-radius: var(--r);
  font-size: .95rem;
  border: 1px solid transparent;
}
.flash-notice { background: var(--brand-tint); color: var(--ink); border-color: color-mix(in srgb, var(--brand) 35%, transparent); }
.flash-alert  { background: var(--live-tint); color: var(--live-ink); border-color: color-mix(in srgb, var(--live) 35%, transparent); }

.site-footer {
  border-top: 1px solid var(--line);
  margin-top: 2rem;
}
.site-footer-inner {
  max-width: var(--maxw);
  margin: 0 auto;
  padding: 1.5rem 1.25rem;
  color: var(--ink-soft);
  font-size: .9rem;
}

/* ----- Controls: buttons & forms ---------------------------------------- */
input[type="text"], input[type="email"], input[type="password"],
input[type="search"], input[type="number"], select, textarea {
  font: inherit;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r);
  padding: .6rem .7rem;
  width: 100%;
  max-width: 26rem;
}
input:focus, select:focus, textarea:focus {
  border-color: var(--brand);
  outline: 2px solid color-mix(in srgb, var(--brand) 35%, transparent);
  outline-offset: 0;
}

.field { margin-bottom: 1.1rem; }
.field label, .field > .form-label {
  display: block;
  font-weight: 600;
  font-size: .92rem;
  margin-bottom: .35rem;
}
.field .hint { margin: .35rem 0 0; }
.field-checkbox { display: flex; align-items: center; gap: .5rem; }
.field-checkbox label { margin: 0; font-weight: 500; }
.field-checkbox input { width: auto; }

/* Checkbox group (e.g. admin settings "Picture types allowed"): a borderless
   fieldset whose legend reads like a field label and whose options are compact,
   inline checkbox rows. Selectors are specific enough to beat `.field label`. */
.checkbox-group { border: 0; padding: 0; margin: 0 0 1.1rem; }
.checkbox-group > legend { display: block; font-weight: 600; font-size: .92rem; padding: 0; margin-bottom: .35rem; }
.checkbox-group .checkbox { display: flex; align-items: center; gap: .5rem; margin: .3rem 0; font-weight: 500; }
.checkbox-group .checkbox input { width: auto; margin: 0; }

/* Brand-color AA suggestion (admin settings): a click-to-apply swatch the
   brand-color controller reveals when the picked color fails contrast, so the
   admin applies the nearest compliant shade instead of guessing a darker one. */
.brand-color-suggestion { display: flex; align-items: center; gap: .4rem; flex-wrap: wrap; }
/* display:flex above would otherwise beat the UA `[hidden] { display:none }`, so the
   controller's hidden toggle needs this higher-specificity rule to actually hide it. */
.brand-color-suggestion[hidden] { display: none; }
.brand-swatch-apply {
  display: inline-flex; align-items: center; gap: .4rem;
  font: inherit; cursor: pointer;
  padding: .15rem .5rem;
  background: var(--surface); color: var(--ink);
  border: 1px solid var(--line); border-radius: var(--r);
  font-family: var(--font-mono);
}
.brand-swatch-apply:hover { border-color: var(--brand); }
.brand-swatch {
  width: 1rem; height: 1rem; border-radius: 3px;
  box-shadow: inset 0 0 0 1px rgba(26, 27, 24, .18);
}

/* Primary action: solid house color. button_to/submit/.btn share this. A bare <button>
   is filled by default; every non-primary button (.queue-tab, .queue-action, .btn-quiet,
   .btn-danger, .table-sort-button, .auth-modal-close, the toolbar buttons) RE-DECLARES its
   own look at matching-or-higher specificity and wins by source order — do NOT lengthen
   this rule's :not() chain to exclude them: raising THIS selector's specificity silently
   overtakes all those source-order-tie rules at once (that regression was caught in review).
   Keep this selector minimal; make new quiet/control buttons out-specify it instead. */
input[type="submit"], button:not(.link-button):not(.gallery-thumb):not(.brand-swatch-apply), .btn {
  font-family: var(--font-body);
  font-weight: 600;
  font-size: .98rem;
  color: var(--on-brand);
  background: var(--brand);
  border: 1px solid transparent;
  border-radius: var(--r);
  padding: .65rem 1.3rem;
  min-height: 44px;
  cursor: pointer;
  /* Anchors styled as buttons (link_to ..., class: "btn", e.g. "Create auction")
     otherwise inherit the global `a { text-decoration: underline }` and show an
     underline inside the filled pill. */
  text-decoration: none;
  transition: filter .15s ease;
}
input[type="submit"]:hover, button:not(.link-button):not(.gallery-thumb):not(.brand-swatch-apply):hover, .btn:hover { filter: brightness(1.08); }

.link-button {
  background: none;
  border: none;
  color: var(--brand);
  text-decoration: underline;
  text-underline-offset: 2px;
  cursor: pointer;
  font: inherit;
  padding: 0;
  min-height: 0;
}
.link-button.danger { color: var(--live); }

/* Destructive action button: red-outlined, fills red on hover. Deliberately NOT a
   solid red at rest (that's the reserved time-urgency signal) and clearly distinct
   from the solid brand primary, so Remove/Cancel/Ban read as destructive. The
   selectors mirror the primary rule's specificity (+ .btn-danger) so they win by
   source order without !important. */
input[type="submit"].btn-danger,
button.btn-danger:not(.link-button):not(.gallery-thumb),
.btn.btn-danger {
  background: var(--surface);
  color: var(--live);
  border: 1px solid var(--live);
}
input[type="submit"].btn-danger:hover,
button.btn-danger:not(.link-button):not(.gallery-thumb):hover,
.btn.btn-danger:hover {
  background: var(--live);
  color: #fff;
  filter: none;
}

.form-errors {
  background: var(--live-tint);
  border: 1px solid color-mix(in srgb, var(--live) 35%, transparent);
  border-radius: var(--r);
  padding: .85rem 1rem;
  margin-bottom: 1.25rem;
}
/* The shared shared/_form_errors partial emits <h3>; the seven inline error blocks use
   <h2>. Match both levels so the summary heading is styled wherever it is used. */
.form-errors :is(h2, h3) { font-size: 1rem; color: var(--live-ink); margin-bottom: .4rem; }
.form-errors ul { margin: 0; padding-left: 1.1rem; }

/* ----- Featured introduction (optional, operator-authored) -------------- */
/* A welcome band above the catalog heading: featured image (~2 card-widths) beside a
   copy panel (~1 card-width). The image sits in a card-stock "mat" (frame + inset)
   echoing a framed auction lot — the one distinctive touch; everything else stays
   quiet. The copy panel rides on a soft wash of the deployment brand color so the
   intro reads as branding, distinct from the neutral catalog grid below. */
.featured-intro {
  display: grid;
  grid-template-columns: 2fr 1fr;
  gap: 1.25rem;
  align-items: stretch;
  margin: 0 0 2rem;
}

.featured-intro-media {
  margin: 0;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  padding: .5rem;                 /* the inset "mat" around the print */
  box-shadow: var(--shadow);
  overflow: hidden;
  /* Floor the band height so one line of copy can't squash the image to a sliver.
     The columns stretch to equal height (grid default), and the image cover-fills
     whatever height results — so height is content-driven, with this as the minimum. */
  min-height: 14rem;
}
.featured-intro-image {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;             /* fill the mat, cropping rather than distorting */
  border-radius: calc(var(--r-lg) - 5px);
  background: #efeee8;            /* placeholder tone while the image loads */
}

.featured-intro-copy {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: .55rem;
  padding: 1.5rem 1.6rem;
  background: var(--brand-tint);
  border: 1px solid color-mix(in srgb, var(--brand) 22%, var(--line));
  border-radius: var(--r-lg);
}
.featured-intro-heading {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(1.35rem, 1.05rem + 1.3vw, 1.95rem);
  line-height: 1.08;
  letter-spacing: -.01em;
  margin: 0;
  color: var(--ink);
}
.featured-intro-text {
  color: var(--ink-soft);
  font-size: .98rem;
  line-height: 1.55;
}
.featured-intro-text p { margin: 0 0 .6rem; }
.featured-intro-text p:last-child { margin-bottom: 0; }

/* Stack while the 1fr copy column would otherwise fall below ~1 catalog-card width
   (260px). At 2fr:1fr with the page gutters and gap, the copy column reaches 260px at
   ~840px viewport — below that (incl. iPad portrait, 768px) stack image-over-copy so the
   copy never reads as cramped. */
@media (max-width: 840px) {
  .featured-intro { grid-template-columns: 1fr; }
  .featured-intro-copy { padding: 1.25rem 1.25rem; }
}

/* ----- Catalog ---------------------------------------------------------- */
.catalog-controls {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: .9rem;
  padding: 1rem 1.1rem;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  margin: 1rem 0 1.75rem;
}
.catalog-controls .field { margin-bottom: 0; flex: 1 1 12rem; }
.catalog-controls .field label { font-size: .72rem; text-transform: uppercase; letter-spacing: .06em; color: var(--ink-soft); }
.catalog-controls input, .catalog-controls select { max-width: none; }
.catalog-controls input[type="submit"] { flex: 0 0 auto; }

.auction-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: 1.25rem;
}
.auction-card {
  position: relative;
  display: flex;
  flex-direction: column;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  overflow: hidden;
  transition: transform .15s ease, box-shadow .15s ease, border-color .15s ease;
}
.auction-card:hover { border-color: color-mix(in srgb, var(--brand) 45%, var(--line)); }
/* Both cover chips (status, then category) in ONE row across the top of the thumbnail,
   rather than two independently positioned corners. Corner positioning gave a long
   category name nothing to stop it: it grew leftward until it ran under the status chip.
   In a row they are measured against each other, so they cannot collide.
   right:.7rem bounds the row at the card's other edge, which is what makes the wrap fire.
   pointer-events:none is inherited by both chips so these non-interactive labels don't
   shadow the stretched title link beneath them; the whole card stays clickable. */
.auction-card-tags {
  position: absolute;
  top: .7rem; left: .7rem; right: .7rem;
  z-index: 1;
  pointer-events: none;
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: .5rem;
}
/* Neither chip's text wraps: these are labels, and a two-line "CAST / IRON" chip reads as
   two chips. When the pair no longer fits on one line the ROW wraps instead, dropping the
   category to its own line under the status chip. flex-shrink:0 says the same thing the
   nowrap does, from the layout side: a chip is its text's width or it goes elsewhere, it
   is never compressed to fit. */
.auction-card-tags .lot-tag { flex: 0 0 auto; white-space: nowrap; }
/* Last-resort guard for the pathological case the wrap can't fix: a category name wider
   than the whole card, which has its own line and still doesn't fit. Ellipsis, not
   overflow past the card edge. display:block because text-overflow ellipsizes inline
   content, and this chip's inherited inline-flex would blockify to `flex` here, making its
   text an anonymous flex item that just gets clipped mid-letter. The full name stays in
   the DOM, so the ellipsis is sighted-only; AT still reads "Category: <full name>". */
.auction-card-tags .category-tag {
  display: block;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Neutrally colored (not state-driven: a category has no fixed palette), so it never
   competes with the status chip. Every AuctionItem has exactly one category, so this
   always renders. */
.category-tag { background: var(--surface); color: var(--ink); box-shadow: inset 0 0 0 1px var(--line); }

.auction-card-thumb {
  display: block;
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  background: #efeee8;
}
.auction-card-thumb-empty {
  display: flex;
  aspect-ratio: 4 / 3;
  align-items: center;
  justify-content: center;
  background: repeating-linear-gradient(135deg, #efeee8, #efeee8 10px, #eae9e1 10px, #eae9e1 20px);
}

.auction-card-title {
  font-family: var(--font-display);
  font-size: 1.12rem;
  font-weight: 700;
  letter-spacing: -.005em;
  line-height: 1.15;
  margin: .9rem 1rem .15rem;
}
.auction-card-title a { color: var(--ink); text-decoration: none; }
.auction-card-title a:hover { text-decoration: underline; text-decoration-color: var(--brand); }
/* Stretched link: the title is still the single, described link (so screen-reader
   users get one labeled link per card), but its hit area is expanded to cover the
   whole card so the entire card is clickable. The card is position:relative and the
   cover chips (.auction-card-tags) sit at z-index:1, above this overlay. */
.auction-card-title a::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
}

.auction-high-bid {
  margin: .35rem 1rem 0;
  font-size: .72rem;
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--ink-soft);
}
.auction-high-bid .card-high-bid {
  display: block;
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: 1.5rem;
  letter-spacing: -.02em;
  color: var(--ink);
  text-transform: none;
}
.auction-card-bids {
  margin: .15rem 1rem 0;
  font-family: var(--font-mono);
  font-size: .82rem;
  color: var(--ink-soft);
}
.auction-card-closes {
  margin: .5rem 1rem 1rem;
  padding-top: .65rem;
  border-top: 1px solid var(--line);
  font-size: .85rem;
  color: var(--ink-soft);
}
.auction-card-closes .countdown {
  font-family: var(--font-mono);
  font-weight: 700;
  color: var(--ink);
}
.auction-card-closes .countdown[data-countdown-state="urgent"],
.auction-card-closes .countdown[data-countdown-state="closing"] {
  color: var(--live);
  /* Non-color, non-motion cue so the closing state is perceivable without relying
     on hue or the (reduced-motion-gated) pulse — WCAG 1.4.1. */
  text-decoration: underline;
  text-decoration-thickness: 2px;
  text-underline-offset: 3px;
}

.catalog-empty {
  text-align: center;
  color: var(--ink-soft);
  padding: 3rem 1rem;
  background: var(--card);
  border: 1px dashed var(--line);
  border-radius: var(--r-lg);
}

/* ----- Auction detail layout -------------------------------------------- */
.auction { margin: 0; }
.auction > header h1 { margin-bottom: 1.25rem; }

.auction-detail {
  display: grid;
  gap: 1.75rem;
  grid-template-columns: 1fr;
}
@media (min-width: 900px) {
  .auction-detail { grid-template-columns: minmax(0, 1.4fr) minmax(320px, 1fr); align-items: start; }
}

/* ----- Gallery ---------------------------------------------------------- */
.gallery { margin: 0; }
/* The main picture is a link to its full-size rendition, so it picks up the global
   `a { color; text-decoration: underline }`. Strip both: the picture is the affordance,
   and an underline under a block-level image is just a stray rule across the layout.
   zoom-in is the cursor that says what activating it does. The focus ring is explicit
   because the global :focus-visible sits tight against the image edge, where a 2px brand
   line disappears into a dark photo. */
.gallery-main-link {
  display: block;
  color: inherit;
  text-decoration: none;
  cursor: zoom-in;
  border-radius: var(--r-lg);
}
.gallery-main-link:focus-visible { outline: 3px solid var(--brand); outline-offset: 3px; }
.gallery-main {
  display: block;
  width: 100%;
  max-height: 560px;
  object-fit: contain;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
}
.gallery-thumbs {
  display: flex;
  flex-wrap: wrap;
  gap: .55rem;
  list-style: none;
  padding: 0;
  margin: .7rem 0 0;
}
.gallery-thumb {
  padding: 0;
  border: 1px solid var(--line);
  border-radius: var(--r);
  background: var(--surface);
  cursor: pointer;
  overflow: hidden;
  line-height: 0;
}
.gallery-thumb img { display: block; width: 72px; height: 72px; object-fit: cover; }
.gallery-thumb[aria-current] { outline: 3px solid var(--brand); outline-offset: 0; border-color: var(--brand); }

.gallery-empty {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 280px;
  color: var(--ink-soft);
  background: repeating-linear-gradient(135deg, #efeee8, #efeee8 12px, #eae9e1 12px, #eae9e1 24px);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
}

/* The full-size viewer (native <dialog>, opened by the `lightbox` controller). Same inert
   pattern as .auth-modal, and for the same reason: `display` is deliberately NOT set here,
   so the UA default dialog:not([open]) { display: none } holds the dialog shut until
   showModal() runs. Setting display here would pin the picture over the page on load.

   Sized to the viewport rather than to the picture, with a transparent panel, so the whole
   thing reads as "the photo on a dark field" instead of "a photo in a box". max-width /
   max-height are reset because the UA caps a <dialog> well short of the viewport. */
.lightbox {
  /* Declared, not inherited: .lightbox-close is position:absolute against this box. Without
     it the button would anchor via whatever the UA happens to give a <dialog>, which is an
     undeclared dependency .auth-modal does not take either. */
  position: relative;
  width: calc(100vw - 2rem);
  height: calc(100vh - 2rem);
  height: calc(100dvh - 2rem);   /* dvh: account for mobile browser chrome */
  max-width: none;
  max-height: none;
  padding: 0;
  border: 0;
  background: transparent;
  overflow: hidden;
}
.lightbox[open] {
  display: flex;
  align-items: center;
  justify-content: center;
}
/* Darker than the auth modal's backdrop: this one is judging a photograph, and the page
   behind it bleeding through changes how the colors read. */
.lightbox::backdrop { background: rgba(26, 27, 24, .88); }
.lightbox-image {
  /* contain, not cover: the point is to see the whole picture, not to fill the box. This
     is also what makes ItemImage::LIGHTBOX_LIMIT a single square limit rather than an
     aspect-shaped one — see the comment on the constant. */
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  border-radius: var(--r);
}
/* Out-specify the primary-button rule (the mirrored-specificity pattern used by
   .auth-modal-close) so this dismiss glyph is never captured as a solid brand action. The
   surface fill is what keeps it legible sitting on top of a photograph. */
button.lightbox-close:not(.link-button):not(.gallery-thumb) {
  position: absolute;
  top: 0;
  right: 0;
  z-index: 2;
  width: 2.75rem;                    /* 44px touch target */
  height: 2.75rem;
  min-height: 0;
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 1.6rem;
  line-height: 1;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r);
  cursor: pointer;
}
button.lightbox-close:not(.link-button):not(.gallery-thumb):hover {
  background: var(--brand-tint);
  filter: none;
}

/* ----- The sale instrument (signature) ---------------------------------- */
.sale-panel { display: flex; flex-direction: column; gap: 1.25rem; }
@media (min-width: 900px) { .sale-panel { position: sticky; top: 1.25rem; } }

.auction-status {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow);
  padding: 1.25rem 1.35rem;
}
/* State line at the top of the instrument becomes the lot-tag treatment. */
/* The status chip reuses the shared .lot-tag (single source of per-state colors,
   defined once above); only its spacing differs inside the instrument. */
.auction-status .lot-tag { margin-bottom: .9rem; }

.auction-status dl { display: grid; grid-template-columns: 1fr; gap: 0; margin: 0; }
.auction-status dt {
  font-size: .72rem;
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--ink-soft);
  margin-top: .85rem;
}
.auction-status dd { margin: .1rem 0 0; }

/* Current high bid (first dt/dd) reads as the big scoreboard number. The styling
   lives on the .high-amount span — NOT the whole dd — so "No bids yet" and the
   connective "by" stay at calm body size. */
.auction-status dl dt:first-of-type { margin-top: 0; }
.auction-status .high-amount {
  display: block;
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: clamp(2rem, 6vw, 3.1rem);
  line-height: 1.05;
  letter-spacing: -.03em;
  color: var(--ink);
}
.auction-status .no-bids {
  font-family: var(--font-body);
  font-weight: 600;
  font-size: 1.1rem;
  color: var(--ink-soft);
}
.auction-status .leader-line {
  display: block;
  font-family: var(--font-body);
  font-weight: 600;
  font-size: .95rem;
  letter-spacing: 0;
  color: var(--ink-soft);
}
.auction-status .leader-line::before {
  content: "";
  display: inline-block;
  width: .5rem; height: .5rem;
  margin-right: .4rem;
  border-radius: 50%;
  background: var(--brand);
  vertical-align: middle;
}

/* Mono values for the remaining metrics + the countdown. */
.auction-status dd { font-family: var(--font-mono); font-size: 1rem; color: var(--ink); }
.auction-status .countdown { font-family: var(--font-mono); font-weight: 700; font-size: 1.35rem; }
.auction-status .countdown[data-countdown-state="urgent"],
.auction-status .countdown[data-countdown-state="closing"] {
  color: var(--live);
  /* Non-color, non-motion cue (WCAG 1.4.1) — see card countdown note. */
  text-decoration: underline;
  text-decoration-thickness: 2px;
  text-underline-offset: 3px;
}
.auction-status .countdown[data-state="sold"],
.auction-status .countdown[data-state="closed"],
.auction-status .countdown[data-state="cancelled"],
.auction-status .countdown[data-state="no_sale"] {
  display: inline-block;
  font-family: var(--font-display);
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

/* Bid form sits in the panel under the instrument on wide screens. */
.bid-form {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow);
  padding: 1.25rem 1.35rem;
}
.bid-form h2 { margin-top: 0; }
.bid-form input[type="number"] { max-width: none; font-family: var(--font-mono); font-size: 1.1rem; }
.bid-form input[type="submit"] { width: 100%; margin-top: .35rem; }

/* ----- Bid history ledger ----------------------------------------------- */
.bid-history { margin-top: 2rem; }
.bid-history-list { list-style: none; margin: 0; padding: 0; }
.bid-row {
  display: grid;
  grid-template-columns: 1fr auto;
  grid-template-areas: "amount time" "bidder time";
  column-gap: 1rem;
  align-items: center;
  padding: .7rem .9rem;
  border: 1px solid var(--line);
  border-top: none;
  background: var(--card);
}
.bid-row:first-child { border-top: 1px solid var(--line); border-radius: var(--r) var(--r) 0 0; }
.bid-row:last-child { border-radius: 0 0 var(--r) var(--r); }
/* Rows are ordered placed_at DESC (newest first), so the FIRST valid row is the
   current leader. A brand left-rule marks it (box-sizing: border-box keeps row width
   unchanged). :first-of-type targets .bid-row specifically; the empty-state <li> only
   renders when there are no bid rows, so it is never matched here. */
.bid-history-list .bid-row:first-of-type {
  border-left: 3px solid var(--brand);
  background: var(--brand-tint);
}
.bid-amount {
  grid-area: amount;
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: 1.05rem;
  color: var(--ink);
}
.bid-bidder { grid-area: bidder; color: var(--ink-soft); font-size: .9rem; }
.bid-time {
  grid-area: time;
  font-family: var(--font-mono);
  font-size: .82rem;
  color: var(--ink-soft);
}
.bid-history-empty {
  padding: 1.25rem;
  text-align: center;
  color: var(--ink-soft);
  background: var(--card);
  border: 1px dashed var(--line);
  border-radius: var(--r);
}

/* ----- Auth & account --------------------------------------------------- */
.auth-card {
  max-width: 27rem;
  margin: 1rem auto;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow);
  padding: 1.75rem 1.6rem;
}
.auth-card h1 { font-size: 1.7rem; }
.auth-card input[type="submit"], .auth-card .btn { width: 100%; }
.auth-card .auth-alt { margin: 1.1rem 0 0; font-size: .92rem; color: var(--ink-soft); }

.account-summary {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  padding: 1.1rem 1.25rem;
  margin: 0 0 1.75rem;
}
.account-summary dt { font-size: .72rem; text-transform: uppercase; letter-spacing: .06em; color: var(--ink-soft); margin-top: .75rem; }
.account-summary dt:first-child { margin-top: 0; }
.account-summary dd { margin: .1rem 0 0; }

.danger-zone {
  margin-top: 2rem;
  padding: 1.25rem;
  border: 1px solid color-mix(in srgb, var(--live) 30%, var(--line));
  border-left: 3px solid var(--live);
  border-radius: var(--r);
  background: color-mix(in srgb, var(--live-tint) 55%, var(--surface));
}
.danger-zone h2 { color: var(--live-ink); font-size: 1.1rem; }

/* ----- Account home ----------------------------------------------------- */
/* The bidder's own account page: a single readable column of sections — their
   bidding at a glance, then the controls to run the account. Same tokens/voice as
   the rest of the app; every section is a labelled landmark (Principle XI). */
.account-page { max-width: 52rem; margin: 0 auto; }
.account-header { margin-bottom: 1.75rem; }
.account-header-sub { color: var(--ink-soft); margin: .35rem 0 0; }
.account-email { font-family: var(--font-mono); color: var(--ink); }

/* Operator "Manage auctions" header: title left, primary action right-aligned on
   the same baseline; wraps and stays left-aligned on narrow viewports. */
.console-header {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: .4rem 1rem;
}
.console-header h1 { margin: 0; }

/* Operator/admin table cells that identify a bidder stack the real name over the
   email in a single column, the email reading as secondary. */
.bidder-cell { display: flex; flex-direction: column; gap: .1rem; }
.bidder-cell-email { font-size: .85rem; color: var(--ink-soft); }
.bidder-cell-email a { color: inherit; }

.account-section { margin: 2.25rem 0; }
.account-section-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: .4rem 1rem;
  margin-bottom: .5rem;
}
.account-section-head h2 { margin: 0; }
.account-section-action { font-size: .92rem; white-space: nowrap; }

/* Settings sections sit on an elevated panel; the bidding section stays open on the
   page so its cards read as content, not a form. */
.account-panel {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow);
  padding: 1.5rem 1.6rem;
}
.account-panel > h2:first-child { margin-top: 0; margin-bottom: 1rem; }
/* Inside a panel the summary loses its own card chrome — no box-in-a-box. */
.account-panel .account-summary { background: transparent; border: none; padding: 0; margin-bottom: 1.25rem; }

/* Secondary/quiet button: a hairline-outlined surface action for the account's
   navigational controls (change password/email, sign out). Distinct from the solid
   brand primary and the red danger. Selectors mirror the primary rule's specificity
   (+ .btn-quiet) so they win by source order without !important. */
input[type="submit"].btn-quiet,
button.btn-quiet:not(.link-button):not(.gallery-thumb),
.btn.btn-quiet {
  background: var(--surface);
  color: var(--brand);
  border: 1px solid var(--line);
}
input[type="submit"].btn-quiet:hover,
button.btn-quiet:not(.link-button):not(.gallery-thumb):hover,
.btn.btn-quiet:hover { background: var(--brand-tint); filter: none; }

/* Bidding activity — one card per item the bidder has bid on. Whole card is one hit
   area via a stretched link on the title (the single described link per card, same
   technique as the catalog cards). */
.activity-list {
  list-style: none;
  margin: 1.25rem 0 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
  gap: 1.1rem;
}
.activity-card {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: .55rem;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  padding: 1.1rem 1.2rem;
  transition: transform .15s ease, box-shadow .15s ease, border-color .15s ease;
}
.activity-card:hover { border-color: color-mix(in srgb, var(--brand) 45%, var(--line)); }
.activity-title {
  font-family: var(--font-display);
  font-size: 1.08rem;
  font-weight: 700;
  line-height: 1.15;
  margin: 0;
}
.activity-title a { color: var(--ink); text-decoration: none; }
.activity-title a:hover { text-decoration: underline; text-decoration-color: var(--brand); }
.activity-title a::after { content: ""; position: absolute; inset: 0; }

/* Standing pill. Tones map to design tokens; the label TEXT carries the meaning too,
   so color is never the sole signal (WCAG 1.4.1). Note: the reserved --live red is
   for TIME urgency only, so "Outbid" reads as a prominent dark chip, not red. */
.activity-status {
  position: relative;   /* above the stretched title link */
  z-index: 1;
  pointer-events: none; /* label, not a control — keep the card fully clickable */
  display: inline-flex;
  align-items: center;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: .66rem;
  letter-spacing: .07em;
  text-transform: uppercase;
  padding: .28rem .55rem;
  border-radius: var(--r-tag);
}
.activity-status[data-tone="leading"] { background: var(--brand); color: var(--on-brand); }
.activity-status[data-tone="outbid"]  { background: var(--ink);   color: #fff; }
.activity-status[data-tone="neutral"] { background: var(--brand-tint); color: var(--ink); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--brand) 30%, var(--line)); }
.activity-status[data-tone="muted"]   { background: var(--surface); color: var(--ink-soft); box-shadow: inset 0 0 0 1px var(--line); }

.activity-figures { display: flex; flex-wrap: wrap; gap: .35rem 1.75rem; margin: .15rem 0 0; }
.activity-figures dt {
  font-size: .66rem;
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--ink-soft);
}
.activity-figures dd {
  margin: .1rem 0 0;
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: 1.05rem;
  color: var(--ink);
}
.activity-muted { font-family: var(--font-body); font-weight: 500; font-size: .95rem; color: var(--ink-soft); }

/* When the lot closes / ended — a quiet line under the figures. The live countdown
   reuses the global .countdown urgent styling (red + underline near the anti-snipe
   window), so an Outbid lot the bidder can still win reads as time-sensitive. */
.activity-closes { margin: .1rem 0 0; font-size: .82rem; color: var(--ink-soft); }
.activity-closes .countdown { font-family: var(--font-mono); font-weight: 700; color: var(--ink); }
.activity-ended { font-family: var(--font-mono); }

/* Items to settle — a brand-tinted note (a positive outcome: you won). Informational
   only; the app never touches money (Principle V). */
.account-obligations {
  margin: 1.25rem 0 0;
  padding: 1.1rem 1.25rem;
  background: var(--brand-tint);
  border: 1px solid color-mix(in srgb, var(--brand) 30%, transparent);
  border-radius: var(--r-lg);
}
.account-obligations-title { font-size: 1.1rem; margin: 0 0 .35rem; }
.account-obligations .hint { margin: 0; }
.obligation-list { list-style: none; margin: .85rem 0 0; padding: 0; }
.obligation-row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: .25rem 1rem;
  padding: .6rem 0;
  border-top: 1px solid color-mix(in srgb, var(--brand) 22%, transparent);
}
.obligation-item { flex: 1 1 12rem; font-weight: 600; }
.obligation-amount { font-family: var(--font-mono); font-weight: 700; color: var(--ink); }
.obligation-due { font-family: var(--font-mono); font-size: .82rem; color: var(--ink-soft); }

.account-empty {
  margin-top: 1.25rem;
  padding: 2.5rem 1rem;
  text-align: center;
  color: var(--ink-soft);
  background: var(--card);
  border: 1px dashed var(--line);
  border-radius: var(--r-lg);
}
.account-empty p { margin: 0 0 1rem; }

.account-credentials { display: flex; flex-wrap: wrap; gap: .6rem; margin-top: 1.1rem; }
.account-credentials form { margin: 0; }
.account-verified { color: var(--brand); font-weight: 600; }
.account-unverified { font-weight: 600; }
/* Operator bidder-identity list: a two-column definition list matching the app's
   restrained voice (every other dl is deliberately styled; this one was raw defaults). */
.bidder-identity dl { display: grid; grid-template-columns: max-content 1fr; gap: .35rem 1.25rem; margin: .5rem 0 0; }
.bidder-identity dt { color: var(--ink-soft); font-weight: 600; }
.bidder-identity dd { margin: 0; }
/* Keep "Not verified — Resend…" on one line: drop the inline resend form's block box. */
.account-unverified ~ .button_to { display: contents; }

/* Reduced-motion users get the static resting state (mirrors the catalog/console). */
@media (prefers-reduced-motion: no-preference) {
  .activity-card:hover { transform: translateY(-2px); box-shadow: var(--shadow-lift); }
}

/* ----- Bid-triggered auth: modal, pinned banner, held-bid (005) --------- */
/* The pinned "Bid placed!" confirmation (US1, FR-009/010/011). role="status" in the
   markup; here it is a sticky brand-tinted bar that rides the top of the item while
   the bidder reads their just-placed row, then is dismissible. Brand (not the
   reserved --live red) because a placed bid is a positive outcome. */
.placed-bid-banner {
  position: sticky;
  top: 0;
  z-index: 20;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  margin: 0 0 1.5rem;
  padding: .85rem 1.1rem;
  background: var(--brand);
  color: var(--on-brand);
  border-radius: var(--r);
  box-shadow: var(--shadow-lift);
}
.placed-bid-banner-message {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 1.05rem;
  letter-spacing: .01em;
}
.placed-bid-banner-dismiss {
  color: var(--on-brand);
  text-decoration: underline;
  text-underline-offset: 2px;
  font-weight: 500;
  font-size: .92rem;
  white-space: nowrap;
}

/* The native <dialog> auth prompt (US1/US2, FR-001/012/018). It is INERT without JS
   (no `open` attribute); the bid-gate controller calls showModal(). Elevated panel on
   desktop, a bottom sheet on phones (below). The inner area scrolls so a tall sign-in
   + sign-up never clips, while the close glyph stays pinned to the corner. */
.auth-modal {
  position: relative;
  /* display is intentionally NOT set here. A native <dialog> without the `open`
     attribute must stay hidden (UA default: dialog:not([open]) { display: none }) so
     the prompt only appears AFTER the visitor attempts a bid and the bid-gate
     controller calls showModal(). Setting display here would override that and pin the
     modal over the bid area on page load. The open state lays out as a flex column. */
  width: min(30rem, calc(100vw - 2rem));
  max-height: calc(100vh - 2rem);
  max-height: calc(100dvh - 2rem);   /* dvh: account for mobile browser chrome */
  padding: 0;
  overflow: hidden;                  /* inner scrolls; rounded corners stay clean */
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  background: var(--surface);
  color: var(--ink);
  box-shadow: var(--shadow-lift);
}
.auth-modal[open] {
  display: flex;
  flex-direction: column;
}
.auth-modal::backdrop { background: rgba(26, 27, 24, .45); }
.auth-modal-inner {
  flex: 1 1 auto;
  min-height: 0;                     /* lets the flex child actually scroll */
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  padding: 1.75rem 1.6rem;
}
.auth-modal-inner h2 { font-size: 1.45rem; padding-right: 2rem; }
.auth-modal input[type="submit"] { width: 100%; }
/* Out-specify the primary-button rule (mirrored-specificity pattern, wins by source order)
   so this quiet glyph dismiss button is never captured as a solid brand action. */
button.auth-modal-close:not(.link-button):not(.gallery-thumb) {
  position: absolute;
  top: .55rem;
  right: .55rem;
  z-index: 2;
  width: 2.5rem;                     /* 40px+ touch target */
  height: 2.5rem;
  min-height: 0;
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 1.5rem;
  line-height: 1;
  /* A quiet glyph button, not a solid brand action — it dismisses, never submits.
     Surface fill keeps it legible when content scrolls beneath it. */
  color: var(--ink-soft);
  background: var(--surface);
  border: 1px solid transparent;
  border-radius: var(--r);
  cursor: pointer;
}
button.auth-modal-close:not(.link-button):not(.gallery-thumb):hover { background: var(--brand-tint); color: var(--ink); filter: none; }

/* Phones: a bottom sheet — full-width, pinned to the bottom edge, rounded at the top
   only, with safe-area padding so the actions clear the home indicator. */
@media (max-width: 600px) {
  .auth-modal {
    inset: auto 0 0 0;
    width: auto;
    max-width: 100%;
    margin: 0;
    max-height: 92vh;
    max-height: 92dvh;
    border-radius: var(--r-lg) var(--r-lg) 0 0;
    border-bottom: 0;
  }
  .auth-modal-inner {
    padding: 1.5rem 1.25rem;
    padding-bottom: max(1.5rem, env(safe-area-inset-bottom));
  }
}

/* The inline sign-up section under the sign-in form: hairline-separated alternate. */
.auth-alt-form {
  margin-top: 1.5rem;
  padding-top: 1.25rem;
  border-top: 1px solid var(--line);
}
.auth-alt-form h3 { font-size: 1.05rem; }
.auth-alt-form .auth-alt { margin: 1rem 0 0; font-size: .9rem; color: var(--ink-soft); }

/* Item description sits full-width under the gallery + sale instrument. */
.item-description { margin-top: 2rem; max-width: 68ch; }

/* ----- Accessibility helpers -------------------------------------------- */
.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* ----- Motion (opt-in; respects reduced-motion) ------------------------- */
@media (prefers-reduced-motion: no-preference) {
  .auction-card:hover { transform: translateY(-2px); box-shadow: var(--shadow-lift); }

  /* Live-update pulse: the status node and new bid rows are (re)inserted by the
     auction Turbo Stream, so this entry animation plays on each broadcast. Each
     fades from the brand tint to its own resting surface (panel vs. card stock). */
  .auction-status { animation: live-pulse-surface .7s ease-out; }
  @keyframes live-pulse-surface {
    from { background-color: var(--brand-tint); }
    to   { background-color: var(--surface); }
  }
  .bid-row { animation: live-pulse-card .7s ease-out; }
  @keyframes live-pulse-card {
    from { background-color: var(--brand-tint); }
    to   { background-color: var(--card); }
  }

  .auction-status .countdown[data-countdown-state="urgent"],
  .auction-card-closes .countdown[data-countdown-state="urgent"] {
    animation: urgent-pulse 1.6s ease-in-out infinite;
  }
  @keyframes urgent-pulse { 0%,100% { opacity: 1; } 50% { opacity: .55; } }

  /* In-context auth prompt entrance: the panel rises a touch and the backdrop fades,
     so the sign-in surfaces over the item rather than snapping in. */
  .auth-modal[open] { animation: modal-rise .22s ease-out; }
  @keyframes modal-rise {
    from { opacity: 0; transform: translateY(8px); }
    to   { opacity: 1; transform: none; }
  }
  .auth-modal[open]::backdrop { animation: modal-backdrop .22s ease-out; }
  @keyframes modal-backdrop { from { opacity: 0; } to { opacity: 1; } }
}

/* On phones the same prompt slides up as a sheet (reduced-motion users get neither). */
@media (prefers-reduced-motion: no-preference) and (max-width: 600px) {
  .auth-modal[open] { animation: sheet-rise .28s cubic-bezier(.2, .7, .3, 1); }
  @keyframes sheet-rise {
    from { transform: translateY(100%); }
    to   { transform: none; }
  }
}

/* ----- Operator / admin console + dashboards ---------------------------- */
/* The staff consoles (operator + admin) share this vocabulary. The dashboards are
   built from stat tiles + link cards; the tables they link to (auctions, settlements,
   staff, bidders, the queue, the ladder) all ship as bare <table>s, so the table
   primitive below finishes every one of them at once. Same tokens, same voice as the
   public site — accessible by construction (semantic tables, visible focus, non-color
   cues), matching Principle XI on the rest of the app. */

/* Wrapper for the dense console tables: on a narrow viewport a wide multi-column table
   (with an unbreakable email cell) would otherwise force the whole page body to scroll
   sideways. This box scrolls the table in isolation, matching how the app handles other
   wide content. Applied via <div class="table-scroll"> around each console table. */
.table-scroll { overflow-x: auto; }

/* Shared table primitive: styles the bare console <table>s inside the main well. The
   public catalog/detail use cards + lists (never a bare table), so this is scoped to
   the consoles in practice without needing a class on every table. */
main table {
  width: 100%;
  border-collapse: collapse;
  margin: 1rem 0 1.75rem;
  font-size: .95rem;
}
main caption:not(.visually-hidden) {
  text-align: left;
  color: var(--ink-soft);
  font-size: .9rem;
  margin-bottom: .5rem;
}
main thead th {
  text-align: left;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: .72rem;
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--ink-soft);
  padding: .55rem .75rem;
  border-bottom: 2px solid var(--line);
  white-space: nowrap;
}
main tbody th, main tbody td {
  text-align: left;
  vertical-align: top;
  padding: .7rem .75rem;
  border-bottom: 1px solid var(--line);
}
main tbody th[scope="row"] { font-weight: 600; color: var(--ink); }
main tbody tr:hover { background: var(--card); }
/* Action cells often hold several links + button_to forms; keep them on a tidy,
   wrapping row rather than stacking full-width. */
main td form { display: inline-block; margin: 0; }
main td > a, main td > form { margin: 0 .6rem .25rem 0; }

/* The hammer total — the admin-only callout of gross bid value across every auction.
   An auction TICKET: the same string-tied clipped corner as .lot-tag, blown up to a
   placard, with the figure set in the money face (Space Mono). Boldness is spent here
   and nowhere else on the dashboard. The forest ground is gradient-darkened from the
   injected --brand so it stays cohesive whatever house color a deployment sets; a single
   warm --hammer brass (the gavel/auction-house metal) is the only accent, held to the
   eyebrow mark and the hairline "ledger" rule. clip-path gives the real notched corner
   (it clips a drop shadow, so depth comes from the gradient + inset ring instead). */
.hammer-total {
  --hammer: #e5c07b;   /* brass — scoped to this component */
  margin: 0 0 2.25rem;
  padding: 1.5rem 1.75rem 1.6rem;
  color: var(--on-brand);
  background:
    linear-gradient(135deg,
      color-mix(in srgb, var(--brand) 86%, #000) 0%,
      var(--brand) 58%,
      color-mix(in srgb, var(--brand) 80%, #000) 100%);
  border-radius: var(--r-lg);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--hammer) 30%, transparent);
  /* one clipped corner = the string-tied auction tag, scaled up from .lot-tag's 8px */
  clip-path: polygon(20px 0, 100% 0, 100% 100%, 0 100%, 0 20px);
}
.hammer-total-eyebrow {
  display: flex;
  align-items: center;
  gap: .45rem;
  margin: 0 0 .35rem;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: .72rem;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: color-mix(in srgb, var(--hammer) 75%, var(--on-brand));
}
.hammer-total-mark { color: var(--hammer); font-size: .8em; }
.hammer-total-figure {
  margin: 0;
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: clamp(2.4rem, 7vw, 4rem);
  line-height: 1;
  letter-spacing: -.03em;
  /* tabular figures so the digits don't jitter width while the count-up ticks */
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
  color: var(--on-brand);
}
.hammer-total-ground {
  margin: .85rem 0 0;
  padding-top: .8rem;
  border-top: 1px solid color-mix(in srgb, var(--hammer) 40%, transparent);
  font-family: var(--font-mono);
  font-size: .82rem;
  letter-spacing: .01em;
  color: color-mix(in srgb, var(--on-brand) 82%, transparent);
}
@media (max-width: 32rem) {
  .hammer-total { padding: 1.25rem 1.25rem 1.35rem; }
}

/* The dashboard hub sections (stat tiles + link cards). */
.console-hub { margin: 2.25rem 0; }
.console-hub > h2 { margin-bottom: .85rem; }
.console-hub-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: .5rem 1rem;
}
.console-hub-head h2 { margin: 0; }
.console-hub-head p { margin: 0; font-size: .92rem; }

/* At-a-glance stat tiles — the live Action-queue counts. Each tile is one link. */
.console-stats {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr));
  gap: .9rem;
}
.stat-tile {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow);
  overflow: hidden;
  transition: transform .15s ease, box-shadow .15s ease, border-color .15s ease;
}
.stat-tile-link {
  display: block;
  padding: 1.05rem 1.15rem;
  text-decoration: none;
  color: var(--ink);
}
.stat-tile-link:hover { text-decoration: none; }
.stat-number {
  display: block;
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: clamp(1.9rem, 5vw, 2.6rem);
  line-height: 1;
  letter-spacing: -.03em;
  color: var(--ink);
}
.stat-label {
  display: block;
  margin-top: .45rem;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: .72rem;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--ink-soft);
}
/* A tile with nothing to do reads calm — its number recedes to the muted ink so the
   eye lands on the categories that DO have items. */
.stat-tile[data-empty="true"] .stat-number { color: var(--ink-soft); }
/* Time-sensitive categories (due-soon, forfeited) with items draw a --live accent.
   The urgency is ALSO named in the label text ("Due soon"/"Needs decision"), so the
   colour is an enhancement, never the sole signal (WCAG 1.4.1). */
.stat-tile[data-tone="urgent"] {
  border-left: 3px solid var(--live);
  background: color-mix(in srgb, var(--live-tint) 55%, var(--surface));
}
.stat-tile[data-tone="urgent"] .stat-number { color: var(--live-ink); }

/* Link cards — the hub's navigation to the console surfaces. Whole card is one hit
   area via a stretched link on the title (the single described link per card, so AT
   users get one clean labelled link — same technique as the catalog cards). */
.hub-cards {
  list-style: none;
  margin: 1rem 0 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
  gap: 1.1rem;
}
.hub-card {
  position: relative;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  padding: 1.1rem 1.2rem;
  transition: transform .15s ease, box-shadow .15s ease, border-color .15s ease;
}
.hub-card:hover { border-color: color-mix(in srgb, var(--brand) 45%, var(--line)); }
.hub-card-title {
  font-family: var(--font-display);
  font-size: 1.15rem;
  font-weight: 700;
  margin: 0 0 .35rem;
}
.hub-card-title a { color: var(--ink); text-decoration: none; }
.hub-card-title a:hover { text-decoration: underline; text-decoration-color: var(--brand); }
.hub-card-title a::after { content: ""; position: absolute; inset: 0; }
.hub-card-blurb { margin: 0; color: var(--ink-soft); font-size: .92rem; }
.hub-card-meta {
  margin: .7rem 0 0;
  padding-top: .6rem;
  border-top: 1px solid var(--line);
  font-family: var(--font-mono);
  font-size: .8rem;
  color: var(--ink-soft);
}

/* Action queue page. */
.queue-summary {
  margin: .5rem 0 1.75rem;
  padding: .75rem 1rem;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r);
  font-weight: 600;
  color: var(--ink);
}
.queue-group { margin: 2rem 0; }
.queue-group > h2 {
  display: flex;
  align-items: baseline;
  gap: .5rem;
  margin-bottom: .25rem;
}
.queue-count { font-family: var(--font-mono); font-weight: 400; font-size: .95rem; color: var(--ink-soft); }
.queue-group-blurb { margin: 0 0 .75rem; color: var(--ink-soft); font-size: .9rem; max-width: 62ch; }
.queue-empty, .queue-group-empty {
  color: var(--ink-soft);
  font-size: .92rem;
  padding: 1rem 1.1rem;
  background: var(--card);
  border: 1px dashed var(--line);
  border-radius: var(--r);
}
.queue-empty { text-align: center; padding: 2rem 1rem; }
/* A row whose invoice email bounced (FR-025): tint the row + badge, and keep the
   badge text explicit so the flag never relies on colour alone. */
/* The flag tint must survive the generic row-hover rule (which out-specifies a bare
   .queue-item--flagged), so it is written at matching specificity WITH its own hover
   state — the delivery cue stays put exactly when the operator points at the row. */
main tbody tr.queue-item--flagged,
main tbody tr.queue-item--flagged:hover { background: color-mix(in srgb, var(--live-tint) 70%, var(--surface)); }
.queue-badge {
  display: inline-block;
  margin-top: .35rem;
  font-size: .75rem;
  padding: .2rem .5rem;
  border-radius: var(--r-tag);
}
.queue-badge--delivery {
  background: var(--live-tint);
  color: var(--live-ink);
  border: 1px solid color-mix(in srgb, var(--live) 35%, transparent);
}
.queue-muted { color: var(--ink-soft); font-style: italic; }
/* Inline category actions: a tidy wrapping cluster of compact buttons (the global
   primary button is deliberately chunky for the public bid CTA; the queue packs
   several actions per row, so they compact here). Specificity mirrors the primary
   rule so it wins by source order without !important. */
.queue-actions { display: flex; flex-wrap: wrap; gap: .4rem; align-items: center; }
/* Each action is a button_to <form>; zero its margin so the flex gap is the only spacing
   (mirrors .image-card-toolbar). */
.queue-actions form { margin: 0; }
/* Compact the inline action controls. The confirmed hard actions (forfeit early, end
   no sale) render as <input type="submit"> via _confirm_action, NOT <button>, so the
   element-qualified button rule alone left them at the chunky global submit size —
   noticeably larger than their sibling button_to controls. Match both element types at
   a specificity that beats the global `input[type=submit]` / primary-button rule. */
button.queue-action:not(.link-button):not(.gallery-thumb),
input[type="submit"].queue-action {
  padding: .45rem .8rem;
  min-height: 0;
  font-size: .85rem;
}

/* Tabbed action queue (progressive enhancement, US1). With JS off the queue-tabs
   controller never marks the container enhanced, so the tab bar stays hidden and the
   sections render stacked with their headings — exactly the JS-off/rack_test layout.
   The `.queue-tabs-enhanced` class (added on connect) flips to the tab presentation. */
.queue-tabs { display: none; }
.queue-tabs-enhanced .queue-tabs {
  display: flex;
  flex-wrap: wrap;
  gap: .35rem;
  margin: .5rem 0 1.5rem;
  border-bottom: 1px solid var(--line);
}
/* Written at the same specificity as the primary-button rule (input[type=submit],
   button:not(.link-button):not(.gallery-thumb), .btn) so these win by source order —
   otherwise the tabs render as filled primary buttons with no active-state distinction. */
button.queue-tab:not(.link-button):not(.gallery-thumb) {
  appearance: none;
  background: transparent;
  color: var(--ink-soft);
  font: inherit;
  font-weight: 600;
  padding: .55rem .9rem;
  border: 0;
  border-bottom: 3px solid transparent; /* reserve space so the active underline never shifts layout */
  border-radius: 0;
  margin-bottom: -1px; /* overlap the tablist's bottom hairline */
  cursor: pointer;
  min-height: 0;
}
button.queue-tab:not(.link-button):not(.gallery-thumb):hover {
  color: var(--ink);
  background: transparent;
  filter: none;
}
/* Active tab: brand text + a brand underline bar + heavier weight — a position/weight
   cue, not color alone (WCAG 1.4.1), and mirrored programmatically by aria-selected. */
button.queue-tab[aria-selected="true"]:not(.link-button):not(.gallery-thumb) {
  color: var(--brand);
  border-bottom-color: var(--brand);
  font-weight: 700;
}
/* Inherit the tab's text color (never dim with opacity — that dropped the count below
   the 4.5:1 AA contrast floor on inactive tabs). */
.queue-tab .queue-count { color: inherit; font-weight: 400; }
/* Once enhanced, each section IS a tabpanel: drop the stacked top margin and hide the
   now-redundant per-section heading (the tab is the visible label). Kept in the DOM so
   heading navigation and the stat-tile deep-link anchor still resolve. */
.queue-tabs-enhanced .queue-group { margin: 0; }
.queue-tabs-enhanced .queue-group > h2 {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}
.queue-group[role="tabpanel"]:focus-visible { outline: 2px solid var(--brand); outline-offset: 4px; }

/* Sortable column headers: the label becomes a full-width, keyboard-operable button;
   the live direction is carried on the <th> via aria-sort and cued by a non-color
   arrow glyph (WCAG 1.4.1 — never color alone). Non-sortable headers (Actions) have
   no button and render as plain text. */
/* Sort headers are <button>s (the WAI-ARIA sortable pattern) but must read as plain header
   text, not primary buttons. This reset out-specifies the primary-button rule the same way
   .queue-tab/.btn-quiet/.btn-danger do — the extra :not()s take it to (0,4,2), comfortably
   above the primary rule's (0,3,1), so it can't be silently overtaken when that rule's
   negation list changes (the PR #54 regression: the old 2-class selector lost when a third
   :not() was added). */
.sortable-table th button.table-sort-button:not(.link-button):not(.gallery-thumb) {
  appearance: none;
  border: 0;
  background: transparent;
  font: inherit;
  font-weight: inherit;
  color: inherit;
  text-align: inherit;
  width: 100%;
  padding: 0;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: .35rem;
}
.sortable-table th button.table-sort-button:not(.link-button):not(.gallery-thumb):hover { color: var(--brand); }
.table-sort-indicator { font-size: .7rem; color: var(--ink-soft); }
/* Idle glyph is a faint double-arrow affordance; the active column shows the direction. */
.sortable-table th[aria-sort="none"] .table-sort-indicator::after { content: "\2195"; opacity: .4; }
.sortable-table th[aria-sort="ascending"] .table-sort-indicator::after { content: "\2191"; }
.sortable-table th[aria-sort="descending"] .table-sort-indicator::after { content: "\2193"; }
.sortable-table th[aria-sort]:not([aria-sort="none"]) .table-sort-indicator { color: var(--brand); }

/* Per-auction settlement view: give each top-level section a panel so the dense
   settlement context reads as ordered cards rather than an undivided wall. */
.settlement-page > article > section {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow);
  padding: 1.25rem 1.35rem;
  margin: 1.25rem 0;
}
.settlement-page > article > section > h2:first-child { margin-top: 0; }
.settlement-current-invoice dl, .settlement-next-bidder { margin: 0; }
.settlement-current-invoice dt, .settlement-next-bidder dt {
  font-size: .72rem;
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--ink-soft);
  margin-top: .75rem;
}
.settlement-current-invoice dt:first-child, .settlement-next-bidder dt:first-child { margin-top: 0; }
.settlement-current-invoice dd, .settlement-next-bidder dd { margin: .1rem 0 0; }
/* Status flags: warnings (reserve-not-met, forfeited) read as notices; the cascade
   position is informational (brand tint). */
.settlement-reserve-flag, .settlement-forfeit-flag {
  margin: .75rem 0 0;
  padding: .7rem 1rem;
  background: var(--live-tint);
  border: 1px solid color-mix(in srgb, var(--live) 35%, transparent);
  border-radius: var(--r);
  color: var(--live-ink);
}
.settlement-top-bidder-forfeits, .settlement-next-bidder-forfeits { color: var(--ink-soft); font-size: .9rem; }
/* The rung the next invoice would go to, highlighted in the ladder (brand left-rule +
   tint, echoing the bid-ledger leader row). aria-current in the markup carries it to AT. */
.settlement-ladder-row--cascade-position { background: var(--brand-tint); }
.settlement-ladder-row--cascade-position th[scope="row"] { box-shadow: inset 3px 0 0 var(--brand); }
.settlement-cascade-marker { font-weight: 600; color: var(--brand); }
/* Resolved rungs recede so the eye lands on pending/eligible ones. */
.settlement-ladder-row--forfeited,
.settlement-ladder-row--forfeited_by_ban,
.settlement-ladder-row--skipped { color: var(--ink-soft); }
/* ...including the two most salient cells, which otherwise keep their own color (the
   name link its link color, the row-header <th> the default ink) and defeat the mute. */
.settlement-ladder-row--forfeited th[scope="row"], .settlement-ladder-row--forfeited a,
.settlement-ladder-row--forfeited_by_ban th[scope="row"], .settlement-ladder-row--forfeited_by_ban a,
.settlement-ladder-row--skipped th[scope="row"], .settlement-ladder-row--skipped a { color: var(--ink-soft); }
.settlement-history-list, .settlement-auction-forfeits-list { list-style: none; margin: 0; padding: 0; }
.settlement-history-event {
  padding: .55rem 0;
  border-bottom: 1px solid var(--line);
  font-size: .92rem;
}
.settlement-history-event:last-child { border-bottom: none; }
.settlement-history-actor { font-weight: 600; }
.settlement-history-time { font-family: var(--font-mono); font-size: .82rem; color: var(--ink-soft); margin-left: .3rem; }
/* Each settlement action's descriptive paragraph sits just under its button. */
.settlement-actions form + p, .settlement-actions > p { max-width: 68ch; }

/* Console hover motion (reduced-motion users get the static resting state). */
@media (prefers-reduced-motion: no-preference) {
  .stat-tile:hover, .hub-card:hover { transform: translateY(-2px); box-shadow: var(--shadow-lift); }
}

/* ----- Operator: auction-item editor + picture manager ------------------ */
/* The create/edit surface and its picture desk, built entirely from existing tokens
   and shared primitives (.account-panel, .field, .eyebrow, .btn-quiet, .btn-danger)
   so the operator sheet reads as one system with the bidder catalog. The picture
   manager is a scannable contact sheet: a grid of thumbnail cards whose frequent
   actions (cover / reorder / remove) sit in an always-visible toolbar, with the heavy
   crop + description editors tucked behind a native <details> that unfolds the card to
   full width. Every action is a real form (button_to / form_with), so the whole
   surface is fully operable with JS off (Principle XI). */

/* .badge and .notice were referenced by views (the ★ Cover chip, the manager status
   line) but never defined — define them here. .badge doubles as the locked-terms flag. */
.badge {
  display: inline-flex;
  align-items: center;
  gap: .3rem;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: .62rem;
  letter-spacing: .07em;
  text-transform: uppercase;
  padding: .28rem .55rem;
  background: var(--brand);
  color: var(--on-brand);
  border-radius: var(--r-tag);
}
/* The badge's display:inline-flex (an author rule) otherwise overrides the UA
   [hidden]{display:none}, so a toggled `hidden` badge — e.g. the staging tray's cover
   chip on non-cover cards — would stay visible. Restore the hide at higher specificity. */
.badge[hidden] { display: none; }
.notice {
  background: var(--brand-tint);
  border: 1px solid color-mix(in srgb, var(--brand) 30%, transparent);
  border-radius: var(--r);
  padding: .6rem .85rem;
  color: var(--ink);
}

/* Editor page shell: a single readable column with an eyebrow-led header. */
.editor-page { max-width: 52rem; margin: 0 auto; }
.editor-header { margin-bottom: 1.5rem; }
.editor-header h1 { margin: .15rem 0 0; }
.editor-header-sub { color: var(--ink-soft); margin: .4rem 0 0; }
.editor-header-actions { display: flex; flex-wrap: wrap; gap: .6rem; margin-top: 1rem; }
.editor-page > .account-panel + .account-panel { margin-top: 1.5rem; }

/* Form: full-width text fields, borderless eyebrow-legend sections, a responsive grid
   for the money + schedule rows, and a hairline-topped action footer. */
.editor-page .field textarea { max-width: none; min-height: 8rem; resize: vertical; }
.form-section { border: 0; margin: 0 0 1.25rem; padding: 0; }
.form-section > legend {
  width: 100%;
  font-family: var(--font-display);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .06em;
  font-size: .72rem;
  color: var(--ink-soft);
  border-top: 1px solid var(--line);
  padding: 1.1rem 0 0;
  margin-bottom: .85rem;
}
.form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr)); gap: 0 1.25rem; }
/* Lift the global 26rem input cap inside the grid so cells fill their column. */
.form-grid .field input { max-width: none; }
/* Every `datetime-local` control on the site opts into this: the two auction schedule
   fields (start_at / end_at) and the settlement payment-deadline field (new_due_at).
   `datetime-local` is NOT in the shared text-input rule near the top of this file, so they
   rendered at the UA default: a short, borderless control with a ~13px font. On the auction
   form that is invisible on desktop with JS on (the datetime-picker controller hides the
   native input behind its proxy) but it IS what a phone gets, because the controller
   deliberately keeps the OS picker on coarse pointers, and it is what everyone gets with JS
   off. The settlement field has no picker at all, so it is what every device gets there.
   Give them the same box as every other field. Opt-in by class rather than widening that
   shared selector to the type, so a datetime input that wants a different treatment later
   can simply not ask for this one. */
input.schedule-input {
  font: inherit;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r);
  padding: .6rem .7rem;
  width: 100%;
  max-width: 26rem;
}
/* Touch and narrow viewports only, so the desktop row keeps its current height and the
   schedule fields stay flush with the price fields beside them. The 30rem breakpoint
   matches NARROW_VIEWPORT in datetime_picker_controller.js. The controller copies this
   class onto its visible proxy, so the enhanced control gets the same floor. */
@media (pointer: coarse), (max-width: 30rem) {
  input.schedule-input {
    min-height: 44px;
    /* iOS Safari zooms the page when a control smaller than 16px takes focus. `font: inherit`
       above already resolves to 16px today; this floors it so an ancestor font change can't
       silently reintroduce the zoom on the one control a phone user must operate directly. */
    font-size: max(1rem, 16px);
  }
}
.locked-summary {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: .4rem .6rem;
  background: var(--brand-tint);
  border: 1px solid color-mix(in srgb, var(--brand) 25%, transparent);
  border-radius: var(--r);
  padding: .7rem .9rem; margin: 0 0 1.25rem;
}
/* Neutral flag — a locked state, not a brand action. */
.locked-summary .badge { background: var(--ink-soft); }
.form-actions {
  display: flex; flex-wrap: wrap; align-items: center; gap: .75rem;
  margin-top: 1.5rem; padding-top: 1.25rem; border-top: 1px solid var(--line);
}

/* Picture manager. The status line is a STABLE node rendered outside the Turbo-replaced
   grid (edit.html.erb), so screen-reader announcements fire when its text is updated —
   a live region that is itself torn down and re-inserted each cycle is not announced. */
.image-manager-status:empty { display: none; }
.image-manager-status { margin-bottom: 1rem; }
.image-count { color: var(--ink-soft); }
.image-grid {
  list-style: none; margin: 1.25rem 0 1.5rem; padding: 0;
  display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 1rem;
}
.image-empty {
  margin: 1rem 0 1.5rem; padding: 2rem; text-align: center;
  color: var(--ink-soft); background: var(--card);
  border: 1px dashed var(--line); border-radius: var(--r-lg);
}

/* One thumbnail card. */
.image-card {
  display: flex; flex-direction: column; gap: .6rem;
  background: var(--card); border: 1px solid var(--line);
  border-radius: var(--r-lg); padding: .7rem; box-shadow: var(--shadow);
}
/* Single cover signal: an inset brand ring (the border stays the neutral hairline). */
.image-card.is-cover { box-shadow: inset 0 0 0 2px var(--brand), var(--shadow); }
.image-card figure { position: relative; margin: 0; }
.image-card figure img {
  display: block; width: 100%; aspect-ratio: 4 / 3; object-fit: cover;
  border-radius: var(--r); border: 1px solid var(--line); background: var(--surface);
}
.image-card figcaption { margin-top: .45rem; font-family: var(--font-mono); font-size: .75rem; color: var(--ink-soft); }
/* The cover chip as a notched auction-ticket, echoing the catalog's .lot-tag. */
.image-cover-badge {
  position: absolute; top: .5rem; left: .5rem; z-index: 1; box-shadow: var(--shadow);
  clip-path: polygon(8px 0, 100% 0, 100% 100%, 0 100%, 0 8px);
}

/* Toolbar: each button_to renders its own <form>, so lay the forms out as flex items. */
.image-card-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: .55rem; }
.image-card-toolbar form { margin: 0; }
.image-reorder { display: flex; gap: .35rem; }
/* Isolate the destructive control at the far right, away from the benign group. */
.image-card-toolbar .image-remove-form { margin-left: auto; }
/* Compact the toolbar buttons. Written at the primary-button rule's specificity and
   appearing later in the file, so it wins on source order over both the global submit
   rule and the .btn-danger rule (application.css:277) without !important — the same
   technique as .queue-action. Colour still comes from those rules; only size changes.
   min-height floors the target at ~36px (never the chunky 44px, never sub-24px). */
.image-card-toolbar input[type="submit"],
.image-card-toolbar button:not(.link-button):not(.gallery-thumb),
.image-card-toolbar .btn {
  padding: .4rem .75rem; font-size: .85rem; min-height: 2.25rem;
}

/* The crop + description drawer, collapsed by default (native <details>). */
.image-drawer { border-top: 1px solid var(--line); padding-top: .55rem; }
.image-drawer > summary {
  cursor: pointer; list-style: none; display: flex; align-items: center; gap: .5rem;
  font-family: var(--font-display); font-weight: 700; font-size: .72rem;
  letter-spacing: .06em; text-transform: uppercase; color: var(--ink-soft);
}
.image-drawer > summary::-webkit-details-marker { display: none; }
.image-drawer > summary::after { content: "＋"; margin-left: .1rem; color: var(--ink-soft); font-weight: 400; }
.image-drawer > summary:hover { color: var(--brand); }
.image-drawer[open] > summary { color: var(--brand); }
.image-drawer[open] > summary::after { content: "–"; }
.image-drawer > summary:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.drawer-flag {
  margin-left: auto; font-family: var(--font-mono); font-weight: 400;
  text-transform: none; letter-spacing: 0; font-size: .72rem; color: var(--ink-soft);
}
.image-drawer__body { padding-top: .85rem; }

/* When a card's drawer opens, break it to full grid width and (on wider viewports)
   re-lay it as media | editor so the crop stage gets real room. Depends on :has()
   (net-new to this stylesheet, supported by all current evergreen browsers); where
   unsupported the drawer just opens within its own column — still fully usable, since
   the crop preview is capped at max-width:100%. */
.image-card:has(.image-drawer[open]) {
  grid-column: 1 / -1;
  box-shadow: var(--shadow-lift);
  border-color: color-mix(in srgb, var(--brand) 40%, var(--line));
}
.image-card.is-cover:has(.image-drawer[open]) { box-shadow: inset 0 0 0 2px var(--brand), var(--shadow-lift); }
@media (min-width: 46rem) {
  .image-card:has(.image-drawer[open]) {
    display: grid;
    grid-template-columns: 240px 1fr;
    grid-template-areas: "figure editor" "toolbar editor";
    align-items: start; column-gap: 1.5rem; row-gap: .6rem;
  }
  .image-card:has(.image-drawer[open]) > figure { grid-area: figure; }
  .image-card:has(.image-drawer[open]) > .image-card-toolbar { grid-area: toolbar; align-content: start; }
  .image-card:has(.image-drawer[open]) > .image-drawer { grid-area: editor; border-top: 0; padding-top: 0; }
}

/* Crop + description editors inside the drawer. */
.alt-text-form { display: grid; gap: .55rem; margin-bottom: 1rem; }
.alt-text-form .field { margin: 0; }
.alt-text-form input { max-width: none; }
/* Borderless like the form sections so the open drawer reads as one calm surface. */
.crop-form fieldset { border: 0; padding: 0; margin: 0 0 .8rem; }
.crop-form legend {
  font-family: var(--font-display); font-weight: 700; text-transform: uppercase;
  letter-spacing: .06em; font-size: .7rem; color: var(--ink-soft); padding: 0; margin-bottom: .5rem;
}
.crop-fields { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .5rem 1rem; }
.crop-fields .field { margin: 0; }
.crop-fields input { max-width: none; width: 100%; }
.image-clear-crop { margin-top: .2rem; }

/* Upload zone: a dashed drop target (the submit sits inside, so the affordance is
   honest even with JS off — it is a click-to-browse area). The file input stays
   visible so keyboard users and attach_file both reach it. */
.image-upload {
  margin-top: .5rem; padding: 1.25rem;
  border: 2px dashed color-mix(in srgb, var(--brand) 35%, var(--line));
  border-radius: var(--r-lg); background: var(--card);
}
.image-upload .field { margin: 0; }
.image-upload input[type="file"] { max-width: none; }
.image-upload input[type="submit"] { margin-top: 1rem; }
.image-upload:focus-within { border-color: var(--brand); background: var(--brand-tint); }
.image-upload.is-dragover { border-color: var(--brand); background: var(--brand-tint); }

/* ----- Create-page image staging (specs/007-clientside-image-staging) ---- */
/* The client-side tray shown on the NEW auction form. Reuses .image-grid / .image-card /
   .image-cover-badge so the create picture desk and the edit one read as one system. */
.image-staging .staging-add { margin-top: .75rem; }
.image-staging__tray { margin: 1rem 0 .75rem; }
.staging-empty { margin: 1rem 0; }
.staged-image { gap: .55rem; cursor: default; }
.staged-image__figure { position: relative; margin: 0; }
.staged-image__thumb {
  display: block; width: 100%; aspect-ratio: 4 / 3; object-fit: cover;
  border-radius: var(--r); border: 1px solid var(--line); background: var(--surface);
}
.staged-image__caption { margin-top: .45rem; font-family: var(--font-mono); font-size: .75rem; color: var(--ink-soft); }
.staged-image--cover { box-shadow: inset 0 0 0 2px var(--brand), var(--shadow); }
.staged-image.is-dragging { opacity: .5; }
.staged-image__toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: .4rem; }
.staged-image__toolbar form { margin: 0; }
/* Isolate the destructive control at the far right, away from the benign group. */
.staged-image__remove { margin-left: auto; }
/* Compact the toolbar buttons — same floor as the edit-page toolbar (min ~36px target). */
.staged-image__toolbar input[type="submit"],
.staged-image__toolbar button:not(.link-button):not(.gallery-thumb),
.staged-image__toolbar .btn {
  padding: .4rem .7rem; font-size: .82rem; min-height: 2.25rem;
}
.staged-image__alt { margin: 0; }
.staged-image__alt label { font-size: .82rem; }
.staged-image__alt input { max-width: none; }

/* ----- Shared visual crop dialog (create staging + edit drawer) ---------- */
/* A native <dialog> (INERT without JS — no `open` attr; the cropper controller calls
   showModal). Chrome, backdrop, and Cropper.js's own web-component parts are tamed to the
   forest-green operator surface so the crop stage never reads as a bolted-on widget. */
.crop-adjust { margin-bottom: .8rem; }
.crop-dialog {
  width: min(46rem, calc(100vw - 2rem));
  max-height: calc(100dvh - 2rem);
  padding: 0; border: 1px solid var(--line); border-radius: var(--r-lg);
  background: var(--surface); color: var(--ink); box-shadow: var(--shadow-lift);
  overflow: hidden;
}
.crop-dialog::backdrop { background: rgba(26, 27, 24, .45); }
.crop-dialog__panel { display: flex; flex-direction: column; gap: .75rem; padding: 1.25rem 1.4rem; }
.crop-dialog__title { font-family: var(--font-display); margin: 0; }
.crop-dialog__hint { margin: 0; color: var(--ink-soft); font-size: .9rem; }
.crop-dialog__stage {
  background: var(--paper); border: 1px solid var(--line); border-radius: var(--r);
  overflow: hidden;
}
/* Cropper.js v2 web components — style the light-DOM host + exposed custom properties
   to the brand (Shadow DOM keeps the internals encapsulated; these are the public hooks). */
.crop-dialog__stage cropper-canvas { display: block; width: 100%; height: min(56vh, 460px); }
.crop-dialog__stage cropper-selection { outline: 1px solid var(--brand); outline-offset: -1px; }
.crop-dialog__stage cropper-handle { --cropper-handle-background-color: var(--brand); }
.crop-dialog__stage cropper-grid { --cropper-grid-color: rgba(255, 255, 255, .5); }
.crop-dialog__stage cropper-shade { --cropper-shade-background-color: rgba(26, 27, 24, .45); }
.crop-dialog__fields {
  border: 0; padding: 0; margin: 0;
  display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: .5rem .75rem;
}
.crop-dialog__fields .field { margin: 0; }
.crop-dialog__fields label { font-size: .8rem; }
.crop-dialog__fields input { max-width: none; width: 100%; }
.crop-dialog__actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: .5rem; }

@media (min-width: 600px) {
  .crop-dialog { width: min(46rem, calc(100vw - 4rem)); }
}
@media (prefers-reduced-motion: no-preference) {
  .crop-dialog[open] { animation: modal-rise .2s ease-out; }
  .crop-dialog[open]::backdrop { animation: modal-backdrop .2s ease-out; }
  /* Cards ease into the tray + slide on reorder, matching the app's motion vocabulary. */
  .staged-image { transition: box-shadow .18s ease, opacity .18s ease; }
}

/* ── Admin TV display (specs/007-tv-display) ─────────────────────────────────
   A full-bleed, dark, room-distance dashboard. Reduced-motion is honored in the
   feed callout (see .tv-bid--entered). Not part of the standard app chrome. */
.tv-body { margin: 0; background: #0b0b0f; color: #f5f5f7; }
.tv-display { min-height: 100vh; padding: 2vw 3vw; box-sizing: border-box;
  display: grid; grid-template-rows: auto auto 1fr; gap: 2vh; }
.tv-header { display: flex; align-items: center; gap: 1.5vw; }
.tv-logo { height: 6vh; width: auto; }
.tv-site-name { font-size: clamp(2rem, 4vw, 4rem); margin: 0; letter-spacing: 0.01em; }
.tv-total { text-align: center; }
.tv-total-eyebrow { text-transform: uppercase; letter-spacing: 0.2em; opacity: 0.7; margin: 0 0 0.5vh; }
.tv-total .hammer-total-figure { font-size: clamp(3rem, 9vw, 9rem); font-weight: 700; margin: 0; line-height: 1; }
.tv-total .hammer-total-ground { opacity: 0.7; margin: 0.5vh 0 0; font-size: clamp(1rem, 1.6vw, 1.6rem); }
.tv-columns { display: grid; grid-template-columns: 1fr 1fr; gap: 3vw; min-height: 0; }
.tv-panel-heading { text-transform: uppercase; letter-spacing: 0.15em; opacity: 0.7;
  font-size: clamp(1rem, 1.6vw, 1.6rem); margin: 0 0 1.5vh; }
.tv-feed-list, .tv-ending-list { list-style: none; margin: 0; padding: 0;
  display: flex; flex-direction: column; gap: 1vh; }
.tv-bid, .tv-ending-item { display: grid; grid-template-columns: 1fr auto auto; gap: 1.5vw;
  align-items: baseline; padding: 1.4vh 1.6vw; background: #16161d; border-radius: 0.6vw;
  font-size: clamp(1.2rem, 2.1vw, 2.4rem); }
.tv-bid-amount, .tv-ending-price { font-weight: 700; }
.tv-bid-time { opacity: 0.55; font-size: 0.7em; }
.tv-ending-item { grid-template-columns: 1fr auto auto; }
.tv-ending-clock { font-variant-numeric: tabular-nums; }
.tv-ending-clock .countdown[data-countdown-state="closing"] { color: #ff6b6b; }
.tv-feed-empty, .tv-ending-empty { opacity: 0.6; padding: 1.4vh 1.6vw; }

@keyframes tv-bid-enter {
  0%   { background: var(--brand-primary, #4f7cff); transform: translateY(-0.6vh); }
  100% { background: #16161d; transform: translateY(0); }
}
.tv-bid--entered { animation: tv-bid-enter 1.8s ease-out; }
@media (prefers-reduced-motion: reduce) {
  .tv-bid--entered { animation: none; }
}

/* ----- Rails mailer previews ------------------------------------------- */
.mailer-preview-list { grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr)); }
.mailer-preview-actions { margin: 0; padding-left: 1.1rem; }
.mailer-preview-actions li + li { margin-top: .3rem; }

/* The rendered preview uses layout: false so the metadata controls and the isolated
   email iframe need their own full-height shell. The tokens still come from the site
   stylesheet; only this frame owns its layout. */
.mailer-preview { min-height: 100vh; display: flex; flex-direction: column; }
.mailer-preview .site-nav { max-width: none; }
.mailer-preview-main { width: 100%; max-width: none; flex: 1; display: flex; flex-direction: column; padding: 1.25rem; gap: 1.25rem; }
.mailer-preview-meta { background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-lg); box-shadow: var(--shadow); padding: 1rem 1.25rem; }
.mailer-preview-meta dl { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: .45rem 1rem; margin: 0; }
.mailer-preview-meta dt { font-size: .72rem; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--ink-soft); }
.mailer-preview-meta dd { min-width: 0; margin: 0; overflow-wrap: anywhere; }
.mailer-preview-meta select { width: auto; max-width: 100%; padding: .35rem .5rem; }
.mailer-preview-meta table { margin: .5rem 0 0; }
.mailer-preview iframe { flex: 1; min-height: 32rem; width: 100%; border: 1px solid var(--line); border-radius: var(--r-lg); background: var(--surface); }
@media (max-width: 600px) { .mailer-preview-main { padding: 1rem; } .mailer-preview-meta { padding: 1rem; } .mailer-preview-meta dl { grid-template-columns: 1fr; gap: .15rem; } .mailer-preview-meta dt:not(:first-child) { margin-top: .6rem; } }
